# My Details (/me) View your details and projects in Descope # Your details # Policies (/policies) Define authorization policies across all Descope Applications and Agentic Clients. # Policies **Policies** are the authorization rules that govern what each subject (a client, an agent, or a user) can access across your Descope project. A policy answers three questions: **who** the rule applies to, **what** Resources or Connections it grants access to, and **how** that access is obtained (the grant type). Policies can apply to every [Inbound Application](/identity-federation/inbound-apps) and [Agentic Client](/agentic-identity-hub/core-components/clients) in your Descope project. You can manage your policies on the [Policies page](https://app.descope.com/policies) in the Descope Console. From there you can: - **Create policies** that grant specific subjects access to specific Resources and Connections, with specific scopes - **View all active policies** and understand which rules are currently in effect across your project - **Delete policies** to immediately remove their effect; there is no disabled state; a policy is either active or gone ![Policies page](/assets/policies-page.webp) Policies follow an **allow** model with least-privilege defaults. Scopes are only granted when an active policy explicitly permits them, and policies take effect as soon as they are created. There is no staging or preview mode. Policies that are created or removed take effect immediately. ## Policies and User Consent Two levels of control decide what an OAuth client or agent ends up with: - **Policies** are the admin level: what an IT admin or Descope administrator allows in the Descope Console. They set the outer boundary of which subjects may receive which scopes on which Resources and Connections. - **User consent** is the end-user level: what the person invoking the client or agent approves on the consent screen during the authorization code flow. Policy always wins. If a policy prohibits clients, or clients acting on behalf of users, from consenting to certain scopes, those scopes **do not appear on the consent screen** at all; the user can only consent within what the admin has allowed. Grant types with no user in the loop, such as token exchange and `client_credentials`, skip consent entirely, so policy is the only control. ## How a Policy Is Structured Every policy is authored in the same way, regardless of the application or grant type it governs: 1. **Rule details**: a name and description 2. **Subjects**: who the rule applies to 3. **Targets**: the Resources or Connections the rule grants access to 4. **Grant types**: how the access is obtained ### 1. Rule Details Start by giving the policy a **rule name** and a **description**. These document the policy's purpose so anyone reviewing your project's access model can understand what it does and why it exists. ![Rule details](/assets/policies-rule-details.webp) ### 2. Subjects **Subjects** define who the policy applies to. Choose one of: | Subject option | Description | |---|---| | **Any** | The policy applies to every subject. Use this for baseline access that should be available to all clients, agents, or users. | | **Select clients** | The policy applies only to specific clients. A searchable dropdown lists all of your clients; search by **name** or **client ID** to select one or more. | | **Custom conditions** | The policy applies to subjects that match a set of conditions evaluated against user attributes, tenant context, JWT claims, and client metadata. | ![Subjects](/assets/policies-subjects.webp) ![Custom conditions](/assets/policies-custom-conditions.webp) #### Custom Conditions When you choose **Custom conditions**, you build one or more conditions that are all evaluated together with logical `AND`. These are the same conditions used throughout Descope policies. **Supported condition keys**: | Condition Key | Description | |---|---| | `user.roles` | Roles assigned to the authenticating user | | `user.tenantIds` | Tenants the user belongs to | | `user.permissions` | Permissions assigned to the user | | `jwtClaims.` | Any claim included in the inbound JWT | | `client.tags` | Tags assigned to the OAuth client during onboarding | | `client.name` | The name of the client (e.g., Claude, ChatGPT, or a custom name) | | `client.clientId` | The client ID of the client | | `client.registrationType` | The [registration type](/agentic-identity-hub/core-components/mcp-servers/registration-methods) of the client | | `client.status` | The status of the client, either `Verified` or `Unverified` | **Supported operators**: | Operator | Meaning | |---|---| | **Equal** | Exact value match | | **Not equal** | Must not match value | | **In** | Value exists in list | | **Not in** | Value does not exist in list | | **Contains** | Substring or array membership match (requires value) | To drive policies from SSO groups (from providers like Okta or Azure AD), first map those groups to Descope [roles](/authorization/role-based-access-control) in your tenant's Roles & Groups tab. Once mapped, reference them with the `user.roles` condition key. See [SSO User and Group Mapping](/sso/sso-mapping) for setup details. ### 3. Targets **Targets** are the [Resources](/resources) or [Connections](/agentic-identity-hub/core-components/connections) the policy grants access to. A Resource is any backend API or MCP server you've registered (an MCP server is simply a type of Resource), so the same model governs an agent calling your API and an agent invoking MCP tools. For each target you select **Any** or **Specified**: | Target option | Description | |---|---| | **Any** | The policy applies to all Resources (or all Connections). | | **Specified** | The policy applies only to the Resources (or Connections) you select. For each selected target, choose the specific **scopes** within it that this policy grants. | When you choose **Specified**, you pick the individual Resources (or Connections) and then, within each one, the exact scopes to grant. This is how you express least-privilege access: for example, granting read-only scopes on one Resource while withholding write scopes. ![Targets](/assets/policies-targets.webp) The set of scopes you can grant comes from the [Resource](/resources) or [Connection](/agentic-identity-hub/core-components/connections) itself. A policy can only grant scopes that already exist on the target. ### 4. Grant Types A policy specifies which **grant types** it applies to. This controls the kind of access the subject is obtaining when the policy is evaluated: | Grant type | Description | |---|---| | **Delegated access (token exchange)** | A subject exchanges an existing token for a new one scoped to the target ([RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693)). Used when an application or agent acts on behalf of a user or another identity to reach a Resource or Connection. | | **Machine-to-Machine (M2M) access** | A subject authenticates as itself using the `client_credentials` flow, with no user present. Used for service-to-service and autonomous agent access. | | **User access** | An end user authenticates directly (authorization code, password, OTP, magic link, SSO, etc.) and the access token is issued to act as that user. | A single policy can cover one or more grant types. The effective scopes a subject receives are the intersection of: the scopes the target defines, the scopes the matching policy grants, and, for user access, the scopes the user consents to. ![Grant types](/assets/policies-grant-types.webp) ## How Policies Are Enforced Policies are evaluated at the moment an access token is issued or exchanged. The **grant type** determines whether a consent screen is involved: - **User access** (authorization code) is the only flow that presents a **consent screen**. The policy will block the issuance of a token if a user attempts to grant consent to scopes that are not allowed by the policy. - **Delegated access** (token exchange) and **M2M** (`client_credentials`) never present a consent screen. The policy is evaluated during the token request and simply allows or blocks delivery of the token, trimming its scopes, before it is returned. Your Resource (API or MCP server) then [validates the token](/sessions/validation), checking `iss`, `exp`, `aud`, and `scope`, before serving any request. Downstream third-party services enforce the scopes on [Connection](/agentic-identity-hub/core-components/connections) tokens when an agent calls their APIs. ## Policy Violations and Audit Events When a client or agent requests scopes on a [Resource](/resources) or [Connection](/agentic-identity-hub/core-components/connections) that no active policy permits, Descope **denies** the request at the token boundary. The access token is not issued, not exchanged, or not expanded with the forbidden scopes. Each denial is recorded as a **Warning** [audit event](/audit-trails-and-integrations/audit-events#policy-violations). Common triggers include: - **Resource scopes**: An MCP client or agent requests scopes on an API or MCP server during authorization, token issuance, or token exchange, but no policy grants those scopes for that subject. - **Connection scopes**: A client or Resource exchanges a token for a Connection credential and the requested Connection or scopes are not permitted. - **User consent**: A user attempts to approve scopes on the consent screen that policy forbids (authorization code flow). Consent fails and no token is issued. The **Data** section of each event includes the client, target Resource or Connection, requested scopes, and the policy evaluation outcome: enough to debug misconfigured clients, stale consents, or missing policies. Monitor violations in the [Agentic Activity Dashboard](/management/project-settings/project-dashboard#agentic-activity-dashboard) for aggregated counts by MCP server and Connection, or search the full [audit trail](https://app.descope.com/audits) and [Search Audit API](/api/management/audit/search-audit). ## Examples #### Read-only API access for a specific client ``` Rule name: Reporting Client - Read Only Subjects: Select clients → "Reporting Dashboard" Targets: Specified → Reports API Scopes: reports.read Grant types: User access ``` #### Tenant-scoped delegated access ``` Rule name: Finance Tenant Delegated Access Subjects: Custom conditions user.tenantIds IN ["corp_finance", "corp_ops"] Targets: Specified → Invoicing API Scopes: invoice.read, invoice.create Grant types: Delegated access (token exchange) ``` #### M2M access for a verified service ``` Rule name: Sync Service - M2M Subjects: Custom conditions client.tags CONTAINS "verified-service" Targets: Specified → Inventory API Scopes: inventory.read, inventory.write Grant types: Machine-to-Machine (M2M) access ``` # Console Search (/console-search) Search the Descope Console from the keyboard to jump to any page, open your own resources, or start a common action. # Console Search Console Search is a search dialog that opens over any page in the Descope Console. Use it to reach a page or a resource without going to the sidebar, or to start an action such as inviting a user. ## Opening Console Search Press `cmd + k` on macOS, or `ctrl + k` on Windows and Linux. You can also click **Search** in the top bar of the Console. ![The Search button and its keyboard shortcut tooltip in the Console top bar](/assets/console-search-button.webp) The dialog opens with a **Recently Used** section listing the last commands you ran. ![Console Search open, showing recently used commands](/assets/console-search-open.webp) ## Navigating to Pages Type part of a page name to find it. Console Search covers every page and sub-page in the Console. Each result carries a breadcrumb showing where the page sits, so you can tell apart pages that share a name. Searching for `attributes`, for example, returns both **Users → Custom Attributes** and **Tenants → Custom Attributes**. ![Search results grouped into Best Match and Suggestions, each with a breadcrumb](/assets/console-search-results.webp) ## Finding Your Own Resources Console Search also matches resources in your project by name: - Flows - Connectors - SSO applications - MCP servers - JWT templates - Lists Selecting a result opens that resource. Typing a project name returns a **Go to \** result that switches you to that project. ## Running Actions Results include actions as well as pages. Selecting **Invite User** takes you to the Users page and opens the invite dialog for you. Actions cover the create, invite, import, and export operations across the Console. Actions match alternate wording, so **New User**, **Add User**, and **Create User** all return the same result. ## Keyboard Navigation | Key | Action | | --- | --- | | `cmd + k` / `ctrl + k` | Open Console Search | | `up` / `down` | Move between results | | `enter` | Run the selected result | | `esc` | Close Console Search | ## Results and Permissions Console Search shows only the pages and actions your Descoper role permits. If your role grants view access to a section but not edit access, the page itself appears in results while its create and edit actions do not. See [Custom Descoper Roles](/management/company-settings#custom-descoper-roles) for how these roles are assigned. Pages you have hidden with [Sidebar Preferences](/sidebar-preferences) still appear in Console Search. Sidebar Preferences change the sidebar only. # Sidebar Preferences (/sidebar-preferences) Personalize which pages appear in your Descope Console sidebar. # Sidebar Preferences Sidebar Preferences let you control which pages appear in your Descope console sidebar, so you can hide the pages you don't use and focus on the ones you do. Sidebar Preferences only change what you see. To control what Descopers are allowed to access, use [Custom Descoper Roles](/management/company-settings#custom-descoper-roles) in [Company Settings](https://docs.descope.com/settings/company/admins). ## Using Sidebar Preferences You can open the sidebar preferences dialog from two different places in the Descope Console. 1. Click your name in the top right of the Console, then select **Sidebar Preferences**: ![Sidebar Preferences in the user menu](/assets/top-right-visibility-preference.webp) 2. Click the tune icon at the top of the sidebar, next to the Descope logo: ![Sidebar Preferences icon in the sidebar](/assets/top-left-visibility-preference.webp) ## Choosing Which Pages to Show You can check a page to show it in the sidebar, or uncheck it to hide it. ![Sidebar Preferences dialog](/assets/visibility-preference-dialog1.webp) ![Sidebar Preferences dialog, scrolled to Localization, Authorization, and Connect](/assets/visibility-preference-dialog2.webp) The dialog mirrors the full sidebar hierarchy (not just top-level items): - Hiding a **parent** (for example **Localization**) also hides all of its sub-pages (**Global Strings**, **Flows and Widgets**, **Templates**, **Inbound Apps**). - You can hide **individual sub-pages** and leave the rest of the group visible. Example sidebar after hiding several pages (**MCP Servers**, **Clients**, **Connections**, **Widgets**, **Styles**, all of **Localization**, and **Federated Apps** / **Inbound Apps**): ![Sidebar after applying hidden-page preferences](/assets/visibility-preference-edit-result.webp) ## How Preferences Apply Preferences are tied to your **Descoper account**, not to a project or company. The same visibility settings apply across every project and company you can access. ### Hidden Pages Stay Accessible Hiding a page removes it from the sidebar only. It does not change access: - Direct and deep links still work (for example `https://app.descope.com/accessKeys` even if **Access Keys** is hidden). - Hidden pages still appear in [Console Search](/console-search). ### Reset to Default Click **Reset to default** at the bottom of the Sidebar Preferences dialog to show every page again and clear your customizations. # Common Errors (/common-errors) This article covers the common errors that that Descope can return. # Common Errors | CODE | Information | Additional Context | | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | E011001 | Request is malformed | The request contains invalid or malformed data. Common causes include: received tenant from query param that does not match any pattern (tenant not found, check if tenant exists and name is spelled correctly), or SSO not supported for domain (user tried to log in with SSO, but there is no tenant with the email domain associated with the user). | | E061102 | One time code is invalid | The provided one-time code is invalid. Note that you can change the number of attempts in the authentication method settings. | | E102116 | Invalid task ID: Execute next direct workflow failed. Failed to get execution info | During flow execution, the next flow step was not found. Ensure that the steps are connected throughout the flow and all the way to the end step. | | E103205 | Did not find execution context: Flow timed out, please try to refresh | The Descope flow component has been idling for too long. Refreshing the component will create a new flow execution. | | E062504 | Token expired: Failed to load magic link token | The magic link has expired. The user will need to restart the authentication process to receive a fresh magic link token. | | E063010 | Failed loading magic/enchanted link from cache, was not found, might be expired | The magic link has expired. The user will need to restart the authentication process to receive a fresh magic link token. | | E011003 | Request is invalid | The request contains invalid arguments or field values. Common causes include: required fields are missing (e.g., The [NAME] field is required), PKCE verifier must be exactly 32 bytes, PKCE challenge must be exactly 32 bytes, redirect URL must be at most 2048 characters, or OTP code must be exactly 6 characters. Make sure all required fields are provided and comply with format requirements. | | E061104 | One time code expired | The one-time code has expired. The user needs to restart the authentication or resend the OTP code to receive a valid new code. | | E062503 | Token not verified: Unauthorized enchanted link status - token was not verified yet | The enchanted link has expired. The user needs to restart the authentication process to receive a fresh enchanted link token. | | E062115 | Attempt to login with unverified email or phone: Unverified email / phone | The user tried to log in with an unverified e-mail or phone number. When used here, the number or email must be verified first. | | E062108 | User not found | User was not found in Descope. Descope uses LoginId to identify the user, make sure that the right loginId is used for the specific step or SDK. This error can occur during general user lookup, OAuth token exchange, magic link sign-in, or NOTP sign-in verification. Verify that the user exists or switch to signing the user up if applicable. | | E062904 | Password does not satisfy policy | User did not comply with the password policy when setting their password. Common violations include: missing non-alphanumeric characters, uppercase characters, lowercase characters, numbers, or minimum length requirements. It is recommended to use the policy previewer component so that the user will be able to see why the password was rejected. | | E064002 | Empty or Non Existent Refresh Token JWT was provided | Token is missing. When using a custom domain check whether DS/DSR (or your custom-configured cookie names) are absent from the localStorage or the cookie. | | E062901 | Invalid signin credentials | One of the provided credentials is wrong, used for password authentication. May include attempt count in the error message. | | E069000 | PKCE Validation Failed: Failed to verify link - PKCE Validation Failed: PKCE challenge and verifier do not match | PKCE already used, or the challenge and the verifier do not match. Validate that the PKCE is being used for the first time, or that it is being created and passed properly. | | E061301 | Failed to exchange token: Failed to exchange sso code | Wrong or missing SSO code for exchange. | | E062209 | Token exchange with OAuth provider failed, please validate your OAuth setup: [E062108] User not found: User does not exists [error: [E013009]] | For signing in with OAuth, the user must exist first in the Descope user table.Check that the user exists or have the user sign up. | | E061103 | Max attempts exceeded for one time code | User has reached the maximum attempts verifying the log in with the OTP. This may be an attempt to break into the account. | | E071001 | Project does not exist | The Descope Project ID is invalid/does not exist. Verify that it has been entered correctly. | | E102111 | Flow reached limit of allowed tasks | The amount of tasks executed exceeded 1000. Check your flow for infinite loops. | | E102103 | Did not find next task: Could not get next task for [NAME] | During flow execution, the next flow step was not found. Ensure that the steps are connected throughout the flow and all the way to the end step.. | | E061002 | Sign up is not allowed: Self provisioning signUp is not allowed | Sign ups are not allowed. This may be a result of checking the “Block self-registration sign up” in the project settings page. | | E033005 | Rate limit exceeded: Exceeded the allowed number of emails in a the defined time frame. Please wait a while and try again | User actions triggered several email messages which exceeded the limit. It is recommended to check the flow and the user actions to find a way to prevent this from happening again. | | E125004 | Connector execution runtime error | The connector encountered an error during execution. Common causes include: URL not reachable (getaddrinfo ENOTFOUND - check the URL and network, contact Descope if everything seems correct), or task timed out after x seconds (response took too long - either increase the timeout in the connector step or check the destination machine for issues). | | E064003 | Invalid Refresh Token JWT was provided: Failed to load user | User’s JWT provided did not find any existing users in the project. Please make sure you are using the right project. | | E013009 | Connector not found | Flow is using a connector that does not exist. Check whether the connector exists. | | E062903 | Password signin failed | Wrong password provided. | | E023001 | User is disabled | User is disabled. Enable the user through the UI or API. You can also alert your users when they are disabled and provide them an option to contact support. | | E067010 | User doesn't have any WebAuthn credentials | The user tried to sign-in with passkeys, and does not have a passkey set on the device. See the ‘promote-biometrics’ flow as an example of how to set up a passkey. | | E062910 | Password cannot be reused [error: rpc error: code = Unknown desc = [E062910] Password cannot be reused] | User tried to set a password that has been used before. You can control the number of passwords that Descope checks against, by going into the password authentication method settings. | | E106003 | Could not find tenant: Cannot determine tenant from JWT | Happens when performing an authorized action and the tenant is required for performing the action. | | E031002 | Missing providers Settings | The provider specified in the flow is missing (probably deleted). | | E011004 | Invalid Arguments | The arguments passed to the API/SDK call are invalid. | | E067015 | Login transaction not found | The passkey operation timed out | | E067020 | Android APK key hash origin is not in the allowed list | The Android app's signing fingerprint is not listed under Passkeys Settings > Android Fingerprints. Add a fingerprint for every signing key in use, including debug and Play App Signing keys. | | E073307 | Failed to save tenant, tenant ID or Name already exist: Failed creating tenant because provisioning domains are duplicate | The tenant already exists. Happens when self-provisioning is used inside a flow or the requested tenant has the same email domain. | | E106004 | Could not find tenant: Illegal tenant requested | The name of the tenant does not exist in Descope. | | E032101 | Failed to send sms: Status: 429 - Max send attempts reached | The maximum number of attempts to send an SMS to a specific number has been reached. | | E062605 | Token exchange with SSO provider failed | SSO token exchange failed. Common causes include: cannot generate redirect URI (redirect URI is not configured or is missing from metadata on the IDP, make sure all URLs are set correctly on the IDP), or user is disabled in Descope (enable the user through the user table). | | E062907 | Password reset send failed | This can be a result of wrong email provider settings. Check the relevant connector. | | E102112 | Invalid execution id | Flow execution ID not found. Restart the flow to create a new ID. | | E112201 | Tenant does not belong to the specified project | The tenant does not belong to the specified project. The tenant ID is case sensitive, so ensure that the tenant ID is cased correctly. | | E023009 | Cannot merge with test user: Cannot merge with test user | Test users are not permitted. | | E062906 | Password update failed | Updating the password failed. | | E062111 | JWT invalid for update user flow - JWT does not match user | The JWT provided does not match the user. | | E102004 | Flow requested is in old version, need to reload page: Got wrong version after reload | Flow has changed, refresh the page to get a newer flow version. | | E011002 | Request is missing required arguments | The request has a missing or invalid argument that does not comply with the field’s format. Make sure to provide that field or check the validity of the field. | | E103003 | Failed getting flow: Failed loading flow by ID | Flow ID was not found on the project. Make sure you have the right flow ID or that you are using the right project ID. | | E032001 | Failed to send email: Failed to send email through SES (MessageRejected): Email address is not verified | SES requires the sender email to be verified on AWS. Make sure you follow the steps to verify it. | | E032106 | Invalid Phone number provided to phone SMS: Failed to send SMS - Invalid Phone To +xx-xxxxxx | The phone number provided does not comply with the phone number format. | | E064011 | JWT inactive for too long: Failed getting tenants from JWT. | The operation requires a specific tenant to work. The JWT contains either no tenant or multiple tenants. | | E061003 | Redirect URL does not match the approved domain list | When using a custom redirect URL with an IDP, make sure to add the domain to the approved domain list in the project settings page. | | E061010 | Your company account is no longer active. Please contact support for assistance | This error occurs when attempting to access a company that has been deleted or disabled. The account is no longer accessible. Contact Descope support if you believe this is an error. | | E062208 | Failed to create user from mapping, external ID does not exist | When trying to merge identities from SSO / OAuth in a sign in process, there is no existing user with the associated ID (e-mail or phone number). | | E062107 | User already exists in SignUp. | User with the provider login ID already exists, use `sign in` or `sign up / in` instead. | | E103202 | Polling status not found. | Magic Link / Enchanted Link reference not found, it is probably expired by time or already clicked on. Try sending it again. | | E061206 | Missing redirect URL for IdP initiated login. | SSO IdP Initiated request while post authentication URL is not configured. Set the Post Authentication Redirect URL — see [SSO login flows → IdP-initiated](/sso/idp-initiated#what-you-must-configure). | | E023019 | Number of test users exceeded. | Number of available test users depends on your company license. | # OAuth OIDC Related Errors `Token exchange with OAuth provider failed, please validate your OAuth setup.`
These errors might indicate a misconfiguration on both sides, the SP and the IDP. Here is a list of all of the errors that might occur when Descope is the SP. To further debug IDP related issues, read the documentation that is associated with the error message and the specific IDP used. | Information | Additional Context | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------| | Failed to connect to user info endpoint | Either the user endpoint cannot be reached or does not return a valid JSON. | | This OAuth Provider is not enabled, need to allow in project settings first | The provider that was used in the flow is not enabled. | | The user has denied access to the scope requested by the client application | The user has declined the access request of the app. | | Disabled user in oauth exchange | The user is disabled in Descope. | | User already exists: User already exists | The user already exists and sign up is rejected. Use sign in instead. | | User not found: User does not exist | The user must exist first in the Descope user table. Check that the user exists or have the user sign up. | | Request is missing required arguments | The provided e-mail address does not comply with the e-mail format. | Are you facing an error that is not listed here? Please contact us, and we will make sure to list it. # Flow Activity (/flow-activity) Trace flow executions end-to-end, inspect per-task status and timing, and navigate to relevant audit and troubleshooting logs. # Flow Activity Flow Activity gives you a single place to trace every flow execution from start to finish. Instead of cross-referencing execution IDs between the Audit log and Troubleshooting page, you can see per-task status, timing, and error details in one view — and jump directly to the relevant logs when you need to dig deeper. You can access Flow Activity from the [Audits & Troubleshooting](https://app.descope.com/audits/flows) page in the Descope console. ![Flow Activity table showing flow executions](/assets/flow-activity-table.webp) ## Filters and Search Flow Activity data is available for the past 120 days. Custom date ranges are limited to 1-7 consecutive days at a time. By default, the view shows executions from the last 15 minutes with no filters applied. Use the free-text search to filter by any value — execution ID, user ID, flow name, error message, or Login ID — across all columns. ## Flow Execution Table Each row in the table represents one flow execution. | Column | Description | |--------|-------------| | Flow Name | The name of the flow that executed | | Start Time | When the flow execution began | | Execution ID | Unique identifier that groups all tasks belonging to the same execution | | Status | `Normal`, `Warning`, or `Error` — applies at the task level | | Task Name | The name of the individual task within the flow | | Task Type | `Screen`, `Action`, `Condition`, `Connector`, or `Sub Flow` | | Action | The action that was performed at the task | | Message | Informational message from the task | | Error Message | Error detail, if the task failed | | User ID | ID of the user associated with this execution | | Tenant ID | Tenant associated with this execution, if applicable | | Origin IP | IP address from which the flow execution was triggered | | Login ID | The Login ID of the user associated with this execution | | Federated App ID | The Federated App ID of the user associated with this execution | | Connector ID | The Connector ID of the connector used in the flow execution | | Flow Version | The version of the flow that executed | ## Expanding Flow Execution Each flow row has a chevron toggle on the left. Clicking it expands the row to show all tasks that belong to that execution, each displayed as its own sub-row with the same columns as above. When a filter or search matches a task rather than the flow-level row, the parent flow group is still displayed so you can see the full execution context. ![Expanded flow row showing individual task details](/assets/flow-activity-expanded-tasks.webp) Task-level rows have the same Audit and Troubleshooting actions, which pass the execution ID and date range so the destination page opens pre-filtered to that specific task's context. Clicking on any row opens a side panel with additional execution details, including the `cf_ray` value, which is not currently displayed in the table columns. ![Flow Activity side panel showing execution details](/assets/flow-activity-side-panel.webp) ## AI Flow Analysis AI Flow Analysis lets you view Flow executions to see the exact path users took through each Flow. ![AI Flow Analysis showing flow execution path](/assets/ai-flow-analysis.webp) This feature requires `Enable AI-powered features and insights` in [Company Settings](/management/company-settings#permissions). Turn it off there if your organization's governance or legal requirements don't allow AI processing of flow data. # Error Handling in SDKs (/sdk-error-handling) Learn how the Descope SDKs type and surface errors, and how to check for specific error codes. # Error Handling in SDKs Every SDK call that reaches the Descope servers can fail. When it does, the SDK reports a Descope **error code** (for example `E061102`), a human-readable **description**, and sometimes a more specific **message**. You can look up any code on the [Common Errors](/common-errors) page. For a list of common error codes and what they mean, see the [Common Errors](/common-errors) reference. ## Checking for Errors by SDK Each tab shows how one SDK types its errors and which codes it exposes, plus how to: - Check for a specific error code - Check for a category, such as unauthorized or rate limited - Read the code and description off an error The Node.js SDK does not throw when the server returns an error. Every call returns an `SdkResponse` — check `ok` before using `data`, and read the details off `error`. The one exception is a response whose body is not valid JSON, which rejects instead of returning. ```ts title="SdkResponse shape" { ok: boolean; // false when the request failed code?: number; // HTTP status code (e.g. 401, 429) error?: { errorCode: string; // Descope error code, e.g. "E061102" errorDescription: string; // human-readable summary errorMessage?: string; // extra context (not always present) }; data?: T; // present when ok === true } ``` Import the client and the typed error-code map (`DescopeErrors`): ```javascript title="error-handling.js" import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: '__ProjectID__' }); const { DescopeErrors } = DescopeClient; const resp = await descopeClient.otp.verify.email(loginId, code); if (!resp.ok) { // Read the code + description off the response console.log(resp.error?.errorCode); // "E061103" console.log(resp.error?.errorDescription); console.log(resp.code); // HTTP status, e.g. 401 // Check for a specific typed error code if (resp.error?.errorCode === DescopeErrors.tooManyOTPAttempts) { // too many wrong OTP attempts (E061103) } // Check for a category using the HTTP status code if (resp.code === 429) { // rate limited } } else { // Success — use resp.data const { sessionJwt } = resp.data; } ``` Codes exposed on `DescopeErrors`: | Constant | Code | | --- | --- | | `DescopeErrors.badRequest` | `E011001` | | `DescopeErrors.missingArguments` | `E011002` | | `DescopeErrors.invalidRequest` | `E011003` | | `DescopeErrors.invalidArguments` | `E011004` | | `DescopeErrors.wrongOTPCode` | `E061102` | | `DescopeErrors.tooManyOTPAttempts` | `E061103` | | `DescopeErrors.enchantedLinkPending` | `E062503` | | `DescopeErrors.userNotFound` | `E062108` | ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. Node.js is the one SDK that does not produce an `SdkResponse` in that case: parsing the body is what populates `error`, so a non-JSON body makes the call **reject** instead of returning. Wrap calls in `try`/`catch` in addition to checking `ok` if you need to survive a malformed gateway response. The Python SDK raises one of two exceptions, both defined in `descope.exceptions` and both inherit directly from `Exception`. ```python title="Exception shapes" class AuthException(Exception): status_code: int | None # HTTP status code (e.g. 400, 401, 500) error_type: str | None # always "server error" for failed requests error_message: str | None # the raw response body for failed requests class RateLimitException(Exception): status_code: str | None # Descope code string, e.g. "E130429" — not the HTTP int used on AuthException error_type: str | None # always "API rate limit exceeded" error_description: str | None # from the errorDescription JSON field error_message: str | None # from the errorMessage JSON field rate_limit_parameters: dict # {"Retry-After": } ``` Because `RateLimitException` does not subclass `AuthException`, an `except AuthException` block silently misses rate-limit errors. Always handle `RateLimitException` in its own clause, and list it first. Also note that `RateLimitException.status_code` is a Descope code string (for example `"E130429"`), while `AuthException.status_code` is an HTTP status `int`. For every failure other than `429`, the SDK does not parse the JSON error body. `error_type` is always the literal string `"server error"` and `error_message` holds the **raw response body**. To read a Descope error code, parse that string as JSON yourself. ```python title="error_handling.py" import json from descope import ( API_RATE_LIMIT_RETRY_AFTER_HEADER, AuthException, DeliveryMethod, DescopeClient, RateLimitException, ) descope_client = DescopeClient(project_id="__ProjectID__") try: jwt_response = descope_client.otp.verify_code( DeliveryMethod.EMAIL, login_id, code ) session_token = jwt_response["sessionToken"].get("jwt") # RateLimitException is a separate type and is not caught by AuthException except RateLimitException as e: retry_after = e.rate_limit_parameters.get(API_RATE_LIMIT_RETRY_AFTER_HEADER, 0) # e.status_code holds the Descope code, e.g. "E130429" — not 429 except AuthException as e: # Read the HTTP status code off the exception if e.status_code == 401: # unauthorized pass # Check for a specific error code by parsing the raw body try: body = json.loads(e.error_message) except (TypeError, ValueError): body = {} if body.get("errorCode") == "E061103": # too many wrong OTP attempts pass ``` Local argument validation raises `AuthException` before any request is sent, with `status_code` set to `400` and `error_type` set to `"invalid argument"` — for example when `login_id` is empty. Constants exposed on the `descope.exceptions` module: | Constant | Value | | --- | --- | | `ERROR_TYPE_INVALID_ARGUMENT` | `invalid argument` | | `ERROR_TYPE_SERVER_ERROR` | `server error` | | `ERROR_TYPE_INVALID_PUBLIC_KEY` | `invalid public key` | | `ERROR_TYPE_INVALID_TOKEN` | `invalid token` | | `ERROR_TYPE_API_RATE_LIMIT` | `API rate limit exceeded` | | `API_RATE_LIMIT_RETRY_AFTER_HEADER` | `Retry-After` | Only `ERROR_TYPE_SERVER_ERROR`, `ERROR_TYPE_API_RATE_LIMIT`, and `API_RATE_LIMIT_RETRY_AFTER_HEADER` are re-exported from the top-level `descope` package. There is no typed map of Descope error codes equivalent to the Node.js `DescopeErrors`. ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. In that case Python gives you **no Descope error code**; the raw response body is left as the error message. Guard for this when you branch on an error code, since the value will not be a Descope `E`-prefixed code. The Go SDK returns a standard `error` whose concrete type is `*descope.Error`. Use the package helpers to inspect it. ```go title="descope.Error shape" type Error struct { Code string // e.g. "E061102" Description string // human-readable summary Message string // extra context (not always present) Info map[string]any // extra metadata (HTTP status, Retry-After) } ``` ```go title="error_handling.go" result, err := descopeClient.Auth.OTP().VerifyCode(ctx, descope.MethodEmail, loginID, code, w) // 1. Check for a specific error code if descope.IsError(err, "E061102") { // wrong OTP code } // 2. Or match a predefined error with the standard errors.Is (requires: import "errors") if errors.Is(err, descope.ErrInvalidOneTimeCode) { // wrong OTP code (E061102) } // 3. Check for a category (400 / 401 / 403 / 404) if descope.IsUnauthorizedError(err) { // unauthorized } // 4. Read the code and description directly if de := descope.AsError(err); de != nil { log.Printf("failed: [%s] %s", de.Code, de.Description) } ``` Rate-limit errors carry a `Retry-After` value in the `Info` map: ```go if de := descope.AsError(err, descope.ErrRateLimitExceeded.Code); de != nil { retryAfter := de.Info[descope.ErrorInfoKeys.RateLimitExceededRetryAfter] // seconds } ``` Predefined error variables (each is a `*descope.Error` with a fixed `Code`): | Variable | Code | | --- | --- | | `descope.ErrBadRequest` | `E011001` | | `descope.ErrMissingArguments` | `E011002` | | `descope.ErrValidationFailure` | `E011003` | | `descope.ErrInvalidArguments` | `E011004` | | `descope.ErrInvalidOneTimeCode` | `E061102` | | `descope.ErrUserAlreadyExists` | `E062107` | | `descope.ErrEnchantedLinkUnauthorized` | `E062503` | | `descope.ErrPasswordExpired` | `E062909` | | `descope.ErrTokenExpiredByLoggedOut` | `E064001` | | `descope.ErrNOTPUnauthorized` | `E066103` | | `descope.ErrManagementUserNotFound` | `E112102` | | `descope.ErrRateLimitExceeded` | `E130429` | Client-side errors (SDK/config, not from the server) use `G`-prefixed codes, such as `descope.ErrMissingProjectID` (`G010001`), `descope.ErrInvalidToken` (`G030002`), and `descope.ErrRefreshToken` (`G030003`). ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. Go returns `descope.ErrInvalidResponse` (`G020002`), with the HTTP status available in the `Info` map. Guard for this when you branch on an error code, since the value will not be a Descope `E`-prefixed code. The Java SDK throws exceptions that all extend the abstract `DescopeException`, in the `com.descope.exception` package. Every one of them is **unchecked** — they extend `RuntimeException`, so the `throws DescopeException` on the service interfaces does not force you to catch anything. | Exception | Raised for | | --- | --- | | `ServerCommonException` | API errors and argument validation failures | | `RateLimitExceededException` | HTTP `429`, or a body carrying `E130429` | | `ClientSetupException` | Client configuration, such as a missing or malformed project ID | | `ClientFunctionalException` | Local JWT/token validation failures | `getCode()` returns the Descope error code from the response body, and `getMessage()` resolves the first of `errorMessage`, `message`, or `errorDescription` that is present. `ServerCommonException.getServerResponse()` additionally gives you the raw response body. ```java title="ErrorHandling.java" import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.enums.DeliveryMethod; import com.descope.exception.ClientFunctionalException; import com.descope.exception.DescopeException; import com.descope.exception.ErrorCode; import com.descope.exception.RateLimitExceededException; import com.descope.exception.ServerCommonException; import com.descope.model.auth.AuthenticationInfo; import com.descope.sdk.auth.OTPService; DescopeClient descopeClient = new DescopeClient( Config.builder().projectId("__ProjectID__").build()); OTPService otps = descopeClient.getAuthenticationServices().getOTPService(); try { AuthenticationInfo info = otps.verifyCode(DeliveryMethod.EMAIL, loginId, code); } // 1. Rate limits carry the number of seconds to wait catch (RateLimitExceededException e) { long waitSeconds = e.getRetryAfterSeconds(); } // 2. Local token validation failed, e.g. code "G030002" catch (ClientFunctionalException e) { System.err.printf("%s - %s%n", e.getCode(), e.getMessage()); } // 3. API and argument errors catch (ServerCommonException e) { // Check for a specific error code if (ErrorCode.INVALID_ARGUMENT.equals(e.getCode())) { // invalid argument (E011004) } if ("E061103".equals(e.getCode())) { // too many wrong OTP attempts } String rawBody = e.getServerResponse(); } // 4. Or catch the base type and read code + message off any of them catch (DescopeException e) { System.err.printf("%s - %s%n", e.getCode(), e.getMessage()); } ``` ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. Java falls back to the bare HTTP status as a string, so `getCode()` returns values like `"400"` or `"401"`. Guard for this when you branch on an error code, since the value will not be a Descope `E`-prefixed code. Codes exposed on the `ErrorCode` class: | Constant | Code | | --- | --- | | `ErrorCode.INTERNAL_SERVER_ERROR` | `500` | | `ErrorCode.INVALID_ARGUMENT` | `E011004` | | `ErrorCode.ERR_MISSING_ARGUMENTS` | `E011002` | | `ErrorCode.RATE_LIMIT_EXCEEDED` | `E130429` | | `ErrorCode.MISSING_PROJECT_ID` | `G010001` | | `ErrorCode.INVALID_PROJECT_ID` | `G010002` | | `ErrorCode.INVALID_TOKEN` | `G030002` | | `ErrorCode.ERR_REFRESH_TOKEN` | `G030003` | | `ErrorCode.INVALID_SIGNING_KEY` | `J010001` | Any other code, such as `E061103`, arrives as a plain string from the server and has no matching constant — compare it as a string literal. The Ruby SDK raises two families of errors, both defined in `lib/descope/exception.rb`. Which family you get tells you where the failure happened, and they do **not** share a common parent below `Descope::Exception`. ```ruby title="Error families" Descope::Exception # base class, exposes #error_data ├── Descope::AuthException # raised by local validation, before any request ├── Descope::ArgumentException # raised by local validation, before any request └── Descope::HTTPError # raised from an HTTP response; adds #http_code and #headers ├── Descope::BadRequest # 400 ├── Descope::Unauthorized # 401 ├── Descope::AccessDenied # 403 ├── Descope::NotFound # 404 ├── Descope::MethodNotAllowed # 405 ├── Descope::RateLimitException # 429 ├── Descope::ServerError # 500 ├── Descope::Unsupported # any other status └── Descope::RequestTimeout # the request timed out ``` Any status not listed above — `502` and `503`, for example — is raised as `Descope::Unsupported`. A failed verification raises a `Descope::HTTPError` subclass, not `Descope::AuthException`. Rescuing only `Descope::AuthException` catches local validation errors while letting real API failures escape. Rescue `Descope::Exception` to cover both. On failure the SDK puts the **raw response body** into the error message without parsing it, and defines no error-code constants. To read a Descope error code, parse `e.message` as JSON yourself. ```ruby title="error_handling.rb" require 'descope' require 'json' descope_client = Descope::Client.new(project_id: '__ProjectID__') begin jwt_response = descope_client.otp_verify_code( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'user@example.com', code: '123456' ) # 1. Check for a category by rescuing the matching class rescue Descope::RateLimitException => e # rate limited (429); the SDK already retried this a few times puts "Rate limited, headers: #{e.headers}" rescue Descope::HTTPError => e # 2. Read the HTTP status off the error puts "API error (#{e.http_code})" # 3. Check for a specific error code by parsing the raw body body = begin JSON.parse(e.message) rescue JSON::ParserError {} end puts body['errorCode'] # e.g. "E061103" puts body['errorDescription'] rescue Descope::Exception => e # 4. Local validation failed before the request was sent puts "Validation error: #{e.message} (#{e.error_data[:code]})" end ``` Local validation errors set a `code` inside `error_data` rather than exposing `#http_code`, so read them with `e.error_data[:code]`. ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. In that case Ruby gives you **no Descope error code**; the raw response body is left as the error message. Guard for this when you branch on an error code, since the value will not be a Descope `E`-prefixed code. The PHP SDK throws four exception classes in the `Descope\SDK\Exception` namespace. All of them implement the `DescopeException` interface, so catching that single type covers everything the SDK throws. | Exception | Raised for | | --- | --- | | `AuthException` | Most request failures, plus local argument validation | | `RateLimitException` | HTTP `429` responses | | `TokenException` | Local JWT parsing and signature verification failures | | `ValidationException` | A required argument was missing before the request | `AuthException` and `RateLimitException` keep `statusCode` and `errorType` as private properties, and the status code and error type are only reachable by casting the exception to a string, which yields JSON. The server's error code is stored in the field named `errorType`, and the message resolves the first of `errorDescription`, `errorMessage`, or `message` that is present: ```json title="AuthException cast to string" { "statusCode": 400, "errorType": "E061102", "errorMessage": "One time code is invalid" } ``` ```php title="error-handling.php" use Descope\SDK\DescopeSDK; use Descope\SDK\Exception\AuthException; use Descope\SDK\Exception\RateLimitException; use Descope\SDK\Exception\TokenException; use Descope\SDK\Exception\ValidationException; $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], ]); try { $response = $descopeSDK->otp->verifyCode('email', 'user@example.com', '123456'); } // 1. Rate limited (429) catch (RateLimitException $e) { // No Retry-After value is parsed; back off on your own schedule } catch (AuthException $e) { // 2. Read the description off the exception echo $e->getMessage(); // 3. Check for a specific error code, which lives in "errorType" $details = json_decode((string) $e, true); if (($details['errorType'] ?? null) === 'E061103') { // too many wrong OTP attempts } // 4. Check for a category using the HTTP status code if (($details['statusCode'] ?? null) === 401) { // unauthorized } // The underlying Guzzle exception is available for logging $guzzleException = $e->getPrevious(); } catch (ValidationException $e) { // A required argument was empty, e.g. a missing session token } catch (TokenException $e) { // Local JWT validation failed, e.g. "Invalid signature" } ``` `RateLimitException` also carries a `rateLimitParameters` field, but the SDK does not read the `Retry-After` header, so it is always empty. ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. In that case PHP gives you **no Descope error code**; the raw response body is left as the error message. Guard for this when you branch on `errorType`, since the value will not be a Descope `E`-prefixed code. The .NET SDK throws `DescopeException` when a request fails. It exposes `ErrorCode`, `ErrorDescription`, and `ErrorMessage`, and its `Message` is formatted as `[code]: description (message)`. ```csharp title="DescopeException shape" public class DescopeException : ApplicationException { public string? ErrorCode { get; } // "E062504" public string? ErrorDescription { get; } // "Token expired" public string? ErrorMessage { get; } // "Failed to load magic link token" // Message => "[E062504]: Token expired (Failed to load magic link token)" } ``` ```csharp title="ErrorHandling.cs" using Descope; try { var response = await client.Auth.V1.Auth.Magiclink.Verify.PostAsync( new VerifyMagicLinkRequest { Token = token }); } catch (DescopeException e) { // Read the details off the exception Console.WriteLine(e.ErrorCode); // "E062504" Console.WriteLine(e.ErrorDescription); // "Token expired" Console.WriteLine(e.Message); // "[E062504]: Token expired (Failed to load magic link token)" // Check for a specific error code if (e.ErrorCode == "E061103") { // too many wrong OTP attempts } // Unparseable body: SDK substitutes HTTP, e.g. "HTTP401" if (e.ErrorCode == "HTTP401") { // unauthorized (server returned 401 with no parseable error body) } } ``` ### When the Error Body Cannot Be Parsed A failing response does not always contain a Descope error body — a gateway may return an HTML error page, or an empty `401`. .NET substitutes a synthetic code of `HTTP`, such as `HTTP401` for an unauthorized response. Guard for this when you branch on an error code, since the value will not be a Descope `E`-prefixed code. ## Automatic Retries Before surfacing an error, the SDKs transparently retry requests that failed with a transient server status. The NodeJS, Python, Go, Java, PHP, and .NET SDKs all retry HTTP `503`, `520`, `521`, `522`, `524`, and `530` — the Cloudflare and service-unavailable statuses — up to three times after the initial attempt, waiting 100ms before the first retry and 5 seconds before each of the next two. The Ruby SDK is the exception: it does not retry those statuses at all. Instead it retries **only** HTTP `429`, three times by default, using exponential backoff with jitter. # Overview (/) Explore our developer docs to integrate Descope authentication and user management into your app. Get started with no-code workflows, SDKs, or APIs. # Descope Documentation Welcome to Descope, a passwordless authentication and user management service designed for developers. Descope makes it easy to enable a variety of different user authentication methods in your application. With our SDKs and no-code workflow builder, you can easily create and customize secure authentication flows for every interaction a user has with your B2B or B2C application. ## How Descope Helps } title="Descope Flows" description="Design login screens and authentication flows visually without writing code, and embed them into your app." href="/flows" /> } title="Risk-Based MFA" description="Create adaptive MFA flows based on conditional logic to enforce MFA only when the login attempt is deemed to be risky." href="/mfa-and-step-up/mfa" /> } title="Self Service SSO and SCIM" description="Enable your customer tenant admins to configure their own SSO and SCIM connections in a self-service portal." href="/auth-methods/sso/sso-setup-suite" /> } title="Delegated Admin Widgets" description="Hand off identity management to your customer admins and end users with embeddable widgets (user profiles, user and role mgmt., access key mgmt. and more)." href="/widgets" /> } title="Connectors" description="Bring in data and actions from 50+ third-party connectors into your user journey workflows (e.g. CRM, identity verification, fraud prevention, audit)." href="/connectors" /> } title="Agentic Identity Hub" description="Add auth, user consent, scope-based access control, credential management, and policies to your AI agents and MCP servers." href="/agentic-identity-hub" topLabel="New" isActive={true} /> ## Add Authentication to Your App With Our SDKs The recommended approach for integrating Descope is to use [Descope Flows](/flows), which provide the easiest and most straightforward way to implement authentication. However, if you prefer to build your own custom authentication experience, you can use our SDKs instead. } title="Client SDKs" description="Design your own login screens and authentication flows in your frontend with code, while we handle session management for you." href="/client-sdk" /> } title="Mobile SDKs" description="Add authentication to your mobile application with secure session management and token handling." href="/mobile-sdk" /> } title="Backend SDKs" description="Build your own custom APIs for authentication. You handle all frontend code and session management, while we exist in the backend behind your APIs." href="/backend-sdk" /> ## Quickstart Tutorials Watch these quick video tutorials to learn the fundamentals of integrating Descope into your application. Each video covers a key aspect of authentication and user management, from implementing Flows to working with SDKs and managing tenants.
## Example Apps # Integration Approaches (/integration-approaches) Use Descope Client, Mobile, and Backend SDKs to add authentication to your app. Supports Go, Python, React, Node.js, JavaScript, and more. # Integration Approaches With Descope, there are many ways you can integrate our services into your application. The most common and **recommended** way is with [Descope Flows](/flows), however this page will help outline the Pros vs Cons of every single integration approach. ## Integration Approaches There are three common ways to integrate the Descope service into your application. Below is a comparison of the different integration approaches and when to use them.
App Client to Descope with Flows App Client to Descope (Client SDK, No Flows) App Server to Descope (Backend SDK)
Details Create authentication flows and screens using Descope's drag-and-drop visual editor. We strongly recommend using this approach to integrate Descope into your application. Integrate your application client to individual authentication methods supported by Descope. Integrate your application server with the Descope service.
Signup/Login Screens Descope visual screen editor Build on your own Build on your own
Authentication flow Descope visual workflow Build on your own Build on your own
Session Management Handled by Descope Client SDK Handled by Descope Client SDK Build on your own
Device Fingerprinting & User Risk Descope risk-based workflows Not available Not available
When to use? Choose this approach to go live with Descope in the fastest and easiest manner. Design authentication flows and screens directly in the Descope console. Deploy your app with a few lines of code. Choose this approach if you want to build authentication flows and screens on your own, but want to use the Descope SDK for session management. Descope Flows will not work with this approach. Choose this approach if you want to handle all frontend flows and logic - including session management - on your own. Descope Flows will not work with this approach.
# SDK Overview Descope offers both client and backend SDKs for many languages and frameworks. There are three ways to integrate the Descope service with your application. Based on your chosen approach, you will use only backend SDK or both client and backend SDK. The [quick start guide](/getting-started) covers the most common and recommended integration approach. The SDK guides cover the other approaches. Below is a quick summary of all the supported SDKs. ## Descope Client SDKs If you decide to integrate using either App Client to Descope with Flows or App Client to Descope (Client SDK, No Flows), then Descope Client SDK is used. If you decide to use App Client to Descope with Flows App, the Descope Client SDK is used to trigger different authentication flows. This is documented in the [Quick Start Guide](/getting-started). If you decide to use App Client to Descope (Client SDK, No Flows) for integrating with Descope, Descope Client SDK can be used to integrate with any of the authentication methods supported. | SDK | Github Link | | -------------- | ---------------------------------------------------------------------------- | | Web-JS SDK | [Repo](https://github.com/descope/descope-js/tree/main/packages/sdks/web-js-sdk) | | React SDK | [Repo](https://github.com/descope/descope-js/tree/main/packages/sdks/react-sdk) | | Vue SDK | [Repo](https://github.com/descope/descope-js/tree/main/packages/sdks/vue-sdk) | | Web Component SDK | [Repo](https://github.com/descope/descope-js/tree/main/packages/sdks/web-component)| | Swift SDK | [Repo](https://github.com/descope/descope-swift) | | Flutter SDK | [Repo](https://github.com/descope/flutter-sdk) | | React Native SDK | [Repo](https://github.com/descope/descope-react-native) | | Kotlin SDK | [Repo](https://github.com/descope/descope-kotlin) | ## Descope Backend SDKs Descope Backend SDKs are most commonly used for JWT validation after the user authenticates with Descope. If you are using Descope Client SDK for integration App Client to Descope with Flows or App Client to Descope (Client SDK, No Flows), then the only purpose of using the Backend SDK is for JWT validation as shown in the [Quick Start Guide](/getting-started). In case you chose App Server to Descope (Backend SDK) then the backend SDK will be your main point of integration for all authentication calls for different authentication methods. The SDK documentation for each SDK will demonstrate how to implement authentication methods like magic link, OTP etc using approach 3. | SDK | Github Link | | ---------- | ---------------------------------------------------------------------------- | | Python SDK | [https://github.com/descope/python-sdk](http://github.com/descope/python-sdk) | | Go SDK | [https://github.com/descope/go-sdk](https://github.com/descope/go-sdk) | | Node SDK | [https://github.com/descope/node-sdk](http://github.com/descope/node-sdk) | | PHP SDK | [https://github.com/descope/descope-php](https://github.com/descope/descope-php) | | Java SDK | [https://github.com/descope/descope-java](https://github.com/descope/descope-java) | # Learn the Lingo (/lingo) Explore a glossary of acronyms, concepts, and market terms from the world of identity and authentication. # Learn the Lingo The world of identity and authentication comes with a lot of acronyms and market terms that can be tricky to keep up with. This page contains a glossary of authentication “lingo” meant for newbies and veterans alike. # Tutorials (/tutorials) Tutorial on how to use Descope to implement authentication methods, handle multi-tenancy, authorization, and customize flows. # Tutorials Below are short video tutorials covering a variety of topics from introduction to deeper topics such tenant, roles and permissions management. If you would like to look at our written tutorial guides, they are located on the left hand sidebar, underneath the tutorials tab. ## Descope Fundamentals Quick overview of Descope user management and authentication platform. ## Build your app using Descope Flows Build your first application with a few lines of code and Descope flows. The tutorial covers all the steps needed to set up Descope project and integrate a flow into a react application. ## Customize styling Descope allows extensive customization capabilities for styles for the screens designed in Descope flows. Learn how to customize styles to match your application branding: ## Customize Flows Descope flows provide a drag-and-drop approach to building user journeys. The video tutorial covers basic flow concepts, like screens, actions, conditions, and connectors. ## Descope console walk-through Quick overview of all the menu items and basic settings exposed in the Descope console. ## Build your app using Descope Client SDK A short video with an overview of the Descope Client SDK and how to use it to build a simple application with OTP over email as the sign-in mechanism. ## Build your app using Descope Backend SDK A short video with an overview of the Descope Backend SDK and how to use it to build a simple application with OTP over email as the sign-in mechanism. ## Session Management A short video with an overview of the Descope Session Management. The video covers the details on how session and refresh token exchanges happen in different integration approaches. ## Tenants, Roles and Permissions Management A short video with an overview of the common concepts like tenants, roles, and permissions. The video covers the details on how you can manage tenants, roles and permissions using the Descope console or management SDK. # Password Hashing (/password-hashing) How Descope securely hashes passwords using Argon2id # Password Hashing At Descope, we ensure that user passwords are stored securely using modern, memory-hard hashing algorithms. We follow industry best practices to protect against brute-force and side-channel attacks. ## What Algorithm We Use Descope uses the **Argon2id** algorithm, as specified in [RFC 9106](https://datatracker.ietf.org/doc/html/rfc9106). Argon2id combines the benefits of Argon2d and Argon2i to provide robust defense against both parallel and side-channel attacks. ### Why Argon2id? - **Memory-hard:** Increases cost for attackers using GPUs or ASICs. - **Side-channel resistant:** Reduces exposure to timing and cache-based attacks. - **Modern & recommended:** Winner of the Password Hashing Competition (PHC) and endorsed by modern cryptographic standards. ## Configuration Parameters Descope uses the “uniformly safe” parameter set recommended in [RFC 9106, Section 4.2](https://datatracker.ietf.org/doc/html/rfc9106#name-parameter-choice): | Parameter | Value | |------------------|---------------------| | Algorithm | Argon2id | | Iterations (t) | 3 | | Parallelism (p) | 4 lanes | | Memory (m) | 64 MiB (2^16 KiB) | | Salt length | 128 bits | | Output length | 256 bits | These parameters offer a strong baseline for secure password hashing across modern hardware. ## Implementation Notes - Salts are generated using a cryptographically secure random number generator. - Password hashes are versioned internally to support future upgrades or rehashing. - Plaintext passwords are never stored or logged at any stage. ## Standards Alignment Our use of Argon2id aligns with guidance from: - [RFC 9106 – Argon2](https://datatracker.ietf.org/doc/html/rfc9106) - [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) - [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) For more details on our security practices, refer to our [Security & Compliance page](https://www.descope.com/security-compliance). For any questions about password security or compliance, please contact [security@descope.com](mailto:security@descope.com). # Rate Limits (/rate-limiting) This guide details the rate limits present within Descope's API and SDK. # Descope Rate Limiting This guide outlines how Descope enforces rate limits across its APIs and SDKs, ensuring efficient usage of resources. Below, you'll find detailed information about the various rate limits Descope has implemented, along with best practices to avoid exceeding these limits and ensure optimal performance. When your SDK or API calls are rate limited by Descope, you will receive a 429 HTTP status code. Go over the [Rate Limit Best Practices](/rate-limiting#rate-limit-best-practices) for proper methods on how to handle being rate limited. ## Rate Limit Calculations Descope calculates the rate limit based on IP address for most operations; however, rate limits for user management calls are limited based on project ID. ## Flow Rate Limits Descope limits the number of tasks that can be executed in a flow to 200 tasks per flow. If you reach the 100 task soft limit, you will see an error in the troubleshooting logs saying that the soft limit has been reached. ## User Management Limits Descope rate limits specific user management paths with lower limits than the generalized limits. ### Generic User Management Unless otherwise specified below, user management paths under `/v1/mgmt/user/*` are limited to 1000 requests per 60 seconds, with a 60-second backoff, whether initiated from the SDK or API. ### Specific User Management Paths The paths below are limited to 100 requests per 60 seconds, with a 60-second backoff, whether initiated from the SDK or API. - `/v1/mgmt/user/create` - `/v1/mgmt/user/create/batch` These limits govern how often you call the endpoint. A separate cap governs batch size on `/v1/mgmt/user/create/batch`: a cleartext `password` on any user in the request limits that batch to 100 users. Batches using only `hashedPassword` carry no such cap. See [Batch Create Users](/api/management/users/batch-create-users) for details. The paths below are limited to 200 requests per 60 seconds, with a 60-second backoff, whether initiated from the SDK or API. - `/v1/mgmt/user/update` - `/v2/mgmt/user/search` ## Audit Management Limits Descope rate limits audit management operations with the following limits: ### Audit Search The audit search path is limited to 10 requests per 60 seconds. - `/v1/mgmt/audit/search` ### Audit Create The audit create path is limited to 100 requests per 60 seconds. - `/v1/mgmt/audit/event` ## Tenant Management Limits Descope rate limits tenant management operations to 200 requests per 60 seconds. - `/mgmt/tenant/*` ## Authentication API Limits Descope rate limits various authentication-related endpoints with the following limits: ### M2M Key Exchange The M2M key exchange endpoint is limited to 500 requests per 30 seconds. - `/v1/auth/accesskey/exchange` ### Password Related APIs Password-related endpoints are limited to 20 requests per 60 seconds. - `/v1/auth/password/*` ### User Logout The user logout endpoint is limited to 100 requests per 60 seconds. - `/v1/auth/logout` ## Backend API and SDK Limits ### Descope SDKs #### Backend The Descope backend SDK rate is limited to 1000 requests per 10 seconds, with a 10-second backoff for non-authentication-related tasks. Note that management-related tasks have stricter rate limits, which can be found within the above rate limit outline for [user management](/rate-limiting#user-management-limits). #### Frontend The Descope frontend SDKs are rate-limited at a lower value; the rate limit is 100 requests per 60 seconds with a 60-second backoff. ### Descope API Descope's API rate limits all traffic to 100 requests per 60 seconds with a 60-second backoff, excluding paths with a higher rate limit for [user management](/rate-limiting#user-management-limits). #### Excluded Endpoints Outside of the abovementioned rate limits, the following paths have an enhanced limit, whether from API or SDK. The rate limit of the paths below is 1000 requests per 10 seconds with a 10-second backoff. - `/scim/*` - `/.well-known/*` #### Outbound Apps When using outbound apps, you might have a lot of requests for all of your end users/AI agents that are [requesting](/api/management/outbound-apps/fetch-outbound-app-user-token) their OAuth tokens with a management key. In this case, the rate limit is 600 tokens per minute. ## Rate Limit Best Practices ### Monitor and Backoff With all API and SDK usage, one must protect themselves from rate limiting. It is advised to build rate limit error handling within your application to abide by the rate limits set in place by Descope. Monitor for the `429` responses code and implement a backoff as defined above-outlined backoffs. ### Review Utilization If you frequently hit a rate limit scenario, it is recommended that you review how you utilize the endpoints that are rate-limiting you. To review your Descope API and SDK usage, review the API URI that you are hitting the rate limit or which SDK calls you are hitting the rate limit. Once you have identified these paths, review where these calls are frequently utilized in your application. Once you have found where these calls are being made within your application, review them for consolidation. For example, if you're doing multiple user updates in sequence, switch to a patch or update call to consolidate these calls. Some common cases misuse the Descope API and SDK. For example, you frequently load a user for a specific user detail, such as their email address or custom attribute. In that case, you can save execution time and additional API/SDK calls to load the user by adding the items to the custom claim. See [this documentation](/flows/actions/custom-claims) for details on adding items to the custom claims. #### Session Validation If you are experiencing rate limits within Descope on the session validation endpoints, you may be manually validating the sessions within your application. Manually validating the user's session is not required as the SDKs cache the Descope public keys to validate the session to prevent hitting these rate limits. Rather than explicitly calling the Descope API to validate the session, you can depend on the Descope SDK to validate this for you. ### Inquire with Descope Support If you are hitting rate limits within your application and are unable to determine how to best accommodate your use case without hitting the rate limit, feel free to reach out to the [Descope team](/support) for assistance. # FGA Cache (/authorization/fga-cache) Learn how to use Descope AuthZ Cache to accelerate Fine-Grained Authorization (FGA) checks for ReBAC, ABAC, and all authz service operations. # FGA Cache The **Descope AuthZ Cache** is a high-performance authorization cache service that accelerates Fine-Grained Authorization (FGA) checks by caching authorization data locally within your cluster. By deploying this service alongside your application, you can significantly reduce latency for authorization checks and improve overall application performance. ## Overview The AuthZ Cache service applies to select authorization operations that use Descope's **FGA service** (`/v1/mgmt/fga/*` endpoints). The cache service acts as a local cache layer between your application and Descope's authorization services. It automatically syncs with remote authorization data and provides fast, local access to relationship and permission information needed for FGA checks. ## How It Works The AuthZ Cache service: 1. **Connects to Descope**: Uses your management key to authenticate and fetch authorization data 2. **Caches Locally**: Stores direct and indirect relations in local memory for fast access using LRU (Least Recently Used) eviction when caches reach their configured size limits 3. **Syncs Automatically**: Periodically polls Descope's services (based on `AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS`) to keep the cache up-to-date 4. **Serves Requests**: Responds to FGA check requests from your application SDKs with cached data When your application performs any FGA operations (ReBAC relation checks, ABAC attribute evaluations, schema queries, etc.), the SDK first queries the local cache service. If the data is available and fresh, it's returned immediately. If not, or if the cache needs to refresh, it falls back to querying Descope's authz service directly. ### Cache Behavior The cache uses a sophisticated two-tier caching strategy: - **Direct Relations Cache**: Stores direct relationships (e.g., `user1` directly owns `file1`) - **Indirect Relations Cache**: Stores indirect relationships (e.g., `user1` owns `file1` through group membership) **Cache Lookup Order**: 1. Check direct relations cache first 2. If not found, check indirect relations cache 3. If still not found, query Descope SDK and update cache with result **Cache Invalidation**: - **Schema changes**: Purges all caches (direct + indirect) to ensure consistency - **Relation additions/deletions**: Updates direct cache incrementally, purges indirect cache (since indirect relations may have changed) - **Remote polling**: Detects changes via polling and removes affected resources/targets from cache - **Polling errors**: By default, purges all caches for safety to prevent serving stale data. If `AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTES` is set to a positive value, the cache waits that long after the first error before purging. It keeps serving existing data, stale or not, during the wait, and cancels the purge if a poll succeeds before the window closes **Negative Caching**: Both allowed (`true`) and denied (`false`) authorization results are cached to avoid repeated remote queries for the same checks. **Write Operations**: When you create or delete relations through the cache service, the cache is updated immediately - you don't need to wait for the next polling cycle. This ensures write operations are immediately reflected in subsequent read operations. ### Lookup Cache In addition to the relation check cache, the AuthZ Cache also caches responses from lookup queries: - **WhoCanAccess** (`/v1/mgmt/authz/re/who`) — returns all targets that have a given relation to a resource - **WhatCanTargetAccess** (`/v1/mgmt/authz/re/targetall`) — returns all resources a given target can access The lookup cache has a configurable TTL and size limit. Before returning a cached result, it re-verifies every candidate against the relation check cache and drops any that no longer hold. Tune or disable the lookup cache independently of the relation check cache with the `AUTHZCACHE_LOOKUP_CACHE_*` environment variables. When disabled, lookup requests pass straight through to the Descope backend, so you can turn this cache off and still route lookup queries through the authzcache container. #### Lookup Cache Freshness Between refreshes, a cached lookup result can shrink but never grow. Re-verification removes candidates, and only a fresh backend query adds them. Change | When it reaches lookup results -- | -- Revocation written through the cache container | Immediately Revocation written directly to the backend | On the next successful poll New grant, either path | When the cached result's TTL expires ### Conditional Caching with CEL in ABAC The FGA cache allows for fully-supported local conditional evaluation. This particularly helps with ABAC, improving efficiency and correctness. Consider a ReBAC response that is cached and relies on ABAC. The CEL engine used in the cache can re-evaluate the conditions from ABAC with respect to the request's context locally. This eliminates the need for a roundtrip to the server; the cache can be used quickly and effectively to produce an accurate result. For example, given a schema with a conditional relation: ```yaml condition IsAdmin(role string) { role == "admin" } type doc relation viewer: user with IsAdmin ``` The cache stores the underlying `viewer` relation once. On each check, it evaluates `IsAdmin` locally against the `role` passed in the request context. Checking `viewer` for `user1` on `doc1` with `role: "admin"` returns `true`, and the same check with `role: "editor"` returns `false`, both served from the cache without a call to Descope. ### Remote Polling The service automatically starts polling for remote changes when a project cache is created. Polling behavior: - Polls Descope's `GetModified` API for changes since the last poll time - On schema changes → purges all caches - On relation changes → removes affected resources/targets from direct cache, purges indirect cache - If no cached relations exist → skips remote call but invalidates schema cache to detect schema changes - On polling errors → purges all caches for safety, unless `AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTES` delays the purge (see [Cache Invalidation](#cache-behavior) above) The polling interval is configurable via `AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS` (minimum 15,000ms). ### Run with Docker The management key must have proper [FGA read/write permissions](/management#management-key-roles) for the cache to function correctly. In order to deploy the FGA Cache service using Docker, you can use the following command: ```sh title="Terminal" docker run -d \ --name authzcache \ -p 8189:8189 \ -e HTTP_HOST=0.0.0.0 \ -e DESCOPE_MANAGEMENT_KEY=your_management_key_here \ descope/authzcache:latest ``` The service exposes the following port `8189` for HTTP REST API. The service uses gRPC internally and exposes HTTP REST API via gRPC-Gateway. ## Configuration ### Required Environment Variables - **`DESCOPE_MANAGEMENT_KEY`** - Your Descope management key for authentication. This key must have FGA read/write permissions. The management key is used by the cache service to authenticate with Descope's services. ### Optional Environment Variables - **`DESCOPE_BASE_URL`** - Custom Descope base URL (default: production Descope service) - **`CONTAINER_HTTP_PORT`** - HTTP gateway port (default: `8189`) - **`AUTHZCACHE_SDK_DEBUG_LOG`** - Enable debug logging of the internally used Descope SDK (`TRUE`/`FALSE`, default: `FALSE`) - **`AUTHZCACHE_DIRECT_RELATION_CACHE_SIZE_PER_PROJECT`** - Direct relation cache size per project (default: `1,000,000`). Note: This is per project - if you have multiple projects, each maintains its own cache of this size. - **`AUTHZCACHE_INDIRECT_RELATION_CACHE_SIZE_PER_PROJECT`** - Indirect relation cache size per project (default: `1,000,000`). Note: This is per project - if you have multiple projects, each maintains its own cache of this size. - **`AUTHZCACHE_REMOTE_POLLING_INTERVAL_IN_MILLIS`** - Remote polling interval in milliseconds (default: `15,000`). **Minimum value is 15,000ms (15 seconds)** - any value below this will be automatically increased to 15,000ms. - **`AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTES`** - Cooldown window in minutes before purging the cache on a remote polling error (default: `0`, meaning purge immediately). Set it to a positive value and the cache waits that long after the first error before purging, serving existing (possibly stale) data in the meantime. A successful poll during the window cancels the purge. - **`AUTHZCACHE_LOOKUP_CACHE_ENABLED`** - Enable the lookup cache for `WhoCanAccess`/`WhatCanTargetAccess` queries (`TRUE`/`FALSE`, default: `TRUE`) - **`AUTHZCACHE_LOOKUP_CACHE_SIZE_PER_PROJECT`** - Max number of lookup cache entries per project (default: `10,000`) - **`AUTHZCACHE_LOOKUP_CACHE_TTL_IN_SECONDS`** - TTL in seconds for lookup cache entries (default: `60`, minimum: `1`) - **`AUTHZCACHE_LOOKUP_CACHE_MAX_RESULT_SIZE`** - Skip caching lookup results larger than this size (default: `1,000`) - **`AUTHZCACHE_METRICS_REPORT_ENABLED`** - Enable cache metrics reporting to Descope backend for observability (`TRUE`/`FALSE`, default: `TRUE`). - **`AUTHZCACHE_METRICS_REPORT_INTERVAL_IN_SECONDS`** - Interval in seconds for reporting aggregated cache metrics to Descope backend (default: `60`, minimum: `10`). Only applies when metrics reporting is enabled. - **`AUTHZCACHE_HTTP_WRITE_TIMEOUT_IN_SECONDS`** - Max seconds the HTTP gateway spends producing a response before closing the connection (default: `30`). Takes precedence over the generic `HTTP_GATEWAY_WRITE_TIMEOUT`, which is still honored when this is unset. ## In Your Application To have the Descope SDK use this cache container for accelerating FGA operations (including ReBAC and ABAC checks), pass its URL via the `FGACacheURL` configuration field when initializing your Descope SDK client. Point the URL to the running container/service inside your local environment or cluster. Once configured, all authorization operations that use the FGA service will benefit from the local cache. The FGA Cache accelerates operations on `/v1/mgmt/fga/*` endpoints. Additionally, lookup queries — `WhoCanAccess` (`/v1/mgmt/authz/re/who`) and `WhatCanTargetAccess` (`/v1/mgmt/authz/re/targetall`) — are also cached via the lookup cache. Cached lookup candidates are always re-verified against the fresh check cache before being returned. ### URL Configuration - **Local Docker container**: `http://localhost:8189` (or the mapped host/port you selected) - **Kubernetes service**: Use the internal service DNS name, e.g., `http://authzcache.default.svc.cluster.local:8189` Ensure the container is reachable from where your code runs. Check network policies, firewall rules, and service mesh settings to allow communication between your application and the cache service. ### SDK Examples The cache proxy requires both `fgaCacheUrl` and `managementKey`. Without `managementKey`, requests use the standard Descope API instead. ```typescript title="app.ts" import DescopeClient from '@descope/node-sdk'; // Configuration constants const YourProjectID = 'your-project-id'; const YourFGAReadWriteApprovedMGMTKey = 'your-management-key'; // must have proper FGA permissions const URLToThisContainer = 'http://localhost:8189'; // or cluster service URL // Initialize the Descope SDK client with the AuthZ cache URL const descopeClient = DescopeClient({ projectId: YourProjectID, managementKey: YourFGAReadWriteApprovedMGMTKey, fgaCacheUrl: URLToThisContainer, }); // Continue with your application logic... ``` When you configure `fgaCacheUrl`, these FGA methods route through the cache proxy instead of the default Descope API: `saveSchema`, `createRelations`, `deleteRelations`, and `check`. If the cache proxy is unreachable or returns an error, the SDK falls back to the standard Descope API. `loadResourcesDetails` and `saveResourcesDetails` can only use the standard Descope API endpoints. ```go title="app.go" package main import ( "context" "github.com/descope/go-sdk/descope/client" ) const ( YourProjectID = "__ProjectID__" YourFGAReadWriteApprovedMGMTKey = "your-management-key" // must have proper FGA permissions URLToThisContainer = "http://localhost:8189" // or cluster service URL ) func main() { ctx := context.Background() // Initialize the Descope SDK client with the AuthZ cache URL err := client.InitDescopeSDKClient(ctx, &client.DescopeSDKClientConfig{ ProjectID: YourProjectID, MgmtKey: YourFGAReadWriteApprovedMGMTKey, FGACacheURL: URLToThisContainer, }) if err != nil { panic(err) } // Continue with your application logic... } ``` ```python title="app.py" from descope import DescopeClient # Configuration constants YOUR_PROJECT_ID = "__ProjectID__" YOUR_FGA_READ_WRITE_APPROVED_MGMT_KEY = "your-management-key" # must have proper FGA permissions URL_TO_THIS_CONTAINER = "http://localhost:8189" # or cluster service URL # Initialize the Descope SDK client with the AuthZ cache URL try: descope_client = DescopeClient( project_id=YOUR_PROJECT_ID, management_key=YOUR_FGA_READ_WRITE_APPROVED_MGMT_KEY, fga_cache_url=URL_TO_THIS_CONTAINER, ) except Exception as error: raise error # Continue with your application logic... ``` ## API Endpoints The service supports **multiple projects simultaneously** (multi-tenant). Each project maintains its own isolated cache instance with separate direct/indirect relation caches and independent remote polling. The service exposes REST API endpoints for managing FGA schemas, relations, and performing authorization checks. All endpoints require authentication with a bearer token via the `Authorization` header in the following format: `Bearer :` Create or update the FGA schema for your project. The schema defines the authorization model including namespaces, relation definitions, and permissions. **Request Body:** ```json { "dsl": "string" } ``` - **`dsl`** (string, required) - The FGA schema in [DSL (Domain-Specific Language) format](/authorization/rebac/define-schema) **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/fga/schema" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "dsl": "model AuthZ 1.0\ntype user\ntype doc\n relations\n define owner: [user]\n define viewer: [user]" }' ``` When relations are created through the cache service, the cache is updated immediately. You don't need to wait for the next polling cycle for these changes to be reflected. Create FGA relations (tuples) that define relationships between resources and targets. Relations are the actual data that represents who has access to what resources. **Request Body:** ```json { "tuples": [ { "resource": "string", "resourceType": "string", "relation": "string", "target": "string", "targetType": "string" } ] } ``` - **`tuples`** (array, required) - Array of relation tuples to create - **`resource`** (string, required) - The resource identifier (e.g., `"doc-123"`) - **`resourceType`** (string, required) - The type of resource (e.g., `"doc"`) - **`relation`** (string, required) - The relation definition name (e.g., `"owner"`, `"viewer"`) - **`target`** (string, required) - The target identifier, usually a user ID (e.g., `"user-456"`) - **`targetType`** (string, required) - The type of target (e.g., `"user"`) **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/fga/relations" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ { "resource": "doc-123", "resourceType": "doc", "relation": "owner", "target": "user-456", "targetType": "user" }, { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-789", "targetType": "user" } ] }' ``` When relations are deleted through the cache service, the cache is updated immediately. The direct cache is updated incrementally, and the indirect cache is purged to ensure consistency. Delete FGA relations (tuples) from your authorization model. This removes the specified relationships between resources and targets. **Request Body:** ```json { "tuples": [ { "resource": "string", "resourceType": "string", "relation": "string", "target": "string", "targetType": "string" } ] } ``` - **`tuples`** (array, required) - Array of relation tuples to delete - **`resource`** (string, required) - The resource identifier - **`resourceType`** (string, required) - The type of resource - **`relation`** (string, required) - The relation definition name - **`target`** (string, required) - The target identifier - **`targetType`** (string, required) - The type of target **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/fga/relations/delete" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-789", "targetType": "user" } ] }' ``` This endpoint is optimized for performance and uses the local cache to serve authorization checks. Both allowed (`true`) and denied (`false`) results are cached to avoid repeated remote queries for the same checks. Check if the given relations are allowed. This is the main endpoint for performing authorization checks. It determines whether a target (typically a user) has a specific relation to a resource. **Request Body:** ```json { "tuples": [ { "resource": "string", "resourceType": "string", "relation": "string", "target": "string", "targetType": "string" } ], "computePaths": false } ``` - **`tuples`** (array, required) - Array of relation tuples to check - **`resource`** (string, required) - The resource identifier to check access for - **`resourceType`** (string, required) - The type of resource - **`relation`** (string, required) - The relation definition to check (e.g., `"owner"`, `"viewer"`) - **`target`** (string, required) - The target identifier (usually a user ID) to check access for - **`targetType`** (string, required) - The type of target (usually `"user"`) - **`computePaths`** (boolean, optional) - If `true`, includes the full path of intermediate relations in the response. Default: `false` - **`context`** (object, optional) - A map of variable values to evaluate against [CEL conditions](/authorization/rebac/define-schema#conditions-abac-with-common-expression-language) attached to the checked relations, keyed by the parameter names declared on the condition. Required only if the tuple being checked has a condition attached via `with` to one of its underlying relations/permissions. **Response:** ```json { "tuples": [ { "allowed": true, "tuple": { "resource": "string", "resourceType": "string", "relation": "string", "target": "string", "targetType": "string" }, "info": { "direct": true, "path": { "steps": [ { "stepType": 0, "tuple": { "resource": "string", "resourceType": "string", "relation": "string", "target": "string", "targetType": "string" }, "permission": "string", "subPaths": [] } ] } } } ] } ``` - **`tuples`** (array) - Array of check results, one for each input tuple - **`allowed`** (boolean) - `true` if the relation is allowed, `false` otherwise - **`tuple`** (object) - The original tuple that was checked - **`info`** (object, optional) - Additional information about the check - **`direct`** (boolean) - `true` if the relation is direct (can only be changed by creating/deleting relations involving the resource or target), `false` if indirect - **`conditional`** (boolean) - `true` if the result was decided by evaluating a CEL condition in the schema - **`missingContext`** (array of strings, optional) - Names of context variables a condition needed but that weren't supplied - **`conditionalErr`** (string, optional) - The CEL evaluation error message if a condition couldn't be evaluated (e.g. a context value had the wrong type); `allowed` is `false` in that case - **`path`** (object, optional) - If `computePaths` was `true` and the check succeeded, contains the full path of intermediate relations between the target and resource **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/fga/check" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-456", "targetType": "user" } ], "computePaths": false }' ``` **Example Response:** ```json { "tuples": [ { "allowed": true, "tuple": { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-456", "targetType": "user" }, "info": { "direct": true } } ] } ``` **Example Request with a Conditional Relation:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/fga/check" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "tuples": [ { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-456", "targetType": "user" } ], "context": { "role": "admin" } }' ``` **Example Response:** ```json { "tuples": [ { "allowed": true, "tuple": { "resource": "doc-123", "resourceType": "doc", "relation": "viewer", "target": "user-456", "targetType": "user" }, "info": { "direct": false, "conditional": true } } ] } ``` This endpoint is served by the [lookup cache](#lookup-cache). Cached candidates are always re-verified against the check cache before being returned, so revocations written through the cache container, or already picked up by a successful poll, are never served. A newly granted relation, though, may not appear until the cached result's TTL expires; see [Lookup Cache Freshness](#lookup-cache) for details, including the staleness bounds that apply to revocations made directly against the backend. Find all targets that have a given relation to a resource. **Request Body:** ```json { "resource": "string", "relationDefinition": "string", "namespace": "string" } ``` - **`resource`** (string, required) - The resource identifier to query (e.g., `"doc-123"`) - **`relationDefinition`** (string, required) - The relation definition to query (e.g., `"viewer"`) - **`namespace`** (string, required) - The namespace of the relation definition (e.g., `"doc"`) **Response:** ```json { "targets": ["string"] } ``` - **`targets`** (array of strings) - The identifiers of all targets that have the given relation to the resource **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/authz/re/who" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "resource": "doc-123", "relationDefinition": "viewer", "namespace": "doc" }' ``` **Example Response:** ```json { "targets": ["user-456", "user-789"] } ``` This endpoint is served by the [lookup cache](#lookup-cache). Cached candidates are always re-verified against the check cache before being returned, so revocations written through the cache container, or already picked up by a successful poll, are never served. A newly granted relation, though, may not appear until the cached result's TTL expires; see [Lookup Cache Freshness](#lookup-cache) for details, including the staleness bounds that apply to revocations made directly against the backend. Find all resources (and the relations through which they're reachable) that a given target can access. **Request Body:** ```json { "target": "string" } ``` - **`target`** (string, required) - The target identifier to query, usually a user ID (e.g., `"user-456"`) **Response:** ```json { "relations": [ { "resource": "string", "relationDefinition": "string", "namespace": "string", "target": "string", "targetNamespace": "string", "targetSetResource": "string", "targetSetRelationDefinition": "string", "targetSetRelationDefinitionNamespace": "string" } ] } ``` - **`relations`** (array) - All relations through which the target can reach a resource - **`resource`** (string) - The resource the relation is defined on - **`relationDefinition`** (string) - The name of the relation definition - **`namespace`** (string) - The namespace of the relation definition - **`target`** (string) - The target for the relation - **`targetNamespace`** (string) - The namespace of the target - **`targetSetResource`**, **`targetSetRelationDefinition`**, **`targetSetRelationDefinitionNamespace`** (string, optional) - Present instead of `target`/`targetNamespace` when the relation resolves through a target set (anyone who has another relation), identifying that other relation **Example Request:** ```bash curl -X POST "http://localhost:8189/v1/mgmt/authz/re/targetall" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "target": "user-456" }' ``` **Example Response:** ```json { "relations": [ { "resource": "doc-123", "relationDefinition": "viewer", "namespace": "doc", "target": "user-456", "targetNamespace": "user" } ] } ``` The service exposes health check endpoints for container orchestration: **HTTP**: `GET http://localhost:8189/healthz` This endpoint can be used by Kubernetes liveness and readiness probes, or other orchestration tools to monitor the service health. **Example Request:** ```bash curl -X GET "http://localhost:8189/healthz" ``` ## Deployment Considerations - The cache automatically syncs with remote authorization data based on the polling interval environment variable - Ensure the container is reachable from where your code runs (network policy / firewall / service mesh settings) - The cache size can be configured per project to match your authorization data volume. Each project maintains its own isolated cache. - Caches use LRU (Least Recently Used) eviction - when a cache reaches its size limit, the least recently used entries are evicted to make room for new entries. - For production deployments, consider running multiple cache instances behind a load balancer for high availability - The service supports multiple projects simultaneously - each project gets its own cache instance with isolated data and independent polling # Authorization (/authorization) Learn how to easily implement authorization via RBAC and Fine-Grained Authorization (FGA) within the backend of your app with Descope. # Authorization Descope provides two primary authorization approaches for your application: - **Role-Based Access Control (RBAC)** - Simple, role-centric authorization using roles and permissions - **Fine-Grained Authorization (FGA)** - Advanced authorization using the authz service, including ReBAC and ABAC **RBAC and FGA are not mutually exclusive.** You can use both approaches together in your application. For example, you might use RBAC for basic role-based permissions while using FGA for more complex, relationship-based or attribute-based access control. ## Role-Based Access Control (RBAC) [Role-Based Access Control (RBAC)](/authorization/role-based-access-control) is a straightforward authorization model where you create roles and assign permissions to them. Users are assigned roles, and access is granted based on the permissions associated with those roles. **Roles and Permissions**: All permissions must be associated with a role. Roles can have specific permissions assigned (e.g., "Admin" role with "documents:read", "documents:write" permissions) or be standalone roles without pre-defined permissions for organizational or programmatic use. **Project-Level and Tenant-Level Roles**: Roles can be created at the project level (shared across all tenants) or tenant level (specific to individual tenants). This enables tenant separation, allowing users to have different permissions depending on which tenant they're signed into (e.g., "Admin" in Tenant A, "Viewer" in Tenant B). ## Fine-Grained Authorization (FGA) **Fine-Grained Authorization (FGA)** refers to any authorization that uses Descope's authz service (`/v1/mgmt/authz/*` endpoints). FGA includes: - **[Relationship-Based Access Control (ReBAC)](/authorization/rebac)** - Define permissions based on relationships between users and resources - **[Attribute-Based Access Control (ABAC)](/authorization/abac)** - Gate access on user, resource, and environmental attributes, either as CEL conditions attached to a ReBAC schema or as custom attributes checked in your code FGA provides more granular and flexible authorization than RBAC, allowing you to model complex access control scenarios with relationships, attributes, and dynamic policies. ### FGA Cache For applications using FGA, you can deploy the **[FGA Cache](/authorization/fga-cache)** service to accelerate authorization checks by caching authorization data locally within your cluster. The FGA Cache applies to all authorization operations that use the authz service, including ReBAC and ABAC checks. ## Choosing the Right Authorization Approach ### When to Use RBAC Use RBAC when you need: - **Role-based permissions**: Assign specific permissions to roles (e.g., Admin role has "delete" and "modify" permissions, while Viewer role only has "read" permission) - **Tenant-specific roles**: Different roles per tenant (e.g., Tenant A has "Manager" and "Employee" roles, while Tenant B has "Admin" and "User" roles) - **Simple, scalable access control**: A straightforward permission model with clearly defined roles that can scale with your organization - **Easy permission management**: The ability to assign and change roles without modifying individual user permissions ### When to Use FGA (ReBAC or ABAC) - **Complex Relationships**: When your application's access control needs to reflect complex relationships between users and resources, [ReBAC](/authorization/rebac) offers the flexibility you require. - **Dynamic Permissions**: If permissions need to change frequently based on context or user relationships, ReBAC can dynamically adjust access rights accordingly. - **Contextual Access Control**: [ABAC](/authorization/abac) is ideal when you need to incorporate a wide range of attributes, such as user characteristics, resource types, and environmental context (e.g., time of access), to make authorization decisions. - **Fine-Grained Control**: For applications that need to control access at a more granular level than roles, FGA provides the ability to define precise access control policies. ### Using Both Together You can combine RBAC and FGA in your application. For example: - Use **RBAC** for basic role-based permissions (e.g., Admin, Editor, Viewer) - Use **FGA (ReBAC)** for resource-specific access (e.g., "user can edit document X because they own it") - Use **FGA (ABAC)** for context-based access (e.g., "user can access resource during business hours if they have Manager role") This hybrid approach allows you to leverage the simplicity of RBAC for common cases while using FGA for more complex authorization scenarios. # Overview (/flows) Learn about Descope Flows, a visual no-code interface to build screens and authentication flows for common user interactions with your application # Flows Descope Flows are visual, drag-and-drop workflows designed to simplify the creation of user authentication journeys, built in the [Descope Console](https://app.descope.com/flows). With flows, you draw arrows between blocks on the canvas. There is no separate "orchestration" layer — the flow **is** the orchestration. By abstracting complex authentication logic, Flows enable developers to build secure, customizable user journeys without writing any code. The entry point is the flow itself (selected by flow ID within your app's SDK integration) or using Descope as a federated identity provider, and the output is a Descope [session token (JWT)](/sessions) issued at the end of the flow. There are two types of flows in Descope: - **Interactive Flows**: User-facing authentication flows that handle login, registration, password reset, and other user interactions - **Management Flows**: Backend automation flows for administrative tasks, user management, and system integrations Throughout most of the documentation, typically when we refer to "flows", we are referring to Interactive Flows. For more information on Management Flows, see [Management Flows](/flows/management-flows). ## Why Use Flows? Flows provide several key advantages over traditional authentication implementation: - **Rapid Development**: Build complex authentication sequences in minutes instead of weeks - **Security First**: Built-in security best practices and automatic updates - **Consistent UX**: Maintain a uniform experience across your application - **Easy Maintenance**: Update logic without deploying code - **Flexible Integration**: Works seamlessly with your existing tech stack ## Flow Components Flows are constructed using a drag-and-drop Flow Builder, by connecting the following four types of building blocks together: - [**Screens**](/flows/screens) - the **UI layer**. - Each screen is a form that collects user input — login credentials, profile fields, MFA codes, consent checkboxes. - You design screens with a component editor (text inputs, dropdowns, buttons, images) and they're rendered by Descope's client SDK in your app or on our [hosted page](/identity-federation/auth-hosting). - [**Actions**](/flows/actions) - the **logic layer**. - An action performs a single task: authenticate with email/password, send an OTP, verify a magic link, create a user, update user properties, assign roles, add custom claims to the JWT, and more. - Each action has configurable [error handling](/handling-flow-errors/customizing-flow-errors#customizing-flow-errors-in-flow-builder) — Automatic (return to previous screen with error), Mitigate (silently continue), Continue (pass error to next step), or Ignore. - [**Conditions**](/flows/conditions) - the **branching layer**. - A condition evaluates one or more expressions against dynamic values from the flow context (user attributes, form inputs, device info, risk scores, connector responses, JWT claims) and routes the flow down different paths. - [**Connectors**](/connectors) - the **integration layer**. - A connector makes an HTTP request to an external service during the flow — call your backend API, query a database, send a notification, validate data with a third-party service. - Connector responses are stored in the flow context and accessible by subsequent conditions, actions, or screens. Three additional building blocks that round out a typical flow are: - [**Scriptlets**](/flows/actions/scriptlets): These let you write custom JavaScript inline within a flow for data manipulation (string formatting, hashing, date math, conditional logic). - [**Subflows**](/flows/intro-to-flows/subflows): These let you embed one flow inside another for reuse. - [**Flow Notes**](/flows/intro-to-flows/flow-notes): These let you attach explanatory notes and labels to steps in the flow editor as inline documentation. They have no effect on flow execution. ![Descope flow example](/assets/flows-screenshot.webp) These components work together to define the logic and UI of your authentication processes. ## Get Started with Flows For detailed information about editing and managing flows, see [Managing Flows](/management/flows). 1. **Start Simple**: Begin with basic flows from our [flow library](/flows/intro-to-flows/flow-library), and gradually add complexity 2. **Test Thoroughly**: Use the [flow-runner](/handling-flow-errors/troubleshooting-flows#flow-runner) to test all scenarios 3. **Monitor Performance**: Track [flow completion rates and user behavior](/management/project-settings/project-dashboard) and iterate on your flows accordingly ## Testing Authenticated Flows Some flows are designed to run in the context of an already-authenticated user — step-up MFA, profile updates, impersonation, MFA enrollment, and other post-authentication journeys. The flow runner starts an unauthenticated session by default, but you can provide a refresh JWT as input to simulate an existing user session. See [Testing Authenticated Flows with JWT Input](/flows/use-cases/flow-runner-jwt-input) for details. # Getting Started (/getting-started) Quickstart guide on how to implement Descope Flows and authentication methods. # Quickstart Guide Descope makes it very easy to enable a variety of different user authentication methods in your application. The recommended approach of integrating Descope in your application, which is also the most straightforward and extensible way, is to use [Descope Flows](/flows). This guide will cover the basics on how you can implement Descope in your application. All you'll need is your [Project ID](https://app.descope.com/settings/project) to get started.
## How do you plan to use Descope? Flows are the easiest and recommended approach to implementing auth in your application. With either the **Native Implementation** or **Hosted Application**, you can easily develop a beautiful, frictionless, and secure authentication experience for your customers. However, there are two other ways you can utilize Descope in your application without using Flows. You can read about these two other implementation methods below. ## Building a B2B app with SSO? If you need SAML or OIDC single sign-on for your business customers, start with the dedicated SSO quickstart. It walks you through configuring a connection, adding SSO to your app, and testing it end-to-end (including how to test without your own identity provider). ## Not Using Flows? If you would like to build out your own frontend login experience, but you want to rely on Descope for just the session management in the frontend, you can use our **Client SDKs**. If you would like to build out your frontend completely from scratch, including the session management, and build out your own custom APIs for authentication with your backend, you can use our **Backend SDKs**. # OIDC Endpoints (/getting-started/oidc-endpoints) Get started with Descope's OIDC endpoints using Descope as an OIDC provider. # Descope OIDC Endpoints Quickstart If you have deployed your own OIDC client, Descope can become your IdP. OpenID Connect (OIDC) operates over OAuth 2.0, using endpoints for different parts of the authentication and authorization processes. Understanding these endpoints and how to leverage them is key to integrating OIDC authentication. ## Available OIDC Endpoints Descope's OIDC offers the following primary endpoints: - **Authorization Endpoint:** This endpoint initiates the authorization flow leading to the return of an `id_token` and an `access_token`. The endpoint for this service is: `__BaseURL__/oauth2/v1/authorize` - **Token Endpoint:** Once you have an authorization code from the Authorization Endpoint, you can request tokens from this endpoint. Access it at: `__BaseURL__/oauth2/v1/token` - **UserInfo Endpoint:** After you've obtained an `access_token`, you can retrieve detailed information about the authenticated user from this endpoint. Find it at: `__BaseURL__/oauth2/v1/userinfo` - **JWKs URI:** Endpoint containing the JSON Web Key Set, which is a set of keys containing the public key used to verify any JSON Web Token (JWT) issued by the authorization server. Access it at: `__BaseURL__/__ProjectID__/.well-known/jwks.json` - **End Session Endpoint:** Used to end the user's session. The endpoint for this service is: `__BaseURL__/oauth2/v1/logout` - **Revocation Endpoint:** This is where you can revoke tokens. Visit: `__BaseURL__/oauth2/v1/revoke` ## Configuring Descope with OIDC using Endpoints OpenID Connect (OIDC) allows you to authenticate users via an external Identity Provider. Below, we'll guide you on how to use the provided OIDC endpoints to integrate Descope and OIDC. Descope supports the following grant types with OIDC: - **[Authorization Code Flow (with PKCE)](#guide-to-using-oidc-endpoints)** - most commonly used grant type, for safe and secure authentication and token generation - **[Client Credentials Flow](#client-credentials-flow)** - usually used for machine-to-machine (m2m) authentication ### Using PKCE in Your Endpoints PKCE (Proof Key for Code Exchange) is an extension to OAuth 2.0 for public clients (e.g., mobile applications), preventing interception attacks during the OAuth authorization code flow. In the PKCE flow: 1. **Code Verifier Generation**: The client creates a large random string called the "code verifier". 2. **Code Challenge Calculation**: The client then hashes the code verifier to create the "code challenge", typically using SHA-256. 3. **Authorization Request**: The client starts the authorization code flow by sending the code challenge and its method (S256 for SHA-256) to the authorization server when accessing the `/authorize` endpoint. 4. **Token Exchange**: Once the client receives the authorization code, it sends a request to the token endpoint, including the code verifier. The server will hash the verifier and ensure it matches the challenge sent earlier. ### Guide to Using OIDC Endpoints Before using any of the endpoints, you will need to generate a Code Verifier and Code Challenge, which you can do [here](https://tonyxu-io.github.io/pkce-generator/). You can also do this yourself following the code below: ```javascript function generateCodeVerifier() { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const charactersLength = characters.length; for (let i = 0; i < 128; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } function generateCodeChallenge(verifier) { return crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)) .then(arrayBuffer => { const base64Url = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))) .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); return base64Url; }); } const codeVerifier = generateCodeVerifier(); const state = generateCodeVerifier(); const codeChallenge = await generateCodeChallenge(codeVerifier); ``` After generating the Code Verifier and Challenge, you should have something like this: **Code Verifier:** `N7QLWqAp2aYxDGRtqbXjbfusYLE97XAui-nnW9hOofI` **Code Challenge:** `KM6LN2hMVS5xeS3CHhoNuHtqtWD0-EIbkIq8u_KZl3U` **State:** `KM6LN2fFDS32duHtqtWD0-EIbkIq8u_KfG3U` Then you'll need to actually begin the OIDC flow, to generate all of the necessary tokens you'll need (e.g. id_token, access_token). 1. **Make Authorization Request**: ```javascript const authUrl = `__BaseURL__/oauth2/v1/authorize?response_type=code&client_id=__ProjectID__&redirect_uri=YOUR_REDIRECT_URI&scope=openid&code_challenge=${codeChallenge}&code_challenge_method=S256&state={state}&flow=YOUR_FLOW_ID`; // Redirect the user to the authorization URL window.location.href = authUrl; ``` The `flow` query parameter can be left out of the authorization request if set in the Flow Hosting URL of your OIDC Application. Once the user has authorized the application, they will be redirected to the `redirect_uri` you specified, with an authorization code attached as a query parameter. 2. **Exchange Authorization Code (w/ Code Verifier) for Tokens:** Send a POST request to the Token Endpoint with the authorization code to exchange it for tokens. ```javascript const tokenUrl = '__BaseURL__/oauth2/v1/token'; const tokenData = { grant_type: 'authorization_code', code: 'AUTHORIZATION_CODE_RECEIVED', redirect_uri: 'YOUR_REDIRECT_URI', client_id: '__ProjectID__', code_verifier: codeVerifier, }; // Use fetch or any HTTP client to POST the data to the tokenUrl ``` 3. **Fetching User Information:** With the acquired `access_token`, you can send a GET request to the UserInfo Endpoint to get detailed user information. ```javascript const userInfoUrl = '__BaseURL__/oauth2/v1/userinfo'; fetch(userInfoUrl, { method: 'GET', headers: { 'Authorization': `Bearer YOUR_ACCESS_TOKEN` }, }) .then(response => response.json()) .then(data => { console.log(data); }); ``` The `userinfo` endpoint should return both roles and custom claims of the user, if you originally requested the correct scopes from the `/authorize` endpoint above for `descope.claims` and `descope.custom_claims`. Example Response: ``` { "aud": "P2OsuPlphesdfsdf3wbMx5M59", "email": "test@descope.com", "email_verified": true, "iss": "__BaseURL__/P2OsuPlphesdfsdf3wbMx5M59", "name": "Kevin Gao", "nsec": { "dob": "1979-01-01", // Date of Birth Custom Attribute "scope": "openid profile phone email descope.custom_claims descope.claims" }, "picture": "https://lh3.googleusercontent.com/a/ACg8ocKKtkt5NfqoqUbio8hcxWssW3jjApFczAwk5o7sczh4UDnyI78=s96-c", "sub": "U2S7Ga6GSYsdf233uG4lbKBEh0K", "tenants": { "T2OtO3ngPunQLsdfd2yZsRpRf": { "permissions": [ "SSO Admin" ], "roles": [ "Basketball Coach" ] } } } ``` As you can see, the `dob` custom attribute (added via a JWT template) and the `tenants` with roles and permissions are added to the `/userinfo` response. 4. **Logging out the User:** To log the user out, redirect them to the End Session Endpoint. You will need to have access to the `id_token` created in Step 2, to use this endpoint. ```javascript const logoutUrl = '__BaseURL__/oauth2/v1/logout?id_token_hint=YOUR_ID_TOKEN&post_logout_redirect_uri=YOUR_POST_LOGOUT_REDIRECT_URI'; window.location.href = logoutUrl; ``` 5. **Token Revocation:** If for any reason, you need to invalidate a token (for instance, the user changed their password), send a POST request to the Revocation Endpoint with the token you wish to revoke. ```javascript const revokeUrl = '__BaseURL__/oauth2/v1/revoke'; const revokeData = { token: 'TOKEN_TO_BE_REVOKED', client_id: '__ProjectID__', }; fetch(revokeUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams(revokeData), }) .then(response => { if (response.status === 200) { console.log("Token revoked successfully"); } else { console.error("Failed to revoke the token"); } }); ``` 6. **Verify JWTs:** To verify JWTs, fetch the public key from the JWKs URI and use it to verify the signature of any JWTs you receive. If you want to have your own hosted Descope Flow, you can use [this repository](https://github.com/descope/auth-hosting) as a template, host it (using Vercel, localhost, etc.), and change the Flow Hosting URL in the Descope Console. ### Passing Dynamic Values to a Flow In some scenarios, you may need to pass dynamic values into a flow for use in screen or conditional blocks. However, when using OIDC, you don’t have direct access to the web component to manually pass these values. Therefore, when using OIDC, you can pass values through the `/authorize` URL and extract them within the flow using a [Scriptlet](/flows/actions/scriptlets). For example, consider the following authorize URL, where we pass two values: `fruit=apple` and `age=60`: ``` https://${baseURL}/oauth2/v1/authorize? response_type=code &client_id=${client_id} &redirect_uri=${redirect_uri} &scope=openid &code_challenge=${codeChallenge} &code_challenge_method=S256 &state=${codeVerifier} &dynamic_val={"fruit":"apple","age":60} ``` When you start the OIDC flow, these parameters will appear in the URL bar. You can access them within the flow using the key `device.location.uri` and then extract the values using a scriptlet. To process the parameters, pass the `device.location.uri` key as an argument to the scriptlet, as shown here: ![OIDC Dynamic Values Scriptlet Argument](/assets/oidc-dynamic-values-arguments.webp) Below is the Scriptlet code that extracts the query parameters and makes them available for use in your flow: ``` const uri = startUrl; // Extract query string const queryString = uri.split("?")[1] || ""; // Convert query string into an object const queryParams = Object.fromEntries( queryString .split("&") .map(param => param.split("=").map(decodeURIComponent)) ); // Parse JSON payload from dynamic_val let dynamicValues = {}; try { dynamicValues = JSON.parse(queryParams["dynamic_val"] || "{}"); } catch (e) { dynamicValues = {}; } return { fruit: dynamicValues.fruit, age: dynamicValues.age }; ``` Once the Scriptlet runs, you can reference the extracted values (fruit and age) within conditions and screens in your flow. Here’s an example of using the Scriptlet context keys for conditions: ![OIDC Dynamic Values Scriptlet Condition](/assets/oidc-dynamic-values-condition.webp) ![OIDC Dynamic Values Flows](/assets/oidc-dynamic-values-flow.webp) ### Client Credentials Flow With the Client Credentials grant type, applications request access tokens directly without any user intervention. It authenticates the application using its client ID and secret. This is typically used in Machine-to-Machine applications. You must use `Client Secret Basic` when handling client secrets in the HTTP request for the access token. You will need to encode the Client ID with a Descope Access Key. Then in the Authorization header, you'll need to take that string and append it after the word `Basic`. The Client ID will be an encoded string tied to the specific Access Key that's generated. The Client Secret will be actual [Access Key](https://app.descope.com/m2m/accessKeys) secret generated in the Console. ![Client ID under Access Keys](/assets/client-id-access-key.webp) Once you've put the `:` (`UDJQRDhI*********DBaQ2kwdzp:P2PD8H2*******00ZCi0w`) together, you'll need to base64 encode the string to make something like this: `UDJWSGFwODVOY1JJR***************jJRZERzTGM3TTZjUmwyNTB5SXBHeVc=`. You can see this in the example given below: ```sh title="Terminal" curl -X POST \ __BaseURL__/oauth2/v1/token \ -H 'Authorization: Basic UDJWSGFwODVOY1JJR***************jJRZERzTGM3TTZjUmwyNTB5SXBHeVc=' \ -d 'grant_type=client_credentials&scope=openid%20profile%20email%20phone' ``` After making this POST request, you should be able to retrieve the necessary access token for your M2M connection. If you wish to add custom claims to your JWT created with this Client Credentials flow, utilize our [Access Key JWT Templates](/management/token/jwt-templates#access-key-jwt-templates) under your [Project Settings](https://app.descope.com/settings/project/jwt). Here is a diagram illustrating the Client Credentials flow: ![Client Credentials Flow Authentication Diagram](/assets/client-creds-auth-diagram.webp) ### Silent Authentication Silent authentication is a process that allows applications to authenticate users without interactive login prompts, thereby providing a seamless user experience. Silent auth leverages the user's existing login session to grant or renew access tokens without requiring the user to actively re-enter their credentials. It's typically implemented using an invisible iframe or a background HTTP request in web applications. #### Use Cases for Silent Authentication - **Single Page Applications (SPAs)** - SPAs can refresh tokens without reloading the entire page or disrupting the user experience. - **Maintaining User Sessions** - Automatically renew user sessions in the background to avoid session timeouts. - **Microservices Architecture** - Seamless authentication across different microservices without prompting the user repeatedly. #### How to Use Silent Authentication If you're using the `/authorize` endpoint listed [above](#guide-to-using-oidc-endpoints), then all you'll need to do to attempt a silent auth request is include `prompt=none` as a parameter like this: ```sh title="Terminal" __BaseURL__/oauth2/v1/authorize?response_type=code&client_id=__ProjectID__&...&prompt=none ``` If you're looking for how you can alter the `prompt` parameter when signing in with other OAuth providers in your flow, you can visit this [documentation](/auth-methods/oauth#prompt). ### Experimental Playground To have a better understanding and practical experience with these endpoints, you can experiment with the [OAuth Tools Collection](https://oauth.tools/c/bd1c944c341b12888fa748ac#WEQoTTMykFCzKnf6deGXY1IxqlTuW8Ntf5md7CBugQY=). This platform provides a visual and interactive way to work with all the available OIDC endpoints. It's especially useful to see how the `access_token` and `id_token` are utilized across different requests. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Agent Auth SDK (/agentic-identity-hub/agent-auth-sdk) Sign your agents in to Descope and fetch Resource and Connection tokens for the APIs and MCP servers they call, with the Agent Auth SDK. # Agent Auth SDK The [Descope Agent Auth SDK](https://github.com/descope/descope-agent-auth) is a client-side SDK, in Python and TypeScript, that does two things for a custom agent: 1. **Signs your agent in** to Descope and gets a Descope token. 2. **Fetches the tokens it needs**, a [Connection](/agentic-identity-hub/core-components/connections) token or a [Resource](/resources) token, whenever the agent calls an API or MCP server. It is the identity layer under your agent's tool calls. You still write the tool code and API wrappers; the SDK gets each tool the token it needs, and Descope stores and refreshes those tokens so your code never holds a long-lived secret. This SDK is the OAuth **client** side, fetching the tokens an agent's tools need. To **protect** an MCP server you built (client registration, token validation, tool scopes), use Descope MCP Auth, covered in [MCP Servers](/agentic-identity-hub/core-components/mcp-servers) and the [MCP SDKs](/mcp/sdks). Inside a server's tool handler, use this SDK to fetch the downstream token the tool needs. ## Install ```bash pip install descope-agent-auth ``` ```bash npm install @descope/agent-auth ``` The two packages are kept identical, so the mental model transfers. TypeScript names are the camelCase of the Python ones. ## Two kinds of token Everything the SDK fetches is one of two kinds. The distinction mirrors [Resources vs. Connections](/agentic-identity-hub#resources-and-connections) in the Agentic Identity Hub. | Your agent needs to… | Method | Token | | --- | --- | --- | | Call your own API or MCP server, protected by Descope as the authorization server | `resources.get_token` | [Resource](/resources) token, minted on demand via token-exchange | | Call a third-party OAuth provider (GitHub, Slack, Google) | `connections.get_token` | provider OAuth token from the [Connections](/agentic-identity-hub/core-components/connections) vault | | Call a third-party or internal API with a stored API key | `connections.get_token` | API key from the Connections vault | A **Resource token** needs no provisioning: it is minted on demand for the agent. A **Connection token** must already exist in the vault, put there either when the user connected the account (OAuth consent) or when your backend wrote an API key through the Management API. ## Quickstart Sign the agent in once, then fetch tokens whenever a tool runs. If the user has not connected the account yet, `get_token` raises `ConnectionAuthorizationRequired`, which carries a connect URL to send them through. ```python from descope_agent_auth import AgentAuthClient, AccessTokenProvider from descope_agent_auth.errors import ConnectionAuthorizationRequired client = AgentAuthClient( project_id="P2...", credential=AccessTokenProvider(access_token=user_jwt), ) try: github = client.connections.get_token(connection="github", identifier="user@example.com") use(github.access_token) # a fresh, scoped GitHub token except ConnectionAuthorizationRequired as e: redirect_user_to(e.connect_url) # the user hasn't linked GitHub yet ``` ```ts import { AgentAuthClient, AccessTokenProvider, ConnectionAuthorizationRequired } from '@descope/agent-auth'; const client = new AgentAuthClient({ projectId: 'P2...', credential: new AccessTokenProvider({ accessToken: userJwt }), }); try { const github = await client.connections.getToken({ connection: 'github', identifier: 'user@example.com' }); use(github.accessToken); // a fresh, scoped GitHub token } catch (e) { if (e instanceof ConnectionAuthorizationRequired) { redirectUserTo(e.connectUrl); // the user hasn't linked GitHub yet } else { throw e; } } ``` ## How your agent signs in Pick how the agent authenticates to Descope, configured once at client init by passing a `credential`. When the agent signs in with OAuth client credentials it becomes a first-class identity in your [Agent Directory](/agentic-identity-hub/core-components/agents), and [policies](/agentic-identity-hub/policies) govern what its token can obtain. | Provider | Use when | | --- | --- | | `ClientCredentialsProvider` | Autonomous agent, no user (M2M) | | `AccessTokenProvider` | You already hold a user's Descope token from your app's login | | `DeviceCodeProvider` | Headless or CLI agent with no browser | | `CibaProvider` | Out-of-band user approval (push), and the approval gate for sensitive calls | | `JwtBearerProvider` | Exchange a signed JWT from a trusted issuer (RFC 7523) | | `ManagementKeyProvider` | Privileged static key that **bypasses policies**; not recommended, requires explicit opt-in | A client-credentials (M2M) sign-in **cannot** read a *user-level* Connection token. To read a user's token, sign in with that user's access token (`AccessTokenProvider`, `DeviceCodeProvider`, or `CibaProvider`) or use a management key. ## Autonomous vs. Acting for a User **Autonomous: acts as itself.** With client credentials the agent mints Resource tokens scoped to itself and reads **tenant-level** Connection tokens for a tenant it belongs to. ```python from descope_agent_auth import AgentAuthClient, ClientCredentialsProvider client = AgentAuthClient( project_id="P2...", credential=ClientCredentialsProvider(client_id="...", client_secret="..."), ) res = client.resources.get_token(resource="urn:my-api", scopes=["read"]) # agent-scoped slack = client.connections.get_tenant_token(connection="slack", tenant_id="acme") # org-shared ``` ```ts import { AgentAuthClient, ClientCredentialsProvider } from '@descope/agent-auth'; const client = new AgentAuthClient({ projectId: 'P2...', credential: new ClientCredentialsProvider({ clientId: '...', clientSecret: '...' }), }); const res = await client.resources.getToken({ resource: 'urn:my-api', scopes: ['read'] }); // agent-scoped const slack = await client.connections.getTenantToken({ connection: 'slack', tenantId: 'acme' }); // org-shared ``` **Acting for a user.** Supply the user's access token to read that user's Connection tokens or mint a user-scoped Resource token. Bind it at init, or pass it per call with `act_as_user_token` on a shared client. ```python from descope_agent_auth import AgentAuthClient, AccessTokenProvider client = AgentAuthClient(project_id="P2...", credential=AccessTokenProvider(access_token=user_jwt)) gh = client.connections.get_token(connection="github", identifier=user_id) # or per call on a shared client: gh = client.connections.get_token(connection="github", identifier=user_id, act_as_user_token=user_jwt) ``` ```ts import { AgentAuthClient, AccessTokenProvider } from '@descope/agent-auth'; const client = new AgentAuthClient({ projectId: 'P2...', credential: new AccessTokenProvider({ accessToken: userJwt }) }); await client.connections.getToken({ connection: 'github', identifier: userId }); // or per call on a shared client: await client.connections.getToken({ connection: 'github', identifier: userId, actAsUserToken: userJwt }); ``` ## Connect to a Resource Use `resources.get_token` for an API or MCP server that **you** build and protect with Descope as the [authorization server](/resources). The token is minted via token-exchange with no prior authorization step; what you signed in with sets the scope (a user token is user-scoped, client credentials are agent-scoped). ```python res = client.resources.get_token( resource="urn:my-api", # RFC 8707 resource indicator scopes=["orders.read"], audience="https://api.example.com", # optional RFC 8693 audience ) call_my_api(res.access_token) ``` ```ts const res = await client.resources.getToken({ resource: 'urn:my-api', // RFC 8707 resource indicator scopes: ['orders.read'], audience: 'https://api.example.com', // optional RFC 8693 audience }); callMyApi(res.accessToken); ``` ## Connect to a Connection Use `connections.get_token` (user-level) or `connections.get_tenant_token` (org-shared) for a credential in the [Connections](/agentic-identity-hub/core-components/connections) vault. The credential has to exist first, created when the user completes OAuth consent or when your backend writes an API key through the Management API. When the user has not connected yet, catch `ConnectionAuthorizationRequired` and send them to its `connect_url`, or generate the URL proactively with `get_connect_url` and poll with `wait_for_connection`. ```python url = client.connections.get_connect_url(connection="github", identifier=user_id) if url: redirect_user_to(url) github = client.connections.wait_for_connection(connection="github", identifier=user_id) ``` ```ts const url = await client.connections.getConnectUrl({ connection: 'github', identifier: userId }); if (url) { redirectUserTo(url); const github = await client.connections.waitForConnection({ connection: 'github', identifier: userId }); } ``` ## Errors All errors extend `AgentAuthError`; match them with `isinstance` (Python) or `instanceof` (TypeScript). | Error | Meaning | | --- | --- | | `ConnectionAuthorizationRequired` | The user hasn't connected this account. Carries `connect_url` / `connectUrl`, `connection`, and `identifier`. | | `PolicyDenied` | The credential lacks [policy](/agentic-identity-hub/policies) permission for the requested token. | | `ApprovalDenied` / `ApprovalTimeout` | A CIBA approval gate was rejected or timed out. | | `CredentialAcquisitionFailed` | The agent could not sign in to Descope. | | `TokenExchangeFailed` | Any other token-fetch failure. | ## Framework support The SDK is framework-agnostic and fetches tokens for the tools you implement in any framework: LangChain, LangGraph, Google ADK, OpenAI, Vercel AI, Mastra, LlamaIndex, Cloudflare Agents, CrewAI, and the Anthropic SDK, among others. A `with_connection` (`withConnection`) tool wrapper injects a fresh scoped token into a tool call. Descope can also manage an agent's connection to a **remote MCP server** through the SDK's MCP auth adapter. See [`docs/FRAMEWORKS.md`](https://github.com/descope/descope-agent-auth/blob/main/docs/FRAMEWORKS.md) in the repo for a copy-paste snippet per framework. # Agentic Identity Hub (/agentic-identity-hub) Manage authentication, authorization, and external credentials for AI agents using Descope's Agentic Identity Hub. # Agentic Identity Hub The Agentic Identity Hub is Descope's control plane for AI agent identity. It covers every agent that calls your APIs, every tool your agents use, and every credential those tools need. ## Why Agents Need Different Identity Infrastructure Traditional IAM assumes software follows a script. A user clicks a button, an application calls a known endpoint, a service account holds a credential that rarely changes. The access pattern is knowable at deploy time. Agents do not work that way. They reason about a task, choose tools at runtime, chain calls across services, and adapt based on what each step returns. Three assumptions that hold for traditional software break with agents: 1. **Access patterns can be assigned ahead of time.** An agent reasoning about a task chooses its tools at runtime. You cannot enumerate those choices in a config file before deployment. 2. **A human can approve every meaningful action.** At agent speed, they cannot. 3. **Long-lived credentials are safe enough.** An agent holding broad, static secrets is a blast radius waiting to happen, especially one that chains calls across services. Descope issues short-lived, scope-limited credentials at the moment an agent needs them, evaluates policy against runtime context, and produces an audit trail that connects every action back to the originating user. ## Resources and Connections Two nouns anchor everything in the Hub, and they are easy to mix up: - A **[Resource](/resources)** is what an agent connects *to*: an API or MCP server you protect with Descope, identified by an **audience** (its URL) and a catalog of scopes. When an agent asks Descope for a token, it names that audience: a standardized way of saying "this is what I am trying to access." Every access token, and every ID-JAG, is issued *for* a Resource. - A **[Connection](/agentic-identity-hub/core-components/connections)** is where Descope stores the tokens and API keys an agent *uses* on a user's or tenant's behalf, such as a Google or Slack OAuth token a tool needs downstream. A Connection is a credential vault, not a target an agent authenticates into. ### The working model Define a Resource for each thing an agent should be able to reach. That can be one Resource per internal service, or a single Resource for a **gateway** you run that fronts many downstream MCP servers and APIs. Either way, the Resource's audience is what the agent's token is minted for, and Descope is the authorization server that protects it. Keep long-lived credentials off the agent. An agent *can* fetch a Connection token directly, but the recommended pattern puts a Resource, an MCP server or gateway, **between the agent and the Connections vault**. The agent authenticates to the Resource and gets a short-lived, scoped token; the Resource pulls the API key or external OAuth token from Connections at runtime and calls the downstream service. The agent never holds the third-party secret, which is often long-lived. [Policies](/agentic-identity-hub/policies) govern access to both Resources and Connections. ## Where Descope Fits Descope serves three jobs in the Agentic Identity Hub. Each is covered in full on [Use cases](/agentic-identity-hub/use-cases). - **[Building an MCP server](/agentic-identity-hub/use-cases#building-an-mcp-server):** Descope is the OAuth 2.1 authorization server for your MCP server ([MCP Auth](/mcp)) — consent, agentic identities, Connections, Bring Your Own Auth, and more. - **[Building agents](/agentic-identity-hub/use-cases#building-agents):** Use the [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk) and [Clients](/agentic-identity-hub/core-components/clients), with [Resources](/resources) to protect what you own and [Connections](/agentic-identity-hub/core-components/connections) as a token vault for third-party services. - **[Governing agents internally](/agentic-identity-hub/use-cases#governing-agents-internally):** Use [XAA or a gateway](/agentic-identity-hub/enterprise-managed-authorization) so pre-built clients in your enterprise reach third-party tools under one IdP — choice depends on features you need and XAA support. All three run on the same Descope project, with the same policy engine, user records, and audit log. ## How Agents Get Access Every time an agent uses a tool, Descope does four things: 1. **Identify:** Descope recognizes the agent from its registered [client](/agentic-identity-hub/core-components/clients) record, a [cloud workload OIDC token](/agentic-identity-hub/core-components/clients/workloads) (AWS or GCP), or credentials issued at runtime via [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) or [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd). 2. **Authorize:** Policies enforce what the agent can access. For `client_credentials` flows, policies control which scopes the client is permitted to request at the token endpoint. For `authorization_code` flows, policies control which scopes a user can consent to in the consent screen. Either way, the agent receives only what policy allows. 3. **Protect:** Resources validate Descope-issued tokens natively using your [Descope project JWKs](/management/project-settings#signing-keys). No middleware required. 4. **Broker:** When a tool needs a different credential (a narrower scope, a different audience, or a third-party API key), Descope exchanges the base token at the [token endpoint](/api/third-party-apps/token-endpoint) (RFC 8693) on demand. No re-authentication, nothing stored on the agent. This loop runs for every agent in your system, whether it acts on behalf of a user, runs autonomously, or authenticates via a cloud workload identity provider. For which of the three jobs fits your project, see [Use cases](/agentic-identity-hub/use-cases). For credential issuance patterns, integration architectures, and the full lifecycle map, see [How the Agentic Identity Hub works](/agentic-identity-hub/auth-patterns). All three patterns share one foundation in your Descope project: the same user records, policy engine, connections vault, and audit log. A user who authorized an MCP client is the same subject a policy evaluates when an internal agent acts on their behalf; nothing is configured twice. ## Core Components } title="Use Cases" href="/agentic-identity-hub/use-cases" description="Three jobs: build MCP servers with MCP Auth, build agents with the Agent Auth SDK, or govern agents internally with XAA or a gateway." /> } title="Agentic Identity" href="/agentic-identity-hub/core-components/agents" description="View and manage agentic identities: the authorization records created when users consent, tenants are granted access, or autonomous clients are registered." /> } title="Clients" href="/agentic-identity-hub/core-components/clients" description="Register and manage OAuth clients. Supports pre-registration, DCR, CIMD, and JWT Bearer for cloud workloads." /> } title="MCP Servers" href="/agentic-identity-hub/core-components/mcp-servers" description="Protect MCP servers with OAuth 2.1 auth, per-tool scopes, and tenant isolation." /> } title="Resources" href="/resources" description="Define the APIs and MCP servers agents can reach, and the scopes that gate each action. Tokens are issued for a Resource." /> } title="Connections" href="/agentic-identity-hub/core-components/connections" description="Vault OAuth tokens and API keys for downstream services. Agents fetch credentials at the moment of use." /> } title="Policies" href="/agentic-identity-hub/policies" description="Write authorization rules evaluated at token issuance and exchange." /> } title="Enterprise-Managed Authorization" href="/agentic-identity-hub/enterprise-managed-authorization" description="Connect pre-built MCP clients to third-party tools with XAA or a gateway, or accept customers' XAA tokens for MCP servers you host." /> # Agent Authorization (/agentic-identity-hub/policies) How policies govern agent access to Resources and Connections, with recommended patterns for MCP servers, backend APIs, and third-party credential brokering. # Agent Authorization **Policies** are allow rules that control who can access which [Resources](/resources) and [Connections](/agentic-identity-hub/core-components/connections), and with which scopes. See our [Policies doc](/policies) for the full guide: subjects, targets, grant types, and how to author rules in the Console. For agents specifically, policies answer two questions at runtime: - **Resource targets**: Which scopes can this agent receive on a backend API or MCP server access token? - **Connection targets**: Can this agent retrieve a third-party Connection token from the Vault, and with which scopes? Policies cannot be managed with Terraform or via APIs at this time. ## How Policies Are Enforced for Agents See [How Policies Are Enforced](/policies#how-policies-are-enforced) for grant-type behavior across all applications. For agents, evaluation differs by target: ### Resource Targets The access token a policy governs here is the one your MCP server or backend API validates at runtime. **[User access](/identity-federation/inbound-apps/using-inbound-apps#authorization-code-flow)** (authorization code): the flow most user-delegated agents use: 1. **Scope request**: The client calls `/authorize` with requested scopes. Descope confirms the [client configuration](/agentic-identity-hub/core-components/mcp-servers#client-details) allows those scopes. 2. **Policy filtering**: Descope evaluates matching policies (`user.roles`, `user.tenantIds`, `client.tags`, JWT claims, and other [condition keys](/policies#custom-conditions)). Scopes that no policy permits are removed and **do not appear on the consent screen**. 3. **User consent**: The user authenticates and approves the remaining scopes. The user can only consent within what an admin has allowed via policy. 4. **Token issuance**: Descope issues an access token carrying the intersection of client-allowed scopes, policy-permitted scopes, and user-consented scopes. 5. **Runtime enforcement**: The Resource validates the token on each request. An MCP server does this [when tools are invoked](/agentic-identity-hub/core-components/mcp-servers#token-validation--tool-execution). If a client needs a scope that no policy permits, it is never offered for consent and never appears in the token. A Descope admin must update the policy to grant it; alternatively, the client can reconnect with a reduced scope request (fewer scopes on the `/authorize` call, or a different tool configuration on the agent side). **[Delegated access](/identity-federation/inbound-apps/authorization-server#token-endpoint)** (token exchange) and **[M2M](/identity-federation/inbound-apps/using-inbound-apps#client-credentials-flow)** (`client_credentials`) skip consent. Descope evaluates the policy during the token request and returns only the scopes the policy permits. ### Connection Targets Connection policies are enforced when a Descope JWT is exchanged for a Connection token from the Vault. This is always a token exchange, with no consent screen: 1. **Token exchange request**: An agent presents a Descope access token and requests a Connection token. 2. **Policy evaluation**: Descope checks whether the subject may retrieve a token for the requested Connection and which scopes it may carry. 3. **Connection token issuance**: If a matching policy permits the exchange, Descope returns a scoped Connection token. Otherwise the exchange is denied. 4. **Runtime enforcement**: The downstream service enforces scopes when the agent calls its API. ## Recommended Policy Patterns IaC managed policies are coming soon. Common agent patterns use `user.roles` and `user.tenantIds` from SSO group mapping - see [SSO User and Group Mapping](/sso/sso-mapping) for setup details. ### Resource Targets #### Internal MCP: Corporate Tenant Isolation Restricts access to users in finance and operations tenants using internal agents: ``` Rule name: Internal MCP Corporate Tenants Only Conditions: user.tenantIds IN ["corp_finance", "corp_ops"] client.tags CONTAINS "internal-agent" Allowed Scopes: * mcp:invoice.create * mcp:calendar.read ``` #### External MCP: Read-Only Access Read-only calendar access for users with the `calendar_editor` role: ``` Rule name: External MCP - Read Only Default Conditions: user.roles CONTAINS "calendar_editor" Allowed Scopes: * mcp:calendar.read ``` #### Access Only if Using Verified Agentic Identity Verified agents receive read/write calendar and invoice scopes: ``` Rule name: Verified Agent MCP Access Conditions: client.tags CONTAINS "verified-agent" Allowed Scopes: * mcp:calendar.read * mcp:calendar.write * mcp:invoice.create ``` #### Backend API: Read-Only for Delegated Agents Agents acting on a user's behalf via token exchange receive read-only Orders API access: ``` Rule name: Orders API - Delegated Read Only Conditions: client.tags CONTAINS "verified-agent" Allowed Resource: orders-api Allowed Scopes: * orders.read Grant types: Delegated access (token exchange) ``` #### Tenant-Scoped Scheduler (SSO groups → roles) Users mapped from an IdP "Schedulers" group to a Descope role, scoped to specific tenants: ``` Rule name: Scheduler Access to Calendar MCP Conditions: user.roles CONTAINS "Scheduler" user.tenantIds IN ["tenant-a", "tenant-b"] Allowed Scopes: * mcp:calendar.write * mcp:calendar.readonly ``` ![Tenant-scoped scheduler policy](/assets/tenant-scoped-scheduler-policy.webp) ### Connection Targets #### Read-Only Calendar Access for Standard Users ``` Rule name: Calendar Read-Only - Standard Users Conditions: user.roles CONTAINS "user" Allowed Connections: * google-calendar Allowed Scopes: * https://www.googleapis.com/auth/calendar.readonly ``` #### Read-Write Calendar Access for Schedulers ``` Rule name: Calendar Read-Write - Schedulers Conditions: user.roles CONTAINS "scheduler" Allowed Connections: * google-calendar Allowed Scopes: * https://www.googleapis.com/auth/calendar ``` #### Tenant-Scoped Slack Access ``` Rule name: Slack Access - Corporate Tenants Conditions: user.tenantIds IN ["corp_finance", "corp_ops"] Allowed Connections: * slack Allowed Scopes: * chat:write * channels:read ``` #### Verified Agent Access to GitHub ``` Rule name: GitHub Access - Verified Agents Only Conditions: client.tags CONTAINS "verified-agent" Allowed Connections: * github Allowed Scopes: * repo * read:org ``` # Use Cases (/agentic-identity-hub/use-cases) Choose MCP Auth, the Agent Auth SDK, or XAA and gateways based on what you are building. # Use Cases The Agentic Identity Hub covers three common jobs. Pick the one that matches what you are building; many projects use more than one. | You are… | Start here | | --- | --- | | **[Building an MCP server](#building-an-mcp-server)** | [MCP Auth](/mcp) — consent, OAuth 2.1, agentic identities, Connections, Bring Your Own Auth, and more | | **[Building agents](#building-agents)** | [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk) and [Clients](/agentic-identity-hub/core-components/clients), plus [Resources](/resources) and/or [Connections](/agentic-identity-hub/core-components/connections) | | **[Governing agents internally](#governing-agents-internally)** | [Enterprise-Managed Authorization](/agentic-identity-hub/enterprise-managed-authorization) — XAA or a gateway, based on features and protocol support | ## Building an MCP Server You are exposing tools over MCP and need authentication and authorization in front of them. Use **[MCP Auth](/mcp)** (Descope as the OAuth 2.1 authorization server for your MCP server). That gives you a full product surface, including: - **Consent management** — users approve what an MCP client may do, with [scopes](/mcp#scoping-with-mcp-servers) and [consent flows](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) - **[Bring Your Own Auth](/mcp/bring-your-own-auth)** — keep your existing identity system and still protect the MCP server with Descope - **First-class [agentic identities](/agentic-identity-hub/core-components/agents)** with **OAuth 2.1** — clients register via [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) or [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr), and every agent that connects is a managed identity - **[Connections](/agentic-identity-hub/core-components/connections)** — vault third-party service tokens and API keys your tools need downstream - **[Policies](/agentic-identity-hub/policies)**, tenant isolation, and the rest of the Hub shared with agents you run yourself Start with the [MCP overview](/mcp) and the [MCP server guide](/mcp/mcp-server). If other enterprises will connect *their* agents to *your* server using *their* workforce IdP, also see [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). ## Building Agents You are writing the agent (or agent runtime) yourself. You will typically use: - The **[Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk)** — sign the agent in to Descope and obtain the credentials its tools need - **[Clients](/agentic-identity-hub/core-components/clients)** — register the agent as an OAuth client (pre-registration, workload identity support, and related OAuth functionality) Then choose how the agent reaches what it calls: | Target | What to use | | --- | --- | | **APIs or MCP servers you protect with Descope** | **[Resources](/resources)** — Descope mints short-lived tokens for that audience; your Resource validates them | | **Third-party services you do not protect** | **[Connections](/agentic-identity-hub/core-components/connections)** — vault OAuth tokens and API keys; fetch them at runtime instead of embedding secrets in the agent | You do not need Enterprise-Managed Authorization (XAA / gateway) for this path unless you also want the same governance story you use for pre-built clients. For how tokens move between Resources and Connections, see [Auth patterns](/agentic-identity-hub/auth-patterns). ## Governing Agents Internally You need to decide which agents inside your company may reach which third-party MCP servers and APIs — especially when the **client** is something you do not control (Claude Code, VS Code, Cursor) and the **tool** is something you do not protect with Descope (HubSpot, Asana, and so on). That is [Enterprise-Managed Authorization](/agentic-identity-hub/enterprise-managed-authorization). There are two main ways to connect, and the choice depends on **what features you need** and **whether the client and third-party resource support XAA**: | Approach | When it fits | | --- | --- | | **[Cross App Access (XAA)](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-xaa)** | The client and the third-party resource support XAA / ID-JAG. Descope mints a short-lived assertion; nothing sits between the agent and the tool. | | **[Gateway](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-a-gateway)** | The target does not support XAA, **or** you need gateway-only capabilities (prompt-injection detection, central inspection of tool calls, unified audit, tenant-scoped credential routing, and so on). | Many enterprises use both. Full setup: [Manage agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags). Protocol detail: [How XAA and ID-JAG work](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works). XAA is also useful when a client like Claude talks to an MCP server **you** built: Descope can be both the **issuer** (for the client) and the **validator** (for your MCP Resource). See [Manage agents](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags) together with [MCP Auth](/mcp/mcp-server). # Management Overview (/management) Learn how to manage users, tenants, and access keys, and configure authorization and session settings. # Descope Management The Descope service enables granular configuration and management of their Descope instance. Within the Descope UI, you will see the manage section on the left-hand side. This area within the UI allows you to manage your users, access keys, tenants, and authorization and also contains your project's audit trail. You can learn more about the various customizations and management under the following articles. - [User Management](/management/user-management) - [M2M Access Key Management](/management/m2m-access-keys) - [Tenant Management](/management/tenant-management) - [Authorization](/authorization) - [Sessions](/sessions) ## Management Keys Descope allows you to manage your instance through the Descope SDK utilizing a management key. You can create, edit, and delete management keys within the [Management Keys](https://app.descope.com/settings/company/managementkeys) page within the Descope UI. When creating a management key, you will provide a name and expiration. Management keys can also be associated with specific projects within your company. The associated projects are configured during the creation of management keys and cannot be changed later. The options for expiring management keys are 30 days, 60 days, 90 days, or never. You will receive the key in clear text when creating a management key; ensure you safely store it, as you will not be able to view it again once. Once you have created a management key, you can utilize the management key and project id to use the management SDK or the management [API](/api/management/users). ### Management Key Lifecycle Management keys will continue to function as long as they are active and not expired. Once the management key is expired or deactivated, it will no longer be usable. Within the UI, you can deactivate (revoke) management keys; however, the management key will remain in the Descope project and may be reactivated if you choose to reactivate them. You can also delete management keys. Once a management key is deleted, it will no longer be usable. Deleting access keys will remove the access key's details from the Descope project. ### Adding Permitted IPs to Management Keys Management Keys in Descope supports an attribute for CIDR restrictions. Descopers can add IPs in the permitted IPs field by which they can restrict access to their Management service to just specific devices having specific originating IP addresses. The permitted IPs entered will be associated with that specific management key. This way, users have the ability to control restrictions on a per management key basis. ![permitted ips for managementkeys](/assets/permitted-ips-managementkeys.webp) ### Management Key Roles This section defines what roles the management key has. You can choose whether these roles are defined on a company level, for specific projects or for Descopers for SCIM only usage. ![roles for managementkeys 1](/assets/managementkey-scopes.webp) The roles available for Company level access are listed as follows: | Roles | Machine Name | Description | | ------- | -------- | -------- | |Full Access | `company-full-access` | Full read and write access to all projects in region | |User Testing| `company-user-testing` | Read and write access to testing APIs only| |Asset Management - Read Only| `company-asset-mgmt-read` | Read access to Users, Access Keys and Tenants in all projects in region| |Asset Management - Read & Write| `company-asset-mgmt-read-write` | Full read and write access to Users, Access Keys and Tenants in all projects in region| |Audit Handling| `company-audit` | Read and write access to audit related APIs| |Authentication| `company-authentication` | Full access to authentication methods related APIs| |FGA - Read Only| `company-fga-read` | Read access to FGA/Authz related APIs| |FGA - Read & Write| `company-fga-read-write` | Full read and write access to FGA/Authz related APIs| |Infra Management - Read Only| `company-infra-read` | Read access to general project resources such as AuthZ, Project settings, in all projects in region| |Infra Management - Read & Write| `company-infra-read-write` | Full read and write access to general project resources such as AuthZ, Project settings, in all projects in region| |Management Keys - Read & Write| `company-mgmt-keys-read-write` | Full read and write access to management key management APIs| |Descopers - Read & Write| `company-descopers-read-write` | Full read and write access to Descoper management APIs| The roles available for Project level access are listed as follows: | Roles | Machine Name | Description | | ------- | -------- | -------- | |Full Access| `project-full-access` | Full read and write access to the project| |User Testing| `project-user-testing` | Read and write access to testing APIs only| |Asset Management - Read Only| `project-asset-mgmt-read` | Read access to Users, Access Keys and Tenants in project| |Asset Management - Read & Write| `project-asset-mgmt-read-write` | Full read and write access to Users, Access Keys and Tenants in project| |Audit Handling| `project-audit` | Read and write access to audit related APIs| |Authentication| `project-authentication` | Full access to authentication methods related APIs| |FGA - Read Only| `project-fga-read` | Read access to FGA/Authz related APIs| |FGA - Read & Write| `project-fga-read-write` | Full read and write access to FGA/Authz related APIs| |Infra Management - Read Only| `project-infra-read` | Read access to general project resources such as AuthZ, Project settings, in project| |Infra Management - Read & Write| `project-infra-read-write` | Full read and write access to general project resources such as AuthZ, Project settings, in project| ![roles for managementkeys 2](/assets/roles-managementkeys.webp) These roles for Project level access can also be applied to your project tags upon generating a management key. Project tags, used for categorizing your projects (for example, a `staging` or `prod` tag), can be used to apply roles in your management key across all projects with the associated tag. ![roles for tags](/assets/management-tags-mapping.webp) When using roles programmatically (via API, SDK, or Terraform) for tag-level access, use the same role names as project-level but with the `tag-` prefix instead of `project-`. For example, `project-full-access` becomes `tag-full-access`, and `project-asset-mgmt-read` becomes `tag-asset-mgmt-read`. **Descoper Level access (SCIM):** This level of access is when management key is used to perform SCIM related operations to control Descopers in your company. This scope is different from the above mentioned company/project level access as its referencing Descopers access level only with respect to SCIM versus referencing users on your company/project. # Localization (/management/localization) Learn how to customize localization and translation within Descope flows, widgets, and messaging templates. # Localization When building an application for use across many countries, it's essential to translate and localize your application for each user's location. Descope simplifies translating text within your flows, widgets, and messaging templates by offering translation via connectors or manual overrides. When using a connector to translate a Descope Flow, Widget, or messaging template, you can override the translation response from the third-party service to meet your needs. This guide provides an overview of localization configuration. For detailed connector setup guides, see our [Localization Connectors](/connectors/connector-configuration-guides/localization) page. You can also configure localization programmatically through the Descope Client SDKs by passing the `locale` parameter to the [Descope component](/client-sdk/descope-components#descope-flow-component), giving you manual control over which language or locale is displayed for your flow. ## Configuration Overview To configure the localization of a Descope flow, widget, or messaging template, navigate to the [Localization](https://app.descope.com/localization) section within the Descope console. Once here, you can select the applicable flow, widget, or messaging template you want to localize from the dropdown, then click `Configure Localization`. ![Start localization of a flow within Descope](/assets/localization-start.webp) Once you have clicked `Configure Localization`, you will be prompted to select the connector, source language, and target languages. ![Localization modal within Descope for initializing of translation of a flow](/assets/localization-modal.webp) When you select the dropdown for `Connector`, you can choose manual or use one of our [Localization connectors](/connectors/connector-configuration-guides/localization). ![Selecting a configured connector for localization of a flow in Descope](/assets/localization-connector.webp) After selecting `Done`, you will see a page similar to the one below. You can change the settings from the previous modal by clicking the settings icon at the top right. You can also import and export the translations via the arrows icon. You can also select the checkbox to `View overrides only` to show only the overridden items on the right-hand side for the selected language. ![Selecting a configured connector for localization of a flow in Descope](/assets/localization-flow.webp) ## Widget Localization A [widget](/widgets) is translated as a single unit: configuring localization for a widget also translates its main screen and any flow it triggers. Flows owned by a widget are configured from the widget itself, so they don't appear as separate entries in the flow list. ![Widget flow list](/assets/widget-behavior-customization.webp) ## Manual Localization After selecting the manual option for localization, you will see a screen like the example below. You can then manually override the translations per language. ![An example screen for manual localization of a flow within Descope](/assets/localization-manual.webp) ## Connector Based Localization When configuring localization to utilize a [localization connector](/connectors/connector-configuration-guides/localization), the localization will automatically be generated for the flow, widget, or messaging template based on the returned translation. Below you can see an example configuration and the generated translation. ![An example configuration of localization with a connector within Descope](/assets/localization-connector-2.webp) ![An example output of connector based localization of a flow within Descope](/assets/localization-connector-flow.webp) ### Manually Overriding Connector Translations When you configure localization with a connector, you can manually override the translation. When you override the translation, you will see the override highlighted like the below example. ![An example of manually overriding connector based localization of a flow within Descope](/assets/localization-connector-override.webp) ## Right-to-Left Email Templates When you localize an email messaging template into a right-to-left language such as Hebrew, Arabic, or Farsi, Descope sets the direction and alignment of the translated HTML body for you, with no extra configuration. Descope compares the `Source Language` and `Target Language` you configured: | Source and target | Result | |-|-| | Left-to-right source, right-to-left target | The body renders right-to-left, and `left` alignment becomes `right`. | | Right-to-left source, left-to-right target | The body renders left-to-right, and `right` alignment becomes `left`. | | Both the same direction | The original direction and alignment carry over unchanged. | Centered content stays centered. Descope never modifies your source template, only the generated translations. ## Global Strings Global Strings provide a centralized way to define reusable text values that can be referenced across flows, screens, and messaging templates. They are especially useful for maintaining consistent copy, supporting localization, and avoiding duplication of text throughout your authentication experiences. You can define global strings in the dedicated [Global Strings](https://app.descope.com/localization/globals) section of the Descope console. You define a unique key and associate it with one or more string values, each mapped to a specific language. At runtime, the system resolves references to these keys and renders the appropriate string based on the active language context. ![An example of defining a global string](/assets/global-strings.webp) You can reference global strings in your flows, screens, and messaging templates using the `{{strings.}}` syntax. For example, if you have a global string key called `greeting` with the value "Welcome!", you can reference it as `{{strings.greeting}}`. Use the three-dot menu next to `Add Key` to manage language settings and import or export global strings. `Export globals` downloads a JSON snapshot of your current global string configuration, and `Import globals` allows you to import a global strings JSON. Importing will override existing global strings. ![Settings and importing/exporting global strings](/assets/global-strings-settings.webp) ## Localizing Errors When it comes to translating errors, there are a few areas where you need to accomplish this. ### Flow Components Some Descope components have errors that are managed in the flow itself and can be localized within the localization section of the Descope console. Examples of these components would be the missing value messages or customized [Validation Error Messages](/flows/screens/inputs#pattern-validation-error-message). ![Localization of validation error message example within Descope Localization](/assets/localization-error.webp) ### System Flow Errors Descope system-level flow errors, such as OTP verification failures, must be translated using the [Descope error transformer](/handling-flow-errors/customizing-flow-errors) within the frontend SDK. Below is an example of configuring the error transformer within the frontend SDK to localize system-level flow errors. ```js function errorTransformer(error) { const language = getClientLanguage(); // Function to get the client's language, e.g., 'en', 'es', 'de' const translationMap = { en: { OTPVerifyCodePhoneFailed: "Failed to verify OTP code", }, es: { OTPVerifyCodePhoneFailed: "No se pudo verificar el código OTP", }, de: { OTPVerifyCodePhoneFailed: "Der OTP-Code konnte nicht überprüft werden", }, // Add more languages and translations as needed }; const translations = translationMap[language] || translationMap['en']; // Default to 'en' if language not found return translations[error.type] || translations[error.text] || error.text; } function getClientLanguage() { // Logic to determine the client's language, e.g., from browser settings or user profile // For demonstration purposes, return 'en' (English) by default return navigator.language.split('-')[0] || 'en'; } ``` ### Localizing Custom Error Messages in Flows Custom error messages set on flow conditions and action error handling can now be localized, including messages that reference global strings (`{{strings.KEY}}`). Previously, only screen content was localized. Custom errors defined at the flow logic level were not included. This does not apply automatically to existing flows. Detection runs on every flow save and reflects the flow's state as of its most recent save, it is not a one-time flag. If a flow was created before this feature shipped, or if a new custom error is added to a flow that was already saved after the feature shipped, the flow needs to be saved again for that error to be picked up. There is no bulk migration, each flow's custom errors are localized as of its last save. #### What gets localized - Condition error messages - Action error handling error messages #### Where these appear Localized keys for these error messages appear in **Localization > Flows and Widgets**, in the Source and target language JSON editors, using this format: ``` dsl.{taskId}.condition.{index}.error-message dsl.{taskId}.error-handling.{errorType}.error-message ``` `{taskId}` is an internal task identifier and may itself contain dots, for example a task that is the end-step of an action group can have an ID like `4.end`, producing a key such as `dsl.4.end.error-handling.UserDoesNotExists.error-message`. Treat `{taskId}` as an opaque value rather than a fixed set of segments. ![DSL localization keys shown alongside screen content keys](/assets/localization-error-dsl.webp) These keys appear as flat entries alongside standard screen content keys (like `GiTV.2-ym.button`), with no grouping, section header, or label distinguishing them. This is expected. Recognize the `dsl.*` pattern rather than treating it as a display issue. #### Resolution conditions For a custom error message to be localized, two separate things need to be true: **The translation data needs to exist.** This requires the flow to have been saved since this feature shipped (see the note above), which generates the localization keys shown in the Source panel. A manual override can also be added directly for a specific key without waiting for a save, but this is not the standard workflow. **At request time, the request needs to qualify for localization.** Specifically: - A locale is passed on the request - That locale is one of the flow's configured target languages If either of these is not satisfied, the original (source language) message is used instead. #### Editing and overriding translations Any `dsl.*` key can be manually edited or overridden in the Source and target language JSON editors, using the same mechanism as screen content. Manual overrides work even before an auto-generated translation exists for that language, for example before a translation connector runs, or if no connector is configured at all. #### Global strings If a custom error message references a global string (`{{strings.KEY}}`), that reference resolves per language using the flow's configured global strings, the same as it does in screen content. #### Out of scope Error sources other than condition errors and action error handling errors are not covered by this feature. ## Language support Descope includes built-in support for the following languages. If the language or locale you need isn't listed here, you can define one yourself as a [custom language](#custom-languages). | | | | |-|-|-| | en: English (en) | ka: Georgian (ka) | ps: Pashto (ps) | | af: Afrikaans (af) | de: German (de) | pl: Polish (pl) | | sq: Albanian (sq) | el: Greek (el) | pt-BR: Portuguese (Brazil) (pt-BR) | | am: Amharic (am) | gu: Gujarati (gu) | pt: Portuguese (pt) | | ar: Arabic (ar) | ht: Haitian Creole (ht) | pt-PT: Portuguese (Portugal) (pt-PT) | | hy: Armenian (hy) | ha: Hausa (ha) | pa: Punjabi (pa) | | az: Azerbaijani (az) | he: Hebrew (he) | pa-Arab: Punjabi (Shahmukhi) (pa-Arab) | | bn: Bengali (bn) | hi: Hindi (hi) | ro: Romanian (ro) | | bs: Bosnian (bs) | hu: Hungarian (hu) | ru: Russian (ru) | | bg: Bulgarian (bg) | is: Icelandic (is) | sr: Serbian (sr) | | ca: Catalan (ca) | id: Indonesian (id) | si: Sinhala (si) | | zh-CN: Chinese (Simplified) (zh-CN) | ga: Irish (ga) | sk: Slovak (sk) | | zh: Chinese (Simplified) (zh) | it-IT: Italian (Italy) (it-IT) | it-CH: Italian (Switzerland) (it-CH) | | zh-TW: Chinese (Traditional) (zh-TW) | ja: Japanese (ja) | sl: Slovenian (sl) | | hr: Croatian (hr) | ja_JP: Japanese (Japan) (ja_JP) | so: Somali (so) | | cs: Czech (cs) | kn: Kannada (kn) | es: Spanish (es) | | da: Danish (da) | kk: Kazakh (kk) | es-MX: Spanish (Mexico) (es-MX) | | fa-AF: Dari (fa-AF) | ko: Korean (ko) | es-US: Spanish (US) (es-US) | | nl: Dutch (nl) | lv: Latvian (lv) | sw: Swahili (sw) | | en-GB: English (UK) (en-GB) | lt: Lithuanian (lt) | sv: Swedish (sv) | | et: Estonian (et) | mk: Macedonian (mk) | ta: Tamil (ta) | | fa: Farsi (Persian) (fa) | ms-Arab: Malay (Jawi) (ms-Arab) | te: Telugu (te) | | fil: Filipino (fil) | ms: Malay (ms) | th: Thai (th) | | tl: Filipino, Tagalog (tl) | ml: Malayalam (ml) | tr: Turkish (tr) | | fi: Finnish (fi) | mt: Maltese (mt) | uk: Ukrainian (uk) | | fr-CA: French (Canada) (fr-CA) | mr: Marathi (mr) | ur: Urdu (ur) | | fr: French (fr) | mn: Mongolian (mn) | uz: Uzbek (uz) | | fr-FR: French (France) (fr-FR) | no: Norwegian (Bokmål) (no) | vi: Vietnamese (vi) | | my: Burmese (my) | chk: Chuukese (chk) | cy: Welsh (cy) | | km: Khmer (km) | om: Oromo (om) | hmn: Hmong (hmn) | | yi: Yiddish (yi) | | | ## Dialect support Descope supports additional localization dialects to enhance your end user's experience further. Supported extended dialects include: - zh-CN: Chinese (Simplified) as used in Mainland China - zh-TW: Chinese (Traditional) as used in Taiwan - en-GB: English as used in the United Kingdom - it-IT: Italian as used in Italy - it-CH: Italian as used in Switzerland - fr-CA: French as used in Canada - fr-FR: French as used in France - pt-BR: Portuguese as used in Brazil - pt-PT: Portuguese as used in Portugal - es-MX: Spanish as used in Mexico - es-US: Spanish as used in the United States - ms-Arab: Malay in Arabic script (Jawi) - pa-Arab: Punjabi in Arabic script (Shahmukhi) - fa-AF: Dari as used in Afghanistan ![Dialect locales within Descope Localization](/assets/localization-dialects.webp) ### Dialects with connectors The AWS Translate and Google Cloud Translate connectors don't support all of the Descope-supported dialects. AWS Translate does not support en-GB. Google Cloud Translate does not support en-GB, fr-CA, or es-MX. ## Custom Languages Descope ships with a long list of built-in languages. However, if the language or locale you need isn't in the [built-in language list](#language-support), you can define your own entry with a display name and a language code of your choosing, then localize your flows, messaging templates, and global strings with your language. Custom languages are scoped to a project. Once you create one, it's available as a target language for other localization items in the same project. A custom language inherits its text direction from its language code. Give it the code of a [right-to-left language](#right-to-left-email-templates) and email templates translated into it render right-to-left. Descope treats any other code as left-to-right. The region has no effect on direction, so a code of `en` with a region of `IL` still renders left-to-right. ### Adding a Custom Language Custom languages can only be added as target languages. Descope does not support custom source languages, so the `Source Language` input always lists built-in languages only. Navigate to [Localization](https://app.descope.com/localization) on the Descope console. Select the flow or messaging template you want to localize, then click `Configure Localization`. Open the `Target Languages` dropdown. The menu is split into a `Custom` group and a `Default` group, with custom languages listed first. Click `+ Custom language` to open the `Add Custom Language` dialog. Fill in the `Name`, `Language Code`, and optional `Region`, then click `Add Language`. ![The grouped Custom and Default target language picker within Descope Localization](/assets/localization-custom-language-picker.webp) The dialog accepts the following values: | Field | Required | Description | |-|-|-| | `Name` | Yes | The display name for the language, shown wherever the language is listed in the console. For example, `Cantonese (Hong Kong)`. | | `Language Code` | Yes | The language code your client sends to request this language. For example, `yue`. Can't be changed after the language is created. | | `Region` | No | An optional region subtag used to distinguish variants. A language code of `yue` with a region of `HK` resolves to `yue-HK`. Can't be changed after the language is created. | ![The Add Custom Language dialog within Descope Localization](/assets/localization-custom-language-dialog.webp) Click `Add Language` to save. The new language appears immediately in the `Custom` group of the `Target Languages` dropdown, where you can select it and begin entering translations. # Messaging Templates (/management/messaging-templates) Learn how to customize and manage email, voice, and SMS templates for authentication flows and user invitations in Descope. # Messaging Templates This guide explains how to customize and manage messaging templates for various authentication methods and user invitations in Descope. To learn how to use messaging templates for authentication within flows, refer to our [Flow Actions Doc](/flows/actions/email-sms-templates-in-flows), and for user invites, refer to our [User Invite Doc](/management/user-management/invite-users). ## Creating Templates ### Authentication Templates To create an authentication template: 1. Go to the settings of each respective [Authentication Method](https://app.descope.com/settings/authentication) in the Console 2. Select your configured messaging connector (Email, SMS, Voice, or Instant Messaging) 3. Click `+ New Template` ![Example of creating a new template for email authentication methods in Descope](/assets/example-email-template.webp) ### User Invitation Templates To create a user invitation template: 1. Go to **Sign Ups and User Invitations** under [Project Settings](https://app.descope.com/settings/project) 2. Select your configured messaging connector (Email or SMS) 3. Click `+ New Template` Invitation templates support their own set of dynamic variables, listed under [Available Dynamic Variables](#available-dynamic-variables) below. ## Template Editor The template editor supports both HTML (for Email) and plain text (for Email, SMS, Voice, or Instant Messaging) formats. They support dynamic content using the syntax `{{}}`. You can also preview the message by clicking the preview tab. ### Using Global Strings in Templates [Global Strings](/management/localization#global-strings) allow you to utilize Descope's localization features, so that the appropriate value is displayed based on the user's language in the messaging template. You can use global string values in your template, with the `{{strings.}}` syntax. ### HTML Format ![Example of a customized template for email authentication methods in Descope](/assets/email-received-descope-template.webp) ### Plain Text Format ![Example of a plain text mode for email template in Descope](/assets/email-plain-text-mode.webp) ## Template Localization If you would like to translate your email or SMS depending on your user's location, you can set up template localization using the Descope console. Refer to the [Localization Configuration Overview](/management/localization#configuration-overview) for more details. Translate an email template into a right-to-left language such as Hebrew or Arabic and Descope renders the HTML body in the correct direction. See [Right-to-Left Email Templates](/management/localization#right-to-left-email-templates). ## Dynamic Content with Template Options Template options allow you to pass dynamic data to your templates. You can set these options through both flows and SDKs. You can access template options in your templates by prefixing the key with `options_`. For example: ![options in messaging template](/assets/template-options.webp) ### With Flows 1. Select the message-sending action 2. Click `+` to add template options 3. Add key-value pairs for dynamic content ![Using Template options within Descope flows](/assets/example-template-options-flow.webp) ### With SDKs You can utilize the `templateOptions` parameter within the SDK authentication functions to set the template options values. The example below is for the the Sign Up Or In via magic link function using the Node SDK. Check out the specific SDK docs for each [authentication method](/auth-methods) for more details on how to use template options. ```javascript title="index.js" const loginId = "email@company.com" const uri = "http://auth.company.com/api/verify_magiclink" const deliveryMethod = "email" const signUpOptions = { "templateOptions": { "deviceOS": "Value", "startHostName": "Value2" } } const resp = await descopeClient.magicLink.signUpOrIn[deliveryMethod]( loginId, uri, signUpOptions ); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ### Available Dynamic Variables Which variables you can use depends on the template type: authentication method templates (OTP, Magic Link, Enchanted Link, etc.) or user invitation templates. The **Available In** column below shows where each one works. | Dynamic Variable | Description | Available In | | ------- | -------- | ------- | | `userName` | The user's name. | Authentication Templates | | `tenantName` | The name of the user's tenant. | Authentication Templates | | `projectName` | The name of your Descope project. | Authentication Templates, Invitation Templates | | `redirectUrl` | The redirect URL. | Authentication Templates | | `expirationTime` | The expiration time of the link or code. | Authentication Templates | | `expirationTimeMinutes` | The expiration time of the link or code, in minutes. | Authentication Templates | | `expirationTimeSeconds` | The expiration time of the link or code, in seconds. | Authentication Templates | | `inviteUrl` | The link the invited user clicks to accept the invitation. | Invitation Templates | | `tenant.name` | The name of the tenant the invited user belongs to. Populates when the user belongs to a single tenant; empty if the user belongs to multiple tenants and none can be resolved. | Invitation Templates | | `user.name` | The invited user's display name. | Invitation Templates | | `user.email` | The invited user's email address. | Invitation Templates | | `user.phone` | The invited user's phone number. | Invitation Templates | | `user.loginIds` | The invited user's login IDs. | Invitation Templates | | `user.customAttributes.` | The value of a [custom attribute](/management/user-management#custom-user-attributes) set for the invited user. | Invitation Templates | | `options_` | The value of a template option passed with the message. | Authentication Templates, Invitation Templates | `tenantName` and `tenant.name` are different variables. Authentication templates use `tenantName`; invitation templates use `tenant.name`. Using the wrong one for a given template type won't resolve. `tenant.name` works directly in invitation templates configured here. # Auth Hosting (/identity-federation/auth-hosting) Configure and use the Descope Auth Hosting App to deliver authentication flows without embedding the Descope SDK in your own application. # Auth Hosting The Auth Hosting App is a hosted React application that runs your Descope Flows. Every Descope project includes it by default. Rather than embedding the Descope SDK in your own frontend, you point your authentication redirects at this hosted page and Descope handles the rest. It covers any situation where Descope acts as an OAuth or SAML provider: [Federated Applications](/identity-federation/applications), [Inbound Apps](/identity-federation/inbound-apps), and [MCP server authorization](/agentic-identity-hub/core-components/mcp-servers). The hosted page at `https:///__ProjectID__` is available to every project out of the box. Rather than embedding authentication UI in your own app, you redirect users to a fully managed, brandable page that Descope hosts for you. If you're developing your own frontend application, you can embed Descope flows directly using our SDK components for a more seamless, native experience. Auth Hosting is the right choice when you don't have a frontend, or prefer not to embed. ## When Auth Hosting Applies Auth Hosting is relevant any time Descope is the identity provider in a redirect-based flow: - **Federated Applications** — when users authenticate via OIDC or SAML and your app redirects to Descope as the IdP - **Inbound Apps** — when an OAuth client initiates an authorization code flow and needs a hosted consent screen - **Agentic Client** — when an Agent or MCP client like Claude or Cursor triggers the authorization flow for a Descope Resource In all three cases, the redirect lands on whichever URL you've configured as the **Flow Hosting URL** for that app or server. ## Configuring Flow Hosting Each [Federated App](/identity-federation/applications), [Inbound App](/identity-federation/inbound-apps), and [Agentic Client](/agentic-identity-hub/core-components/clients) in the Descope Console has a **gear icon** next to its flow selection. ![Flow hosting configuration modal](/assets/auth-hosting-configuration-gear-icon.webp) Clicking it opens the flow hosting configuration modal for that specific app. The modal gives you two options: ### Hosted by Descope Descope serves the flow at `https:///__ProjectID__`. ![Flow hosting configuration modal](/assets/auth-hosting-hosted-by-descope.webp) Select this option and configure the following: | Setting | Description | |---------|-------------| | **Flow** | The Descope Flow to run when a user arrives at the hosted page. | | **Style** | The [flow style](/management/styles) to apply. Each app can use a different style, so you can match branding per application. | | **Theme** | Light, dark, or system — controls the color scheme of the hosted page. | | **Tenant** | A hardcoded tenant slug. Set this when every user arriving through this app should authenticate against a specific tenant — for example, to automatically redirect to that tenant's SSO provider or apply tenant-specific flow logic, without requiring users to type an email first. | | **Debug mode** | Shows the debug overlay during the flow. Useful for testing; turn it off before going to production. | The **Flow Hosting URL** at the top of the modal reflects your current settings as query parameters and updates as you make changes. You can also edit the URL directly — it serves as the source of truth. If you manually modify the URL, the panel settings update to match. ### Self-hosted Select this option if you've embedded a Descope flow component inside your own web application. Provide the URL where the flow lives. ![Flow hosting configuration modal](/assets/auth-hosting-self-hosted.webp) When using OIDC flows, the `redirect_uri` from the initial `/authorize` request is used automatically — you don't need to hard-code it. Use this option when you need to pass custom values into the flow from your application, or when you want full control over the surrounding page (layout, branding, background images). ## More Customization Options The base URL requires your project ID as a path segment: `https:///__ProjectID__`. All query parameters are optional. The console modal manages `flow`, `style`, `theme`, `tenant`, and `debug` for you. Use the URL directly for the parameters it doesn't expose. | Parameter | Default | Description | |-----------|---------|-------------| | `flow` | `sign-up-or-in` | Which flow to run. Example: `?flow=login` | | `tenant` | — | Tenant ID or domain. When set, Descope skips the email input screen and routes directly to that tenant's SSO provider. Accepts a tenant ID (`?tenant=T2UjlUN1tJsRnrV3jnAkJ3WziaEq`) or a domain (`?tenant=descope.com`). | | `style` | Project default | Which [flow style](/management/styles) to apply. Example: `?style=my-brand` | | `theme` | `light` | Color scheme. Options: `light`, `dark`, or `os` to follow the system setting. Example: `?theme=dark` | | `bg` | — | Background color or image. Accepts a CSS color name (`bg=red`), a URL-encoded hex value (`bg=%23ff0000`), or an image URL sized to cover the screen (`bg=https://example.com/bg.jpg`). | | `wide` | — | Set `?wide=true` to widen the flow container. Useful for large forms built with Flow screens. | | `width` | — | Flow container width in pixels or viewport percentage. Values larger than the screen are clamped. Example: `?width=480px` or `?width=50%` | | `height` | — | Flow container height in pixels or viewport percentage. Values larger than the screen are clamped. Example: `?height=600px` or `?height=80%` | | `shadow` | — | Show or hide the drop shadow on the flow container. Set `?shadow=false` when using [Native Flows](/mobile-sdk/native-vs-browser-flows). | | `debug` | — | Set `?debug=true` to show the debug overlay. Not recommended for production. | | `store_last_auth_user` | `true` | Set `?store_last_auth_user=false` to prevent saving the last authenticated user after the flow completes. | | `client.*` | — | Any parameter prefixed with `client.` is forwarded to the Descope component as its `client` prop. Example: `?client.k1=v1&client.k2=v2` passes `{ k1: 'v1', k2: 'v2' }` to the component. | ### Favicon Favicon configuration applies to the Default OIDC Federated Application only. Custom [Federated Apps](/identity-federation/applications) and [Inbound Apps](/identity-federation/inbound-apps) do not currently support favicon overrides via Auth Hosting. For the default federated application, you can replace the Descope favicon with your own branding. Open the default OIDC Federated Application [settings](https://app.descope.com/applications/descope-default-oidc) in the Descope Console, click the pencil icon on the favicon, and upload your image. The file must be under 1 MB. ### Background and Styling #### Background Color or Image Add the `bg` parameter to your Flow Hosting URL to set a background behind the flow container: - **Color name**: `?bg=navy` - **Hex color** (URL-encoded): `?bg=%23ff6600` - **Image URL**: `?bg=https://example.com/background.jpg` Image backgrounds are sized to cover the full screen. You can append `bg` directly to the Flow Hosting URL in the console modal, since the modal doesn't expose it as a dedicated field. For a fully custom background — CSS gradients, video, or precise positioning — [self-host the app](#self-hosting-the-auth-hosting-app) and modify the layout directly. #### Flow Style and Theme The console modal exposes **Style** and **Theme** settings per app. Use **Style** to apply a specific set of visual tokens (colors, fonts, border radius) from your [Styles configuration](/management/styles). Each app can carry a different style, which lets you match branding per application or per tenant. Use **Theme** to control whether those tokens render in light, dark, or system mode (which follows the user's OS preference). Both settings update the Flow Hosting URL automatically. You can also set them via the `style` and `theme` query parameters if you're constructing the URL manually. For deeper customization beyond what styles support, [self-host the app](#self-hosting-the-auth-hosting-app). ## Custom domain If you've configured a [custom domain](/how-to-deploy-to-production/custom-domain) (available on Pro plans and above), the Auth Hosting App is automatically served from that domain. Your Flow Hosting URL updates to reflect it — no separate setup required. ## Self-hosting the Auth Hosting App For changes that go beyond what query parameters support — custom JavaScript, custom layout, or passing [flow inputs](/flows/dynamic-keys/flow-inputs) from your frontend — you can self-host the app entirely. The [auth-hosting repository](https://github.com/descope/auth-hosting) includes a one-click Vercel deploy button. You can also clone it and deploy to AWS, Netlify, or DigitalOcean. Once deployed, replace the Flow Hosting URL in your app configuration with your own deployment's URL. # Applications (/identity-federation) Learn about Descope's three types of applications and how they enable different identity federation scenarios # Applications Descope provides applications to handle various identity federation scenarios. Understanding the different types of applications and their use cases is essential for implementing the right authentication strategy for your needs. ## [Federated Apps](/identity-federation/applications) Federated Applications in Descope enable you to establish secure Single Sign-On (SSO) connections between your applications and Descope, which acts as the Identity Provider (IdP). This allows users to authenticate once with Descope and gain access to multiple connected applications without needing to log in separately to each one. ### Key characteristics - Single authentication point for multiple applications - Centralized user management - Seamless user experience across applications ### When to use Federated Apps Federated apps are essential when you want to: - Manage federated login across multiple different applications and domains - Provide seamless access across different applications - Centralize user management and access control For example, if your organization has multiple applications (like an internal portal, customer dashboard, and admin panel), you can configure them as federated apps. Once a user logs in to any of these applications through Descope, they'll have seamless access to all other connected applications. ## [Inbound Apps](/identity-federation/inbound-apps) Inbound Apps enable your application to act as an OAuth provider, allowing third-party applications to authenticate and access your resources securely. This lets you manage user consent, permissions, and API access while maintaining control over your authentication system. Descope exposes standard OAuth routes under `/oauth2/v1/apps/`, including [`/authorize`](/api/third-party-apps/authorization-get) and [`/token`](/api/third-party-apps/token-endpoint). See [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server) and the [API reference](/api/third-party-apps). ### Key characteristics - Your application becomes the OAuth provider - Fine-grained control over API permissions through OAuth scopes - Centralized consent and permission management - Support for both user-based and machine-to-machine (M2M) authentication ### When to use Inbound Apps Inbound apps are essential when you want to: - Allow third-party applications to integrate with your platform securely - Provide API access to external services while maintaining control over permissions - Support automated workflows and AI agents that need secure access - Build a marketplace or platform where partners can integrate their services - Support M2M integrations with secure token-based authentication ## [Outbound Apps](/identity-federation/outbound-apps) Outbound Apps let you securely connect your users to third-party providers, without relying on those providers as primary authentication methods. Think of them as a token vault, or an extension of [OAuth social login](/auth-methods/oauth), where you can define default scopes, progressively request new scopes, and rely on Descope to automatically manage and refresh access tokens on your behalf. ### Key characteristics - Connect to third-party providers for additional permissions - Manage OAuth tokens and refresh cycles automatically - Control default scopes and request additional scopes if needed - Works with MCP and AI-related tools for token management for external API connections ### When to use Outbound Apps Outbound apps are essential when you want to: - Manage permissions for AI tools with external APIs - Manage multiple OAuth tokens for users/tenants with the right scopes ## [Resources](/resources) **Resources** are OAuth **resource servers** you define in the Descope Console. There are two types: **API** Resources (scopes mapped to [RBAC roles](/authorization/role-based-access-control)) and **MCP Server** Resources (MCP scopes mapped to [Connection](/agentic-identity-hub/core-components/connections) scopes when tools use external APIs). MCP Server Resources also appear under [Agentic Identity Hub → MCP Servers](/agentic-identity-hub/core-components/mcp-servers). ### Key characteristics - Create **API** or **MCP Server** Resources from one console area - API Resources: OAuth scopes ↔ [Descope roles](/authorization/role-based-access-control) - MCP Server Resources: OAuth scopes ↔ Connection scopes; same server in Agentic Identity Hub - Resource servers validate Descope-issued JWTs (`aud`, `scope`, project JWKs) ### When to use Resources Define Resources when you want to: - Register APIs or MCP endpoints as OAuth-protected servers - Control API access with scopes and internal roles, or MCP tool access with Connection mapping - Issue tokens that clients can use directly with servers that you use Descope to protect Pair Resources with **[Inbound Apps](/identity-federation/inbound-apps)** (API OAuth clients) or **[Agentic clients](/agentic-identity-hub/core-components/clients)** (MCP), not [Federated Apps](/identity-federation/applications). See [Managing Resources](/resources/managing-resources). Resources differ from **Outbound Apps** (vaulted third-party credentials). # REST API Reference (/api) Use the Descope REST API to build authentication and user management for your app while retaining full control over your UI. # REST API Reference The Descope REST API lets you implement every authentication method, every management function, and more directly in your own application. Endpoints are grouped by feature under the navigation on the left. This page highlights the conventions that apply to all APIs. ## OpenAPI Specification You can download the full OpenAPI file and import it into Postman or any other API client: - [Descope API YAML](/examples/Descope_API.yaml) ## Making Requests - All requests are sent over HTTPS to Descope's API endpoints - Request and response bodies use JSON unless stated otherwise - Standard HTTP status codes indicate success or errors Feel free to copy the `curl` examples provided under each endpoint section or use the embedded request runner on each endpoint page. ## Authentication Most endpoints require an `Authorization` header with a bearer token: - **Sign-up / Sign-in endpoints:** use your **Project ID** as the bearer token (`Authorization: Bearer __ProjectID__`) - **User-scoped endpoints:** use the format `Authorization: Bearer __ProjectID__:` - **Management endpoints:** use a **Management Key** or **Access Key** as described in the specific endpoint docs Check the authentication notes above the endpoint you are calling—each page states exactly which credential is required. ## Rate Limits Descope services are [rate-limited](/rate-limiting) to maintain stable performance. If you exceed the allowed rate, the response returns HTTP `429` along with a `Retry-After` header indicating when you can retry. Design your integration to avoid unnecessary retries and to back off when a 429 is received. ## Trying the APIs Each endpoint page in this documentation includes an interactive request runner. Provide the required parameters, supply the appropriate credentials, and send the request directly from the docs to see the response. This is the fastest way to validate payloads before integrating them into your code. # OpenAPI Specification (/api/openapi-spec) Download the Descope OpenAPI (Swagger) specification to import into Postman, generate clients, or browse with any OpenAPI tool. # OpenAPI Specification The Descope REST API is fully documented as an OpenAPI 3.0 specification. You can download it and import it into Postman, use it to generate API clients in any language, or browse it with your favorite OpenAPI viewer.

Download OpenAPI Spec

Descope_API.yaml

## What's included - Every Descope public API endpoint with full request/response schemas - Authentication endpoints (OTP, Magic Link, Enchanted Link, OAuth, SAML, WebAuthn, TOTP, NOTP, Passwords) - Management endpoints (users, tenants, roles, permissions, Federated Apps, third-party apps, FGA, flows, etc.) - Session endpoints (refresh, validate, get keys) - Federated app endpoints (OIDC, SAML IDP, WS-Fed IDP) ## Using the spec ### Postman 1. Open Postman → **Import** → **File** 2. Select the downloaded `Descope_API.yaml` 3. Postman will create a collection with all endpoints organized by tag ### Generating clients Use any OpenAPI client generator (e.g. [openapi-generator](https://openapi-generator.tech/), [swagger-codegen](https://swagger.io/tools/swagger-codegen/)) with the downloaded spec to scaffold a client in your preferred language. ### Online viewers Drop the spec into any OpenAPI viewer ([Swagger Editor](https://editor.swagger.io/), [Redocly](https://redocly.com/), [Stoplight](https://stoplight.io/)) to browse it interactively. # Resources (/resources) Define API and MCP Server Resources in Descope with OAuth scopes, RBAC role mapping, and connection scope mapping for agent and client access. # Resources A **Resource** is an OAuth [Resource Server](https://datatracker.ietf.org/doc/html/rfc6749#section-1.1): the API or MCP server a client requests access to. When a client authenticates, it specifies which Resource it needs and what scopes it's asking for. Descope evaluates those requests against the scope catalog on that Resource and the [Policies](/policies) you've defined, and issues an access token carrying only the permissions granted. Your server then validates the token's `aud`, `scope`, and other claims before serving any request. Resource access is dictated by [Policies](/policies). They govern which subjects can receive which scopes on which Resources, across your Descope Applications ([Inbound Apps](/identity-federation/inbound-apps) and [Agentic Clients](/agentic-identity-hub/core-components/clients)). Manage Resources in the **Resources** section of the [Descope Console](https://app.descope.com/resources). There are two types: | Type | What it protects | Scope model | |------|------------------|-------------| | **API** | REST, GraphQL, or other backends you build | OAuth scopes mapped to [Descope RBAC roles](/authorization/role-based-access-control) | | **MCP Server** | An MCP endpoint serving tools over the Model Context Protocol | MCP OAuth scopes, optionally mapped to [Connection](/agentic-identity-hub/core-components/connections) scopes for third-party APIs | MCP Server Resources created here also appear under [MCP Servers](https://app.descope.com/agentic-hub/mcp-servers) in the Agentic Identity Hub, where you manage clients, policies, and runtime agent governance. ## When You Need a Resource Any OAuth client that requires specific scoping, and needs a front between itself and the thing it is actually accessing, requires a Resource. Two cases come up constantly: - **Agents reaching external services.** An agent that needs Google or HubSpot should not hold those providers' tokens directly. Instead, define a Resource whose scopes [map to the underlying provider scopes](/resources/scopes-and-roles#mcp-server-resources). The agent authenticates to the Resource with a Descope token, and the Resource exchanges it for the [Connection](/agentic-identity-hub/core-components/connections) token when a tool runs. For agentic use cases this Resource is most commonly an [MCP server](/agentic-identity-hub/core-components/mcp-servers), but it can be a plain API. - **Internal services without proper OAuth.** If an internal API has no scoping of its own, define it as a Resource: it gets an audience, a scope catalog, and Descope-validated tokens without you building any OAuth infrastructure. - **M2M services whose permissions you want to control.** A machine-to-machine client authenticating with [`client_credentials`](/identity-federation/inbound-apps/using-inbound-apps#client-credentials-flow) has no user consent in the loop, so the Resource's scopes are the only permission boundary it has. Define scopes on the Resource the M2M client accesses, then use [policies](/policies) to decide which of those scopes each client receives; the client only ever holds the resource access you granted it. ## Scopes Live on the Resource Only your API or MCP server knows the full set of actions it can perform. That's why scopes are defined on the Resource rather than on each client that calls it. Clients request the scopes they need; the Resource defines which scopes exist and what rules govern them. Keeping the permission catalog in one place also means clients don't drift out of sync as your API evolves. Add a new scope to the Resource and any associated client can request it immediately, with no client-side config changes required. See [Managing Resources](/resources/managing-resources) for docs on how to create, associate, and delete Resources. Three things follow from that model: 1. **Register the service**: Add an API or MCP Server Resource with its audience (`aud`) and scope list. 2. **Grant client access**: Register the [Inbound Apps](/identity-federation/inbound-apps) or [Agentic Clients](/agentic-identity-hub/core-components/clients) that will access this Resource. 3. **Set access rules**: Apply [Policies](/policies) to control which subjects can receive which scopes at token issuance, across all applications and agentic clients. For API Resources, you can also map scopes to [RBAC roles](/authorization/role-based-access-control) so users can only approve permissions that their roles allow. Multiple clients can access the same Resource, and a single client can access multiple Resources: [Federated Applications](/identity-federation/applications) do not support Resource association. Use Inbound Apps or Agentic Clients when you need tokens scoped to a Resource. ## Two ways to model Resources However your agents reach your services, each target needs a Resource so the token has a valid audience and the agent has a standardized way to tell Descope what it is trying to access: - **One Resource per service.** Define a Resource for each API or MCP server an agent connects to. Each has its own audience and scope catalog. - **One Resource for a gateway.** If you run a gateway that fronts many downstream MCP servers and APIs, define a single Resource for the gateway's audience. The gateway routes each call to the right downstream target behind it. In both cases Descope is the authorization server: it issues the token for the Resource's audience, and your service validates it before serving the request. ## Keep credentials off the agent Resources are also how you avoid handing long-lived secrets to an agent. An agent *can* fetch a [Connection](/agentic-identity-hub/core-components/connections) token directly with a user JWT (see the [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk) for fetching a Connection token with a user's JWT), but the recommended pattern puts a Resource, an MCP server or gateway, between the agent and the Connections vault: 1. The agent authenticates to the Resource and receives a short-lived, scoped token for it. 2. The Resource pulls the API key or external OAuth token from Connections at runtime. 3. The Resource calls the downstream service and returns only the result. The agent never sees the third-party API key or OAuth token, which are often not short-lived. This is what the [MCP scope to Connection scope mapping](/resources/scopes-and-roles#mcp-server-resources) on a Resource is for. ## Using a Resource Token Descope issues the access token when the client authenticates at the [authorization server](/identity-federation/inbound-apps/authorization-server): through the authorize flow (login and consent where required) and a call to the [token endpoint](/api/third-party-apps/token-endpoint), naming the target Resource via `resource` when needed. The client then calls the Resource directly with that token; `aud` matches the Resource identifier and `scope` carries the permissions Descope granted. If the client needs to reach a **different** Resource, one with a different audience or scope set, it exchanges the existing token at the [Descope token endpoint](/api/third-party-apps/token-endpoint) (RFC 8693) for a new token scoped to that target. The original token doesn't need to be re-issued; the exchange produces a new one for the specific resource being called: ## Pricing Access to a Resource is metered by how the token is obtained, not by how many requests the client makes afterward: - **User-delegated access**, where a user consents to scopes (typically the authorization code flow), counts as a **Monthly Active Consent (MAC)**: one per unique user, per Resource, per month, no matter how many times that user reconnects. - **Autonomous access**, where a client authenticates as itself with no user (`client_credentials` and other machine-to-machine grants), counts as an **M2M exchange**. If serving a request pulls a vaulted credential from a [Connection](/agentic-identity-hub/core-components/connections), that adds a **Monthly Active Token (MATK)**: one per user or tenant, per Connection, per month. See the [Descope pricing page](https://www.descope.com/pricing) for current rates and the complete definitions. ## API Resources An **API Resource** represents a service you operate as an OAuth resource server: platform APIs, partner integrations, or backends exposed to [Inbound Apps](/identity-federation/inbound-apps) and agents. ### What You Configure - **Resource identifier:** Used as the token `aud` and in discovery metadata so clients target the correct server. - **OAuth scopes:** Permission strings your API enforces (for example `shipments.read`, `admin:reports`). - **Role association:** Map each scope to one or more [Descope RBAC roles](/authorization/role-based-access-control). Only users who hold an associated role can consent to or receive that scope. See [Scopes and roles: API Resources](/resources/scopes-and-roles#api-resources) for the full model. ### OAuth Clients for APIs After you create an API Resource, create [Inbound Apps](/identity-federation/inbound-apps) or [Agentic Clients](/agentic-identity-hub/core-components/clients) to let applications and agents request tokens against it. Inbound Apps and Agentic Clients own grant types, consent flows, and session management. The scope catalog and role mapping live on the Resource. ## MCP Server Resources The same MCP Server Resource appears in the [Agentic Identity Hub → MCP Servers](https://app.descope.com/agentic-hub/mcp-servers) view. An **MCP Server Resource** represents an [MCP server](/mcp) you're building. It registers your MCP server URL as the token audience (`aud`), publishes OAuth discovery metadata, and defines the tool-level scopes clients request at authorization time. ### What you configure - **MCP Server URL.** Base URL of your MCP endpoint (typically ending in `/mcp`). Included in the `aud` claim on issued tokens. - **MCP Server Scopes.** Permissions your server enforces per tool (for example `mcp:calendar.read`). - **Connection Scope Mapping.** Map each MCP scope to [Connection](/agentic-identity-hub/core-components/connections) scopes so Descope knows which vaulted credentials to issue at tool runtime. MCP Server Resources don't use the API Resource role-mapping model. Access control for agents goes through [Policies](/policies) and optional connection scope mappings. See [Scopes: MCP Server Resources](/resources/scopes-and-roles#mcp-server-resources) and [MCP server settings](/agentic-identity-hub/core-components/mcp-servers/settings) for operational details. ## Token Validation Once you've defined a Resource, your backend or MCP server should: 1. Validate JWT signatures using your project's public keys ([session validation](/sessions/validation)). 2. Verify that `iss`, `exp`, and `aud` match this Resource. 3. Enforce required `scope`: and for API Resources, optionally `roles` from [RBAC](/authorization/role-based-access-control). All Resource tokens in a project share the same signing keys ([JWKs](https://docs.descope.com/agentic-identity-hub/core-components/mcp-servers/discovery-url#standard-oauth-fields)). ## Next Steps - [Managing Resources](/resources/managing-resources): create, associate with Inbound Apps and Clients, and delete - [Scopes and roles](/resources/scopes-and-roles): API scope ↔ RBAC mapping; MCP scope ↔ Connection mapping - [Policies](/policies): control which subjects receive which scopes, across all applications and agentic clients - [Inbound Apps](/identity-federation/inbound-apps): OAuth clients for API Resources - [MCP Servers](/agentic-identity-hub/core-components/mcp-servers): operating MCP Server Resources in the Agentic Identity Hub - [Developing APIs with OAuth](/identity-federation/inbound-apps/developing-apis): enforce scopes on API Resources # Management (/resources/managing-resources) Create API and MCP Server Resources in Descope, associate them with Inbound Apps and agentic Clients, and delete Resources when they are no longer needed. # Managing Resources Create and maintain [Resources](/resources) in the [Resources](https://app.descope.com/resources) section of the Descope Console. This page covers creating Resources, linking them to OAuth clients, and removing them. ## Creating a Resource 1. Open [Resources](https://app.descope.com/resources) in the Descope Console. 2. Click **+ Resource** and choose **API** or **MCP Server**. 3. Complete the resource details and define scopes (and role or Connection mappings per type). ![Creating a resource in Descope](/assets/creating-a-resource.webp) ### API Resources For an **API Resource**, configure: - **Resource identifier**: Used in token `aud` and discovery metadata so clients target the correct server. - **OAuth scopes**: Permission strings your API enforces (for example `shipments.read`, `admin:reports`). - **Role association**: Map each scope to [Descope RBAC roles](/authorization/role-based-access-control) so only eligible users can consent to or receive that scope. See [Scopes and roles: API Resources](/resources/scopes-and-roles#api-resources) for the full scope model. ### MCP Server Resources For an **MCP Server Resource**, configure: - **MCP Server URL**: Base URL of your MCP endpoint (typically ending in `/mcp`). Included in the `aud` claim on issued tokens. - **MCP Server Scopes**: Permissions your server enforces per tool (for example `mcp:calendar.read`). - **Connection Scope Mapping**: When tools call third-party services, map each MCP scope to [Connection](/agentic-identity-hub/core-components/connections) scopes. The same MCP Server Resource also appears under [Agentic Identity Hub → MCP Servers](https://app.descope.com/agentic-hub/mcp-servers). See [Scopes: MCP Server Resources](/resources/scopes-and-roles#mcp-server-resources) and [MCP server settings](/agentic-identity-hub/core-components/mcp-servers/settings). Management API support for Resources follows the same project APIs used for [Inbound Apps](/identity-federation/inbound-apps/creating-inbound-apps#creating-an-app-with-an-api-or-sdk) and [MCP server management](/agentic-identity-hub/core-components/mcp-servers/management), depending on resource type. ## Associating Resources with Applications Scopes are **defined once** on the Resource. OAuth clients do not own the scope catalog; they **reference** the Resource and select which of its scopes they may request at authorization time. Descope supports Resource association for: | Client type | Where to configure | Typical use | |-------------|-------------------|-------------| | **[Inbound Apps](/identity-federation/inbound-apps)** | [Inbound Apps](https://app.descope.com/apps/inbound) → App → **Scopes** | Third-party applications and API integrations | | **[Agentic Clients](/agentic-identity-hub/core-components/clients)** | [Clients](https://app.descope.com/agentic-hub/clients) → Client → Scope / MCP server settings | MCP clients and autonomous agents | [Federated Applications](/identity-federation/applications) (SSO IdP connections for SAML/OIDC apps) do **not** support association with Resources. Use [Inbound Apps](/identity-federation/inbound-apps) or [Agentic Clients](/agentic-identity-hub/core-components/clients) when you need OAuth tokens scoped to a Resource. ### Inbound Apps Inbound Apps are typically only associated with API Resources. After you create an API Resource: 1. Create or open an [Inbound App](/identity-federation/inbound-apps/creating-inbound-apps). 2. On the Inbound App, link the **API Resource** and choose which of its scopes the app may request. 3. Configure grant types, redirect URIs, and consent as needed. The Inbound App references scopes from the Resource; you do not redefine the permission catalog on the app. The console may show scope configuration on the Inbound App for the linked Resource; those entries reflect the Resource's scope definitions. ![Configuring an inbound app's scopes in Descope](/assets/inbound_app_scopes.webp) When a user or M2M client authorizes, the token includes scopes from the intersection of what the Inbound App allows, what [RBAC](/resources/scopes-and-roles#api-resources) permits for the user, and what was granted at consent. ### Agentic Clients Agentic clients can be associated with both API Resources and MCP Server Resources. After you create an API Resource or MCP Server Resource: 1. Create or open a [Client](/agentic-identity-hub/core-components/clients) in the Agentic Identity Hub (or register one via [DCR/CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods)). 2. Associate the client with the MCP Server Resource and configure which MCP scopes the client may request. 3. Enable the grant types the client will use (for example authorization code or client credentials). For interactive MCP clients, pair the client with [Policies](/policies) so Descope filters which tool scopes users can consent to and receive. For `client_credentials` agents, scope grants follow client configuration and policies, not per-user consent. When targeting a specific MCP Server Resource in OAuth requests, include the [`resource`](/identity-federation/inbound-apps/authorization-server#mcp-server-resources) parameter ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)) on authorize and token calls. ## Deleting a Resource To delete a Resource: 1. Open [Resources](https://app.descope.com/resources). 2. Select the Resource and choose **Delete** (or use the row action menu). Deleting a Resource is immediate and cannot be undone. Any [Inbound Apps](/identity-federation/inbound-apps) or [Clients](/agentic-identity-hub/core-components/clients) linked to that Resource will lose their scope association. Tokens already issued may remain valid until they expire, but new authorizations against that Resource will fail until you recreate it and re-associate clients. Before deleting: - Confirm no production Inbound Apps or agentic Clients still depend on the Resource. - Update or remove [Policies](/policies) that reference the Resource. - Plan to rotate or revoke outstanding tokens if your resource server still accepts them. ## Related documentation - [Resources overview](/resources): why Resources matter and how they fit into Identity Federation - [Scopes and roles](/resources/scopes-and-roles): API RBAC mapping and MCP Connection mapping - [Inbound Apps](/identity-federation/inbound-apps): OAuth clients for API Resources - [Clients](/agentic-identity-hub/core-components/clients): OAuth clients for MCP Server Resources - [Policies](/agentic-identity-hub/policies): govern agent access to MCP scopes at token issuance # Scopes and Roles (/resources/scopes-and-roles) Map OAuth scopes on API Resources to Descope RBAC roles, and map MCP Server Resource scopes to Connection scopes for third-party tool access. # Scopes on Resources **API Resources** use OAuth scopes tied to [Role-Based Access Control (RBAC)](/authorization/role-based-access-control). **MCP Server Resources** use MCP OAuth scopes that can map to [Connection](/agentic-identity-hub/core-components/connections) scopes when tools need external credentials. For how OAuth **clients** request these scopes, see [Inbound Apps](/identity-federation/inbound-apps) (APIs) and [Agentic clients](/agentic-identity-hub/core-components/clients) (MCP). For controlling client access to specific scopes of these Resources, see the [Policies](/agentic-identity-hub/policies) section of our docs. ## API Resources API Resources define the **permission catalog** for a protected API. Scopes express what a client may do; [Descope roles](/authorization/role-based-access-control) express who the user is allowed to be when granting or receiving those scopes. ### Permission scopes | Field | Description | |-------|-------------| | **Name** | Scope string in authorize requests and tokens (e.g. `shipments.read`) | | **Description** | Shown on consent screens when an [Inbound App](/identity-federation/inbound-apps) requests access | | **Roles** | [RBAC roles](/authorization/role-based-access-control) a user must hold to consent to or receive this scope | Define roles and permissions under [Authorization → RBAC](https://app.descope.com/authorization/rbac), or via [Management SDKs](/authorization/role-based-access-control/with-sdks). Assign roles to users in the [users table](https://app.descope.com/users), through SSO [group mapping](/auth-methods/sso/saml#group-mapping), or programmatically. If a scope requires a role the user does not have, they cannot grant consent for that scope. Only users with matching RBAC roles see and approve those permissions during the Inbound App flow. ### User-information scopes API Resources can also define scopes that gate **which user attributes** are shared with a client (email, name, custom attributes). Each scope maps to a Descope user attribute; access still follows RBAC on the underlying data. ### Enforce on your API Tokens include `scope` (space-separated) and typically `roles` in JWT claims: ```json { "sub": "user123", "aud": ["https://api.example.com"], "scope": "contacts.write", "roles": ["Sales Manager"] } ``` Your API should require the correct **scope** for each route and may check **roles** for sensitive operations. See [Developing APIs with OAuth](/identity-federation/inbound-apps/developing-apis#2-use-role-based-access-control-rbac-with-oauth-scopes). ### Example: scope and role on a CRM API Suppose you protect a CRM API as an API Resource with these definitions: | Scope | Mapped Descope role | What it allows | | ----- | ------------------- | -------------- | | `contacts.read` | `Sales Rep` | List and view contacts | | `contacts.write` | `Sales Manager` | Create and update contacts | An [Inbound App](/identity-federation/inbound-apps) for a partner integration requests `contacts.write` during authorization. Descope checks RBAC before consent: 1. The user must hold the **Sales Manager** role: the role linked to `contacts.write` on the Resource. 2. If they do, they can approve the scope on the consent screen and receive a token with `scope: "contacts.write"` and `roles: ["Sales Manager"]`. 3. If they only have **Sales Rep**, they can consent to `contacts.read` but not `contacts.write`. The partner asks for a **scope** (what the app may do). Descope uses **roles** (who the user is) to decide whether that scope can be granted. Your API then validates the token's `scope` on each route and can use `roles` for stricter checks on admin-only operations. ## MCP Server Resources MCP Server Resources define **tool-level OAuth scopes** for an MCP server. Instead of mapping Resource scopes to RBAC roles, you can map **Resource scopes** to **Connection scopes** for when a tool needs credentials from the [Connections vault](/agentic-identity-hub/core-components/connections). | Field | Description | |-------|-------------| | **Scope name** | Machine-friendly string your MCP server enforces (e.g. `mcp:schedule_meetings`) | | **Connection scopes** | OAuth or API permissions on linked Connections to fetch at tool runtime | | **Consent description** | Text shown on the [user consent screen](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) | | **Mandatory** | Whether the scope must be consented to (optional scopes can be declined) | When a client is granted `mcp:hubspot`, your server can [fetch the matching Connection token](/agentic-identity-hub/core-components/connections/fetching-connection-tokens) for HubSpot without storing third-party secrets in the MCP server itself. Defined MCP scopes appear in the server's [discovery document](/agentic-identity-hub/core-components/mcp-servers/discovery-url) under `scopes_supported`. Role-based checks for MCP flows use [Agentic Identity Hub policies](/agentic-identity-hub/policies) (for example `user.roles CONTAINS "scheduler"`) and consent-flow logic, not scope-to-role fields on the MCP Server Resource definition. API Resources use scope ↔ RBAC on the Resource; MCP Server Resources use scope ↔ Connection mapping on the Resource. For more configuration details, see [MCP server scopes](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes). ## Machine (M2M) clients [Client credentials](/identity-federation/inbound-apps/using-inbound-apps) and autonomous [agentic clients](/agentic-identity-hub/core-components/clients) do not run interactive consent. Scope grants follow client configuration and [policies](/agentic-identity-hub/policies), not per-user RBAC at consent time. # From Auth0 (/migrate/auth0) This guide will cover how to migrate your Auth0 users to Descope. # Auth0 Migration Guide Want a faster, guided migration? The **Auth0 to Descope Migration Skill** in [descope/skills](https://github.com/descope/skills) is a skill for AI agents that interactively walks you through the full migration — user export, attribute mapping, SDK replacement, and more. It may seem like an uphill battle when it comes to changing your authentication provider; however, with Descope's migration tool, you can rest assured your migration will be a breeze. There are two ways you migrate to Descope from Auth0. You can either do a [Full Migration](#full-migration) or a [Hybrid Migration](#hybrid-migration). - **Full Migration** - You completely move off of Auth0 to Descope, no longer using Auth0 in any way. **(Most Common)** - **Hybrid Migration** - You still use Auth0 but you will authenticate your users with Descope (as a federated IdP). This documentation will cover both of these migration options. If you want to continue with the Hybrid Migration, complete the first step under Full Migration first, and then skip to [Hybrid Migration](#hybrid-migration). ## Full Migration There are various options for migration including the ability to migrate via Auth0 API if you have less than 1000 users, JSON export when you have more than 1000 users, without passwords, and with passwords. If you desire to completely move away from Auth0, the Full Migration option is the one for you. Otherwise, complete Step 1 under the Full Migration guide and then skip to the [Hybrid Migration](#hybrid-migration) section. ### Prerequisites Ensure you have the following before starting: - Access to your Auth0 account with Permissions - Familiarity with your current Auth0 setup ### 1. Importing from Auth0 #### Configure Local Environment Follow these steps to set up your environment for migration: 1. You will need to have your Auth0 Tenant ID. Your Auth0 Tenant ID can be found in the URL of your Auth0 dashboard. For example, your URL might look something like this: `https://manage.auth0.com/dashboard/us/dev-xyz/` within this example, your tenant ID would be `dev-xyz`. 2. You will need to generate a Auth0 token. You can generate these 24 hour tokens from [this location](https://manage.auth0.com/#/apis/management/explorer) within Auth0. 3. You will need your Descope project ID which can be found [here](https://app.descope.com/settings/project). 4. You will need a Descope Management Key, if you do not already have one stored, you can create one [here](https://app.descope.com/settings/company/managementkeys). 5. The tool depends on a few custom user attributes that will automatically be created for you within Descope to assist you with the migration. The below outlines the machine names of the attributes created within the [user's custom attributes](https://app.descope.com/users/attributes) section of the Descope console. - `connection` (type: text): This custom attribute will contain the different connection types associated to the user which was migrated from Auth0. - `freshlyMigrated` (type: Boolean): This custom attribute will be set to true during the migration. This allows for you to later check this via a conditional during Descope flow execution, see [Post-Migration](#post-migration-verification). 6. (Optional) If you would like to migrate passwords, open a [ticket](https://support.auth0.com/tickets) with Auth0 support to request an export of your user's password hashes. 7. (Optional) Due to the Auth0 API limitation for loading users, even with pagination, if you have over 1000 users within Auth0, it is recommended to export a JSON of users if you have more than 1000 users. To export the JSON, follow [these steps](https://auth0.com/docs/customize/extensions/user-import-export-extension#export-users). 1. Clone the Repo: ```bash git clone git@github.com:descope/descope-migration.git ``` 2. Create a Virtual Environment ```bash python3 -m venv venv source venv/bin/activate ``` 3. Install the Necessary Python libraries ```bash pip3 install -r requirements.txt ``` 4. Setup Your Environment Variables You can change the name of the `.env.example` file to `.env` to use as a template. Then populate with the items generated within the [prerequisites section](#prerequisites) of this guide. ```bash # Required, a is the management token for your Auth0 tenant AUTH0_TOKEN= # Required, tenant ID of your Auth0 tenant AUTH0_TENANT_ID= # Required, this is your Descope Project ID DESCOPE_PROJECT_ID= # Required, this is your Descope Management Key DESCOPE_MANAGEMENT_KEY= ``` The migration tool source code can be found on [GitHub](https://github.com/descope/descope-migration) #### Running the Migration Script You can use the `-v` or `--verbose` flags to enable more detailed output. This works for both live and dry runs, providing you with additional information. *Migrating from JSON Export:* The below is a list of examples of execution flags that are available when migrating from Auth0 using a JSON ```bash Dry run with passwords: python3 src/main.py auth0 --dry-run --from-json ./path_to_user_export.jso --with-passwords ./path_to_exported_password_users_file.json Dry run without passwords: python3 src/main.py auth0 --dry-run --from-json ./path_to_user_export.jso Live run with passwords: python3 src/main.py auth0 --from-json ./path_to_user_export.jso --with-passwords ./path_to_exported_password_users_file.json Live run without passwords: python3 src/main.py auth0 --from-json ./path_to_user_export.jso ``` *Migrating with Auth0 API:* *Dry Run* You can dry run the migration script which will allow you to see the number of users, tenants, roles, etc which will be migrated from Auth0 to Descope. With Passwords ```bash python3 src/main.py auth0 --dry-run --with-passwords ./path_to_exported_password_users_file.json ``` The output would appear similar to the following: ```bash Running with passwords from file: ./path_to_exported_users_file.json Would migrate 2 users from Auth0 with Passwords to Descope Would migrate 112 users from Auth0 to Descope Would migrate 2 roles from Auth0 to Descope Would migrate MyNewRole with 2 associated permissions. Would migrate Role with 0 associated permissions. Would migrate 2 organizations from Auth0 to Descope Would migrate Tenant 1 with 5 associated users. Would migrate Tenant 2 with 4 associated users. ``` Without Passwords ```bash python3 src/main.py auth0 --dry-run ``` The output would appear similar to the following: ```bash Would migrate 112 users from Auth0 to Descope Would migrate 2 roles from Auth0 to Descope Would migrate MyNewRole with 2 associated permissions. Would migrate Role with 0 associated permissions. Would migrate 2 organizations from Auth0 to Descope Would migrate Tenant 1 with 5 associated users. Would migrate Tenant 2 with 4 associated users. ``` *Live Run* To live migrate your Auth0 users, follow the below examples with and without passwords. With Passwords ```bash python3 src/main.py auth0 --with-passwords ./path_to_exported_password_users_file.json ``` The output will include the responses of the created users, organizations, roles, and permissions as well as the mapping between the various objects within Descope. A log file will also be generated in the format of `migration_log_auth0_%d_%m_%Y_%H:%M:%S.log`. Any items which failed to be migrated will also be listed with the error that occurred during the migration. ```bash Running with passwords from file: ./path_to_exported_users_file.json Starting migration of 2 users from Auth0 password file Starting migration of 112 users found via Auth0 API Still working, migrated 10 users. ... Still working, migrated 110 users. Starting migration of 2 roles found via Auth0 API Starting migration of MyNewRole with 2 associated permissions. Starting migration of Role with 0 associated permissions. =================== Password User Migration ==================== Auth0 Users password users in file 2 Successfully migrated 2 users Created users within Descope 2 =================== User Migration ============================= Auth0 Users found via API 112 Successfully migrated 110 users Successfully merged 2 users Users migrated, but disabled due to one of the merged accounts being disabled 1 Users disabled due to one of the merged accounts being disabled ['auth0|653c1bf0398960f19a6d8171'] Failed to migrate 2 Users which failed to migrate: facebook|122094272078100956 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} facebook|10226222057950897 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} Created users within Descope 108 =================== Role Migration ============================= Auth0 Roles found via API 2 Successfully migrated 2 roles Created roles within Descope 2 =================== Permission Migration ======================= Auth0 Permissions found via API 2 Successfully migrated 2 permissions Created permissions within Descope 2 =================== User/Role Mapping ========================== Successfully role and user mapping Mapped 1 user to MyNewRole Mapped 2 user to Role =================== Tenant Migration =========================== Auth0 Tenants found via API 2 Successfully migrated 2 tenants =================== User/Tenant Mapping ======================== Successfully tenant and user mapping Associated 5 users with tenant: Tenant 1 Associated 4 users with tenant: Tenant 2 ``` Without Passwords ```bash python3 src/main.py auth0 ``` The output will include the responses of the created users, organizations, roles, and permissions as well as the mapping between the various objects within Descope. A log file will also be generated in the format of `migration_log_auth0_%d_%m_%Y_%H:%M:%S.log`. Any items which failed to be migrated will also be listed with the error that occurred during the migration. ```bash Starting migration of 112 users found via Auth0 API Still working, migrated 10 users. ... Still working, migrated 110 users. Starting migration of 2 roles found via Auth0 API Starting migration of MyNewRole with 2 associated permissions. Starting migration of Role with 0 associated permissions. =================== User Migration ============================= Auth0 Users found via API 112 Successfully migrated 110 users Successfully merged 2 users Users migrated, but disabled due to one of the merged accounts being disabled 1 Users disabled due to one of the merged accounts being disabled ['auth0|653c1bf0398960f19a6d8171'] Failed to migrate 2 Users which failed to migrate: facebook|122094272078100956 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} facebook|10226222057950897 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} Created users within Descope 108 =================== Role Migration ============================= Auth0 Roles found via API 2 Successfully migrated 2 roles Created roles within Descope 2 =================== Permission Migration ======================= Auth0 Permissions found via API 2 Successfully migrated 2 permissions Created permissions within Descope 2 =================== User/Role Mapping ========================== Successfully role and user mapping Mapped 1 user to MyNewRole Mapped 2 user to Role =================== Tenant Migration =========================== Auth0 Tenants found via API 2 Successfully migrated 2 tenants =================== User/Tenant Mapping ======================== Successfully tenant and user mapping Associated 5 users with tenant: Tenant 1 Associated 4 users with tenant: Tenant 2 ``` ### 2. Testing and Finalizing Migration Once all of these have been completed, it's a good idea to perform comprehensive testing to ensure all functionalities are working as expected, and your user migration went smoothly. Congratulations, you've fully migrated to Descope! If you're doing the Hybrid Migration approach, you can read on to the next section of this documentation. ## Hybrid Migration Hybrid Migration is less common with Auth0; however, if you have a specific use case, Descope can be used as your authentication service within Auth0. ### 1. Integrating Descope as an Identity Provider Assuming you've already followed the [Step 1](#full-migration) under Full Migration above, you should already have all of your users, tenants, permissions, and roles in Descope. After successfully importing from Auth0, you can proceed with configuring Descope as an external identity provider. To do this, follow the [guide](/identity-federation/applications/setup-guides/auth0) on configuring Descope as an external identity provider for Auth0. ### 2. Testing and Finalizing Migration Once you configured Descope as an external identity provider, you should be able to now login using Descope Flows. Once all of these steps have been completed, it's a good idea to perform comprehensive testing to ensure all functionalities are working as expected, and your user migration went smoothly. ## Post Migration Verification Once the migration tool has ran successfully, you can check the [users](https://app.descope.com/users), [roles and permissions](https://app.descope.com/authorization/rbac), and [tenants](https://app.descope.com/tenants) for the migrated items from Auth0. Make sure to verify the created items based on the output of the migration tool. Also, you can define your user's login experience. Utilizing the `freshlyMigrated` custom user attribute, you can define a path for your user. ![An example of using the freshlyMigrated attribute within a Descope flow conditional](/assets/descope-auth0-migration-guide-example.webp) You can then go down different paths from this conditional within the flow. If you'd like to verify the user's email or phone, you can, and then proceed with adding passwords (or forcing update of password if you migrated passwords) and passkeys. Then after defining the user's experience, you will need to update the user's properties setting the `freshlyMigrated` attribute to `false`. ![An example of setting the freshlyMigrated attribute to false within flows](/assets/descope-auth0-migration-guide-flow.webp) # From Clerk (/migrate/clerk) Learn how to migrate your Clerk users, organizations, roles, and sessions to Descope. # Clerk Migration Guide This guide covers how to migrate your Clerk users, organizations, and roles to Descope. There are two main paths to migrate from Clerk to Descope: - [**Full Migration**](#full-migration) - Export your users (and organizations, roles, and metadata) from Clerk, import them into Descope, and cut over. - [**JIT Migration**](#jit-migration) - Provision users in Descope one at a time as they sign in, without a bulk export. You can also combine either path with **[real-time sync via webhooks](#real-time-sync-with-webhooks)** during a phased rollout, and migrate **[Enterprise SSO connections](#migrating-enterprise-sso-connections)** for tenant-scoped SAML/OIDC. ## Clerk vs. Descope Terminology Clerk and Descope model identity in similar ways, so most of this migration is a direct mapping. | **Clerk Concept** | **Descope Equivalent** | **Description** | |---|---|---| | **User** | **User** | A person's identity record. | | **Organization** | **Tenant** | Clerk Organizations map to Descope [Tenants](/management/tenant-management). Both scope users, roles, and, optionally, SSO to a group. | | **Organization Role** (`org:admin`, `org:member`, custom roles) | **Role** | Clerk's built-in and custom organization roles map to Descope [Roles](/authorization/role-based-access-control). | | **Organization Permission** (`org::`) | **Permission** | Clerk permissions are tied to a role and a "Feature". Descope permissions are tied to a role. | | **Public / Private / Unsafe Metadata** | **Custom Attributes** | Arbitrary key-value data on the user record. Descope doesn't distinguish frontend-writable metadata from backend-only metadata the way Clerk does. Enforce that distinction in your own application logic. | | **External Account** (social login) | **OAuth Login ID** | A linked Google/GitHub/etc. identity. | | **Enterprise Connection** (org-scoped SAML/OIDC) | **Tenant SSO Connection** | Both scope an IdP connection to a specific organization/tenant. | | **Session Token (JWT)** | **Session Token (JWT)** | Both issue short-lived JWTs; each has its own issuer, claims, and JWKS endpoint. | ## Full Migration ### Prerequisites - A Clerk **Secret Key** with Backend API access (**Dashboard → API Keys**). - Your [Descope project ID](https://app.descope.com/settings/project) and a [Descope Management Key](https://app.descope.com/settings/company/managementkeys). ### Step 1: Export Users from Clerk You have two options, depending on whether you need password hashes and how much filtering/automation you need. **Option A: Dashboard CSV export (recommended for most migrations)** 1. In the Clerk Dashboard, go to instance **Settings → User Exports**. 2. Click **Export all users**. This generates a downloadable CSV that includes each user's profile data and **bcrypt password hash**, with no support ticket required. 3. Clerk logs every export in the download history so you can audit who ran it and when. **Option B: Backend API** Use [`GET /v1/users`](https://clerk.com/docs/reference/backend/user/get-user-list) if you need to filter (by organization, creation date, etc.) or automate the export: ```bash curl -s "https://api.clerk.com/v1/users?limit=500&offset=0" \ -H "Authorization: Bearer ${CLERK_SECRET_KEY}" ``` - Paginate with `limit` (max 500, default 10) and `offset`; Clerk doesn't support cursor-based pagination on this endpoint. - Useful filters: `email_address[]`, `phone_number[]`, `organization_id[]`, `created_at_before`/`created_at_after`. - [Rate limits](https://clerk.com/docs/guides/how-clerk-works/system-limits): **1000 requests / 10 seconds** on production instances, **100 requests / 10 seconds** on development instances. Back off on `429` responses using the `Retry-After` header. The Backend API `User` object doesn't include the password hash; only the Dashboard CSV export does. If you need both bulk filtering and passwords, export via the API for user data and cross-reference against the CSV for hashes (matched by `id`). ### Step 2: Map Clerk Fields to Descope | **Clerk Field** | **Descope Field** | |---|---| | `email_addresses[].email_address` (primary) | `email` / `loginIds` | | `phone_numbers[].phone_number` (primary) | `phone` | | `username` | `loginIds` (if used as the primary identifier) | | `first_name`, `last_name` | `givenName`, `familyName` | | `id` | `customAttributes.clerkUserId` (keep for traceability and idempotent re-runs) | | `external_id` (your own legacy ID, if set) | `externalIds` | | `public_metadata`, `private_metadata`, `unsafe_metadata` | `customAttributes` | | `created_at` | `createdTime` | ### Step 3: Import Passwords Clerk always hashes passwords with **bcrypt**, so you only need to handle one format here, unlike providers that carry several legacy hash schemes: ```json "hashedPassword": { "bcrypt": { "hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" } } ``` See the [Custom Data Store guide](/migrate/custom#password-algorithms) for the full request format and other supported algorithms, in case some users were originally migrated *into* Clerk from a system using a different hash. ### Step 4: Handle MFA and Passkeys - **TOTP / backup codes**: Clerk's export doesn't expose raw TOTP secrets or backup codes either: `totpEnabled` and `backupCodeEnabled` are booleans, not the underlying values. If you have the raw secret from a system prior to Clerk, pass it as `seed` in Descope's [Batch Create Users API](/api/management/users/batch-create-users) so users keep using their existing authenticator app. Otherwise, users need to re-enroll TOTP in Descope. - **Passkeys**: Clerk doesn't expose passkey public-key credential data for export, so passkey users need to enroll a new passkey in Descope after migration. ### Step 5: Migrate Social Logins For each entry in a user's `external_accounts[]`, note the `provider` (e.g. `google`) and `provider_user_id`. As long as the user's Descope login ID matches what they'd sign in with (their email, in most cases), social login continues to work with no separate re-import step. Descope links the identity the next time the user authenticates through that provider. ### Step 6: Migrate Organizations to Tenants 1. **List organizations**: `GET /v1/organizations`. 2. **List memberships per organization**: `GET /v1/organizations/{organization_id}/memberships`. Each membership includes a `role` (`org:admin`, `org:member`, or a custom role) and any directly-granted `permissions`. 3. **Create the matching tenant**: [Create Tenant](/api/management/tenants/create-tenant) in Descope, using the Clerk organization `id` or `slug` as the tenant ID for a stable mapping. 4. **Create roles**: Recreate Clerk's custom organization roles as Descope [Roles](/api/management/roles/create-role). Clerk [caps custom roles at 10 per instance](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions), making this a small, one-time mapping exercise. 5. **Role Assignment**: Assign a tenant + role to each user via `userTenants` on [Create User](/api/management/users/create-user) or [Batch Create Users](/api/management/users/batch-create-users): ```json "userTenants": [ { "tenantId": "acme-corp", "roleNames": ["admin"] } ] ``` ### Step 7: Import Users into Descope Use the [Batch Create Users API](/api/management/users/batch-create-users) for bulk import. A few things worth doing on the way in: - Set a `freshlyMigrated: true` custom attribute so you can branch on it in a Descope Flow (prompt for MFA re-enrollment, verify contact info, etc.) and clear it once onboarding finishes. See the [Auth0 guide](/migrate/auth0#post-migration-verification) for a worked example of this pattern. - Set `createdTime` from Clerk's `created_at` so long-time users don't look like brand-new signups. ```json { "users": [ { "loginId": "user@example.com", "email": "user@example.com", "verifiedEmail": true, "givenName": "Ada", "familyName": "Lovelace", "hashedPassword": { "bcrypt": { "hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" } }, "userTenants": [ { "tenantId": "acme-corp", "roleNames": ["admin"] } ], "customAttributes": { "clerkUserId": "user_2abcXYZ", "freshlyMigrated": "true" }, "createdTime": 1700000000 } ] } ``` ### Step 8: Verify and Cut Over 1. Check [Users](https://app.descope.com/users), [Roles](https://app.descope.com/authorization/rbac), and [Tenants](https://app.descope.com/tenants) in the Descope console to confirm the import matches Clerk's counts. 2. Swap the Clerk SDK for the matching Descope SDK in your app and test sign-in with a few migrated accounts. 3. Once you're confident, retire the Clerk integration. ## JIT Migration Clerk doesn't expose a server-side password-verification endpoint you can call from your own backend. Credential verification stays inside Clerk's own SDKs and Frontend API. JIT for Clerk works by verifying a user's already-authenticated Clerk session server-side, not by checking a password. This approach works well if you can run both SDKs side by side for a while, for example during a rollout where some users are still on an app version with Clerk initialized: 1. On a user's next visit, while your app still has the Clerk SDK active, capture their current Clerk session. 2. Send the request to your backend and verify it there using Clerk's backend SDK ([`authenticateRequest()`](https://clerk.com/docs/reference/backend/authenticate-request)) or by [validating the session JWT against Clerk's JWKS](https://clerk.com/docs/guides/sessions/manual-jwt-verification) (`GET /v1/jwks`, no rate limit). 3. Once verified, pull the user's profile via the Backend API and create (or link to) the matching user in Descope with [Create User](/api/management/users/create-user), marking their email/phone as already verified. 4. Complete sign-in through your normal Descope authentication method for that user going forward. 5. After you provision a user this way, their later logins go straight to Descope with no further Clerk involvement. This trades the zero-re-login experience of a password-based JIT flow, which Clerk doesn't support, for a still-seamless one: users never re-enter credentials, but they do need an app version that can still reach Clerk during the transition window. ## Real-Time Sync with Webhooks If you're doing a phased rollout rather than a single cutover, [Clerk's webhooks](https://clerk.com/docs/guides/development/webhooks/overview) can keep Descope up to date in real time: - `user.created` / `user.updated` → call Descope's [Create User](/api/management/users/create-user) or [Update User](/api/management/users/patch-user) API. - `user.deleted` → call [Delete User](/api/management/users/delete-user). ```js // Example: sync a Clerk user.created webhook to Descope app.post('/webhooks/clerk', async (req, res) => { const evt = clerkWebhooks.verify(req); // verify Svix signature if (evt.type === 'user.created') { await descopeClient.management.user.create( evt.data.email_addresses[0].email_address, { customAttributes: { clerkUserId: evt.data.id } } ); } res.sendStatus(200); }); ``` This keeps both systems in sync for any users who sign up (or update their profile) between your bulk export and final cutover. ## Migrating Enterprise SSO Connections If any of your Clerk Organizations use [**Enterprise Connections**](https://clerk.com/docs/guides/organizations/add-members/sso) (SAML or OIDC scoped to that organization), recreate them as Descope [Tenant SSO connections](/auth-methods/sso/saml) ([OIDC variant](/auth-methods/sso/oidc)) on the matching tenant from [Step 6](#step-6-migrate-organizations-to-tenants). - Reuse the same client ID/secret (OIDC) or IdP metadata (SAML) where possible, adding Descope's redirect/ACS URL to the IdP's allow-list. - If you'd rather not ask each tenant admin to reconfigure their IdP at all, see the [SSO Migration guide](/migrate/sso) for consuming the existing IdP response without changing the ACS URL or Entity ID. ## Additional Considerations - **Passkeys and TOTP/backup codes are not portable from Clerk** - see [Step 4](#step-4-handle-mfa-and-passkeys). Plan for re-enrollment as part of your post-migration onboarding flow. - **Bulk export rate limits** - Clerk caps production instances at 1000 requests per 10 seconds on the Backend API. Large user bases should paginate with backoff instead of firing many requests in parallel. - **Transitional token validation** - if your API needs to accept requests from both migrated and not-yet-migrated users, validate Clerk-issued JWTs against Clerk's JWKS (`GET /v1/jwks`) for the old cohort and Descope tokens via the Descope SDK for the new one, routing based on the token's issuer. # From Cognito (/migrate/cognito) This guide covers how to migrate your AWS Cognito users to Descope. # Cognito Migration Guide If you want to keep AWS Cognito as the main identity layer and use Descope for authentication (e.g. passkeys or modern methods), you can configure Descope as an OIDC [identity provider](/identity-federation/applications/setup-guides/aws-cognito) in Cognito. You can migrate from [AWS Cognito](https://aws.amazon.com/cognito/) to Descope in two main ways: - **[Full Migration](#full-migration)** (export users and import into Descope) - **[JIT Migration](#jit-migration)** (provision users on sign-in). You can also use [SSO migration](#sso-migration) for seamless migration of your SSO connections. ## AWS Cognito and Descope Terminology The table below shows how authentication and authorization concepts in AWS Cognito map to Descope. | AWS Cognito | Descope | |-------------|---------| | **User Pool** | [Project](https://app.descope.com/settings/project) - your Descope project and configuration boundary. | | **User** (identity in the user pool) | [User](https://app.descope.com/users) - identity record with login IDs, profile, and custom attributes. | | **Hosted UI** or **Custom authentication flow** | [Flow](/flows) - the sign-up/sign-in or authentication journey you design in Descope. | | **Identity Provider** (social, SAML, or OIDC in Cognito) | [Custom OAuth Providers](/auth-methods/oauth/providers) or [Tenant-based SSO](/auth-methods/sso). | | **App Client** (application registered in user pool) | Your Descope [Inbound Application](/identity-federation/inbound-apps). | | **User attributes** (standard and custom) | [User custom attributes](https://app.descope.com/users/attributes) and built-in user fields (email, name, phone, etc.). | | **User Groups** | [Roles](/authorization/role-based-access-control) in Descope (map Cognito groups to Descope roles). | | **Lambda Triggers** (pre/post authentication, etc.) | [Flow steps](/flows), [Connectors](/connectors), and webhook actions in Descope flows. | | **MFA** (SMS, TOTP, software token) | [MFA in flows](/flows) - TOTP, SMS OTP, passkeys, authenticator apps. | AWS Cognito organizes users into **user pools**. Enterprise IdPs (SAML/OIDC) are external identity providers in that pool. User groups provide basic authorization; advanced authorization requires custom Lambda functions or custom attributes. If you use [Descope Tenants](/b2b#how-multi-tenancy-works) or [Descope Roles](/authorization), you are introducing those models yourself — there is nothing in Cognito to map 1:1 unless you used custom attributes or external systems. You will have to decide how to translate your setup (e.g. one Descope tenant per Cognito user pool, Cognito groups mapped to Descope roles, etc). ## Full Migration AWS Cognito does not allow the export of user password hashes. This means a full migration is always a **without-passwords** migration. In a full migration you move your users and identity data to Descope and eventually run only on Descope. After importing users into Descope, you have two options. You can either transition users to **passwordless** authentication methods (magic link, OTP, passkeys), or require users to **reset their password** on first sign-in (e.g. using a `freshlyMigrated` flag to trigger a password-reset step in your flow). If preserving the existing password experience is critical, consider [JIT migration](/migrate/cognito#jit-migration) instead. ### Getting the Existing User Data The recommended approach is to use the [Descope Migration Tool](#using-the-descope-migration-tool) — either out of the box or modified for your needs. The tool imports users and custom attributes from your Cognito User Pool and, if you use Cognito User Groups, **automatically converts them to Descope roles** and assigns users to those roles. For full control over the migration process, you can instead [export users from the Cognito API](#exporting-users-from-the-cognito-api) with the boto3 client and build your own import pipeline. #### Using the Descope Migration Tool You can use the [Descope Migration Tool](https://github.com/descope/descope-migration) to handle much of the work for you. Migrations can differ wildly depending on the specific identity implementation required. However, this tool acts as a template for which you can edit, depending on your needs. It contains the `boto3` SDK initialization, and the basic user/groups import functionality into Descope. This tool will perform the following actions: 1. Configure your Cognito User Groups as [Roles](/authorization) in Descope (if you use groups, they are converted automatically). 2. Import all of your users from the AWS User Pool defined in the environment variables of the script, including custom attributes defined in the user pool. 3. Associate all users under specific User Groups with the new roles created in Descope. If you want to also import multiple user pools into specific [Tenants](/b2b#how-multi-tenancy-works), which is a common ask, you can modify the source code of the tool to handle this. Please refer to our [Python SDK documentation](/management/tenant-management/sdks) on how to implement this. **Configure Local Environment** 1. Clone the Repo: ```bash git clone git@github.com:descope/descope-migration.git ``` 2. Create a Virtual Environment ```bash python3 -m venv venv source venv/bin/activate ``` 3. Install the Necessary Python libraries ```bash pip3 install -r requirements.txt ``` 4. Setup Your Environment Variables a. **Descope project ID**: Can be found [here](https://app.descope.com/settings/project). b. **Descope Management Key**: If you do not already have one stored, you can create one [here](https://app.descope.com/settings/company/managementkeys). c. **Access Key ID and Secret Access Key**: Obtain your Access Key ID and Secret Access Key by following the steps outlined on the [AWS Guide](https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/). Both can be found after an Access key is created. ![Where to find AWS Access Key and ID](/assets/cognito_access_key.webp) The Secret Access Key itself will be permanently hidden once first generated. d. **Cognito User Pool ID**: This can be found in the AWS Cognito console. ![Where to find Cognito User Pool ID](/assets/cognito_user_pool_id.webp) You can change the name of the `.env.example` file to `.env` to use as a template. Then populate with the items generated above: ``` DESCOPE_PROJECT_ID="" DESCOPE_MANAGEMENT_KEY="" AWS_ACCESS_KEY_ID="" AWS_SECRET_ACCESS_KEY="" COGNITO_USER_POOL_ID="" ``` **Running the Migration Script** You can use the `-v` or `--verbose` flags to enable more detailed output. This works for both live and dry runs, providing you with additional information. *Dry Run:* ```bash python3 src/main.py cognito --dry-run ``` The output would appear similar to the following: ```bash =================== User Migration ============================= Cognito Users found 2 Would try to migrate 2 Users =================== User Group Migration ============================= Cognito User Groups found 2 Would try to migrate 2 User Groups ``` *Live Run:* ```bash python3 src/main.py cognito ``` The output would appear similar to the below and a log file will be generated in the form `migration_log_cognito_%d_%m_%Y_%H:%M:%S.log`. ```bash =================== User Migration ============================= Cognito Users found 2 Successfully migrated 2 users =================== User Group Migration ============================= Cognito User Groups found 2 Successfully migrated 2 groups ``` #### Exporting Users from the Cognito API If you would like to have complete control over the migration process, you can export users from the Cognito API yourself. It's recommended to use the [AWS SDK for Python (boto3)](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html) or other AWS SDKs to list and export users. **Prerequisites** - Access to your [AWS account](https://console.aws.amazon.com/) with permissions to read users from Cognito User Pools. - An [IAM user or role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) with the `cognito-idp:ListUsers` permission (or `AmazonCognitoPowerUser` managed policy). - Your [Descope project ID](https://app.descope.com/settings/project) and a [Descope Management Key](https://app.descope.com/settings/company/managementkeys). - The Cognito **User Pool ID** (found in the [Cognito console](https://console.aws.amazon.com/cognito/)). **Step 1: Set Up AWS SDK** Install the AWS SDK for Python (boto3): ```bash pip install boto3 ``` Configure your AWS credentials. You can either: - Use the [AWS CLI](https://aws.amazon.com/cli/) to configure credentials: `aws configure` - Set environment variables: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - Use IAM roles if running on EC2, Lambda, or ECS **Step 2: Export Users from Cognito** Use the `list_users` method to retrieve all users from your Cognito User Pool. This method supports pagination, so you'll need to handle the `PaginationToken` to retrieve all users. ```python import boto3 import json # Initialize Cognito client cognito = boto3.client('cognito-idp', region_name='us-east-1') # Replace with your region user_pool_id = 'us-east-1_XXXXXXXXX' # Replace with your User Pool ID users = [] pagination_token = None # Paginate through all users while True: if pagination_token: response = cognito.list_users( UserPoolId=user_pool_id, PaginationToken=pagination_token, Limit=60 # Max 60 per page ) else: response = cognito.list_users( UserPoolId=user_pool_id, Limit=60 ) users.extend(response['Users']) # Check if there are more pages if 'PaginationToken' in response: pagination_token = response['PaginationToken'] else: break print(f"Total users retrieved: {len(users)}") ``` Key details about this API: - **Pagination**: The `list_users` API returns up to 60 users per page. Use the `PaginationToken` from the response to retrieve the next page. Continue until no `PaginationToken` is returned. - **Rate limiting**: AWS Cognito enforces [request quotas](https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html). The `ListUsers` API has a default limit of 5 requests per second. If you exceed this, you'll receive a `TooManyRequestsException`. Implement exponential backoff and retry logic. - **User attributes**: Each user has an `Attributes` array containing objects with `Name` and `Value`. Standard attributes include `email`, `phone_number`, `name`, etc. Custom attributes are prefixed with `custom:`. - **User status**: The `UserStatus` field indicates the user's state (`CONFIRMED`, `UNCONFIRMED`, `FORCE_CHANGE_PASSWORD`, etc.). Example response (abbreviated): ```json { "Users": [ { "Username": "a1b2c3d4-...", "Attributes": [ {"Name": "sub", "Value": "a1b2c3d4-..."}, {"Name": "email", "Value": "jane@example.com"}, {"Name": "email_verified", "Value": "true"}, {"Name": "name", "Value": "Jane Doe"}, {"Name": "custom:department", "Value": "Engineering"} ], "UserCreateDate": "2024-01-15T10:30:00Z", "UserLastModifiedDate": "2024-03-01T14:20:00Z", "Enabled": true, "UserStatus": "CONFIRMED" } ], "PaginationToken": "..." } ``` **Step 3: Export User Groups (Optional)** If you use Cognito User Groups and want to map them to Descope Roles, export the groups and their memberships: ```python # List all groups in the user pool groups_response = cognito.list_groups(UserPoolId=user_pool_id) groups = groups_response['Groups'] # For each user, get their group memberships for user in users: username = user['Username'] user_groups_response = cognito.admin_list_groups_for_user( Username=username, UserPoolId=user_pool_id ) user['Groups'] = [g['GroupName'] for g in user_groups_response['Groups']] ``` **Step 4: Map Cognito Attributes to Descope** For each user, map the exported attributes: - Cognito `Username` or `email` attribute → Descope `loginIds` (required; unique per user). - Cognito `name` attribute → Descope `name`. - Cognito `given_name` → Descope `givenName`. - Cognito `family_name` → Descope `familyName`. - Cognito `email` → Descope `email`. - Cognito `phone_number` → Descope `phone`. - Cognito `email_verified` → Descope `verifiedEmail`. - Cognito `phone_number_verified` → Descope `verifiedPhone`. - Custom attributes (`custom:*`) → Descope [custom attributes](https://app.descope.com/users/attributes) (create the corresponding custom attribute definitions in Descope first). - Cognito User Groups → Descope `roleNames` (map group names to Descope role names). Build a JSON array in the format expected by Descope (see [user format guide](/migrate/custom/user-format-json)). Example mapping code: ```python def map_cognito_user_to_descope(cognito_user): """Map Cognito user to Descope format""" # Helper to get attribute value def get_attr(name): for attr in cognito_user.get('Attributes', []): if attr['Name'] == name: return attr['Value'] return None email = get_attr('email') phone = get_attr('phone_number') descope_user = { "loginIds": [email] if email else [cognito_user['Username']], "email": email, "phone": phone, "name": get_attr('name'), "givenName": get_attr('given_name'), "familyName": get_attr('family_name'), "verifiedEmail": get_attr('email_verified') == 'true', "verifiedPhone": get_attr('phone_number_verified') == 'true', "customAttributes": {}, "roleNames": cognito_user.get('Groups', []) } # Map custom attributes for attr in cognito_user.get('Attributes', []): if attr['Name'].startswith('custom:'): attr_name = attr['Name'].replace('custom:', '') descope_user['customAttributes'][attr_name] = attr['Value'] # Add migration flag descope_user['customAttributes']['freshlyMigrated'] = True return descope_user # Map all users descope_users = [map_cognito_user_to_descope(u) for u in users] ``` **Step 5: Import Users into Descope** Use the Descope Management API to import users: - **Single user**: [Create User](/api/management/users/create-user) — `POST /v1/mgmt/user/create` - **Batch**: [Batch Create Users](/api/management/users/batch-create-users) — `POST /v1/mgmt/user/create/batch` (see [rate limits](/rate-limiting#specific-user-management-paths) here) Example batch import code: ```python import requests DESCOPE_PROJECT_ID = '__ProjectID__' DESCOPE_MANAGEMENT_KEY = 'your-management-key' def import_users_to_descope(users): """Import users to Descope using batch API""" url = '__BaseURL__/v1/mgmt/user/create/batch' headers = { 'Authorization': f'Bearer {DESCOPE_PROJECT_ID}:{DESCOPE_MANAGEMENT_KEY}', 'Content-Type': 'application/json' } # Batch in groups of 100 (API limit) batch_size = 100 for i in range(0, len(users), batch_size): batch = users[i:i + batch_size] response = requests.post(url, json={'users': batch}, headers=headers) if response.status_code == 200: print(f"Successfully imported batch {i//batch_size + 1}") else: print(f"Error importing batch: {response.text}") import_users_to_descope(descope_users) ``` When importing, you'll need to make sure all `loginId` values you import are unique per user. Optionally set a custom attribute `freshlyMigrated` to `true` for post-migration flows (see [Post-Migration Verification](#post-migration-verification)). You can also set `verifiedEmail` and `verifiedPhone` to `true` if these were already verified in Cognito, so users are not asked to re-verify. Once the users are imported, you can verify them in the [Descope Users](https://app.descope.com/users) list and test sign-in with a few migrated users. ## JIT Migration In order to use JIT migration, you must keep Cognito running until all active users have signed in at least once. With **Just-In-Time (JIT) migration**, you do not bulk-export users. Users are provisioned in Descope **when they sign in**. This approach lets you migrate users gradually without downtime and without needing to coordinate a bulk import. AWS Cognito does not expose user password hashes, so your JIT architecture depends on whether you want to **preserve the password sign-in experience** or **transition to passwordless**. The two main approaches are described below. ### Using Cognito USER_PASSWORD_AUTH with a Generic HTTP Connector This approach requires you to keep your Cognito User Pool and App Client active until all users have migrated. You can decommission Cognito only after all active users have signed in at least once through Descope and been provisioned. Use this approach if you want to **keep passwords working** while ensuring that **all traffic flows through Descope**. Descope collects the username and password, a Generic HTTP Connector calls Cognito's `InitiateAuth` API with the `USER_PASSWORD_AUTH` flow to verify the credentials, and on success the flow provisions (or updates) the user in Descope. #### 1. Configure your Cognito User Pool and App Client In Cognito, make sure your User Pool and App Client are configured for the username/password flow: - **User pool attributes** (User Pool): - `UsernameAttributes`: include `email` (or `phone_number`) so users can sign in with that identifier. - `AutoVerifiedAttributes`: include `email` so Cognito automatically sends verification emails. - `VerificationMessageTemplate` / `EmailConfiguration`: configure how Cognito sends verification emails (for example, `DefaultEmailOption: CONFIRM_WITH_LINK` and `EmailSendingAccount: COGNITO_DEFAULT`). - **App client settings** (User Pool Client): - `SupportedIdentityProviders`: include `COGNITO`. - `ExplicitAuthFlows`: include at least `ALLOW_USER_PASSWORD_AUTH` and `ALLOW_REFRESH_TOKEN_AUTH`. - Remove OAuth-only settings you no longer need (resource server, OAuth scopes, and `GenerateSecret` if you are not using client credentials). With this configuration, Cognito can accept username/password credentials via the `USER_PASSWORD_AUTH` flow. #### 2. Configure a Generic HTTP Connector to call Cognito Create a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) in Descope to call the Cognito `InitiateAuth` API: - **Base URL**: `https://cognito-idp..amazonaws.com` (for example, `https://cognito-idp.us-east-1.amazonaws.com`). - **Headers**: - `Content-Type: application/x-amz-json-1.1` - `X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth` - **Method**: `POST` - **Body** (template): ```json { "ClientId": "", "AuthFlow": "USER_PASSWORD_AUTH", "AuthParameters": { "USERNAME": "{{form.email}}", "PASSWORD": "{{form.password}}" } } ``` You can parameterize `ClientId` or the region with connector variables or environment-specific configuration if needed. ![postman cognito flow](/assets/postman-cognito-request-migration.webp) #### 3. Build the JIT flow in Descope In your Descope Flow: 1. **Check if the user already exists in Descope** - At the start of the flow, use a **Condition** on `user.loginIds` (or an equivalent indicator) to see if the user is already known to Descope. - If `user.loginIds` is **not empty** (user already exists), you can route them through a normal **Sign In / Password** or passwordless path in Descope and **skip Cognito** entirely. 2. **Collect credentials for new / unknown users** - If `user.loginIds` is **empty** (new to Descope), show a **Screen** that collects email and password (for example, `form.email` and `form.password`). - After the screen, call the Cognito `InitiateAuth` endpoint via the **Generic HTTP Connector** configured above to verify the password against Cognito. 3. **Branch on Cognito result** - On **success** (for example, HTTP 200 with an `AuthenticationResult`), treat this as either: - An existing Cognito user with a correct password, or - A brand-new Descope user you are onboarding. In both cases, use the **Sign Up / Password** action in Descope to create the user (or idempotently ensure they exist) with the provided email and password, mapping any needed attributes and roles. Subsequent sign-ins can then go directly through Descope without calling Cognito again. - On **failure** (Cognito returns an error or non-200), route the user to a **Reset Password / Passwordless** path: - For example, send a magic link or OTP, or force a password reset in Descope before allowing access. 4. **Complete the flow** - After successful Sign Up / Password or passwordless recovery, continue the flow as a normal Descope sign-in (issue a Descope session token, set any `freshlyMigrated` flags, etc.). Over time, as users sign in via Descope and are provisioned with a Descope password (or passwordless method), you can gradually stop calling Cognito for those users. Example Cognito API response: ![Cognito API response](/assets/cognito-api-response-migration.webp) #### 4. Storing the password when the user already exists in Descope **Sign Up / Password** fails if the user already exists in Descope. Flows that sign the user in before storing the password hit this: you verify the credentials against Cognito, sign the user in with a magic link or OTP to give them a Descope session, then write the password they just typed to their new Descope identity. For that last step, use the **Update Password** action instead. It requires the user to be signed in at that point in the flow, which the magic link or OTP step has already handled. Both actions read the password from either the **Login Password** or the **New Password** [screen component](/flows/screens/inputs/passwords#using-it-to-set-a-password). Use **Login Password** here. The user is re-entering a password they already have and doesn't need the confirmation field or policy previewer that **New Password** adds. The stored password still has to satisfy your project's [password policy](/auth-methods/passwords/settings), whichever component collected it. A Cognito pool that allowed shorter or simpler passwords than your Descope policy will leave those users stuck at this step. Either match your policy to Cognito's for the migration window, or handle the failure in the flow by sending the user through a password reset. ### Using Cognito as a Custom OIDC Provider Use this approach if you are transitioning to **passwordless** authentication or are okay with requiring users to reset their password on first sign-in. You configure your AWS Cognito User Pool as a [custom OAuth provider](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider) in Descope, so users authenticate through the standard Cognito sign-in experience (including Hosted UI) and are provisioned in Descope automatically on first sign-in. You'll need to make sure there is an [App Client](https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html) in Cognito that Descope can use to sign in users. If using an existing Cognito app client, make sure you add the Descope authorized callback URL in the app client's settings. **How it works:** 1. **Configure Cognito in Descope** - In Descope, add AWS Cognito as a [custom OIDC provider](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider). - Use your Cognito User Pool domain (e.g. `https://your-domain.auth.us-east-1.amazoncognito.com`) - Set the app client ID and secret from your Cognito User Pool - Configure the OIDC endpoints (Cognito follows standard OIDC discovery at `https://your-domain.auth.region.amazoncognito.com/.well-known/openid-configuration`) 2. **Add OIDC sign-in to your flow** - In your Descope flow, add a [Sign Up or In / OAuth](/auth-methods/oauth#flow-actions) step that redirects users to AWS Cognito. They will then sign in through Cognito as usual (including MFA, if enabled). 3. **User is provisioned in Descope** - After Cognito authenticates the user, it returns an ID token with claims (email, name, custom attributes). Descope provisions the user and maps those claims to Descope user attributes, then issues a Descope session token. Once a user has been provisioned in Descope, subsequent sign-ins can go directly through Descope, controlled via a [condition](/flows/conditions) in your flow. ![An example of using the freshlyMigrated attribute within a Descope flow conditional](/assets/descope-auth0-migration-guide-example.webp) ## SSO Migration If your AWS Cognito setup includes SSO (SAML/OIDC) for organizations or partners, you can migrate those configurations to Descope without forcing admins to re-configure their IdPs. Descope can consume the existing IdP response and complete authentication so end users keep a seamless experience. For the full process—implementing SSO with Descope, setting up tenants, DNS redirect, and testing—see [SSO Migration](/migrate/sso). ## AWS Service Integration If you use other AWS services that rely on Cognito tokens (API Gateway, AppSync, Lambda authorizers), you'll need to configure them to accept Descope tokens after migration. ### Transitioning to AWS JWT Authorizers This is necessary if you want to continue to use Descope with API Gateway. You can configure AWS JWT Authorizers as detailed in [AWS JWT Authorizer with Descope](/sessions/validation/jwt-authorizers/aws-jwt-authorizer) KB article. ### Using AppSync Endpoints with Descope This is necessary if you want to continue to use Descope with GraphQL endpoints and AppSync. A detailed guide on how to implement this can be found in our [AppSync Authorizer](/sessions/validation/jwt-authorizers/aws-app-sync) KB article. ## Session Validation Strategy Whether you do a [full migration](#full-migration) or a [JIT migration](#jit-migration), plan for a period where both your **backend** and any **API Gateway layer** (for example, AWS API Gateway with a Cognito authorizer) may need to accept Descope and Cognito tokens at the same time. Your backend (and, if applicable, your API Gateway authorizer) should support **both** token types during the transition: - **Validate Descope session tokens (JWTs)** for users who have already migrated or signed in via Descope. - **Validate AWS Cognito tokens** for users who still have active Cognito sessions. Inspect the token (e.g. issuer or `kid` in the JWT header) to determine the provider, then validate accordingly. This lets you roll out the migration gradually. For the implementation pattern, see the docs on backend session validation [here](/migrate/session-migration#step-1-dual-token-validation-in-your-backend). Example dual validation logic (Python): ```python import jwt import requests COGNITO_REGION = 'us-east-1' COGNITO_USER_POOL_ID = 'us-east-1_XXXXXXXXX' DESCOPE_PROJECT_ID = '__ProjectID__' def validate_token(token): """Validate token from either Cognito or Descope""" # Decode header to check issuer header = jwt.get_unverified_header(token) decoded = jwt.decode(token, options={"verify_signature": False}) issuer = decoded.get('iss', '') if 'amazoncognito.com' in issuer: # Validate Cognito token return validate_cognito_token(token) elif 'descope.com' in issuer: # Validate Descope token return validate_descope_token(token) else: raise ValueError('Unknown token issuer') def validate_cognito_token(token): """Validate Cognito JWT""" # Get Cognito JWKs jwks_url = f'https://cognito-idp.{COGNITO_REGION}.amazonaws.com/{COGNITO_USER_POOL_ID}/.well-known/jwks.json' jwks = requests.get(jwks_url).json() # Validate token (simplified - use a library like python-jose in production) # ... validation logic ... return True def validate_descope_token(token): """Validate Descope JWT""" # Use Descope SDK or validate manually # ... validation logic ... return True ``` # From Firebase (/migrate/firebase) Learn how to seamlessly migrate your Firebase users to Descope using our comprehensive migration tool. # Firebase Migration Guide Migrating your Firebase users to Descope can be a smooth process with the right guidance. This guide provides detailed steps for a seamless transition. There are two ways you migrate to Descope from Firebase. You can either do a [Full Migration](#full-migration) or a [Hybrid Migration](#hybrid-migration). - **Full Migration** - You completely move off of Firebase Authentication to Descope, no longer using Firebase authentication in any way. **(Most Common)** - **Hybrid Migration** - You still use Firebase Authentication but you will authenticate your users with Descope (as a federated IdP). This documentation will cover both of these migration options. If you want to continue with the Hybrid Migration, complete the first step under Full Migration first, and then skip to [Hybrid Migration](#hybrid-migration). ## Full Migration If you desire to completely move away from Firebase Authentication, the Full Migration option is the one for you. Otherwise, complete Step 1 under the Full Migration guide and then skip to the [Hybrid Migration](#hybrid-migration) section. ### Prerequisites Ensure you have the following before starting: - Access to your Firebase account with permissions to manage Authentication. - Familiarity with your current Firebase Authentication setup. ### 1. Importing Users from Firebase #### Configure Local Environment Follow these detailed steps to set up your environment for migration: 1. **Clone the Repo:** ```bash git clone git@github.com:descope/descope-migration.git ``` 2. **Create a Virtual Environment:** ```bash python3 -m venv venv source venv/bin/activate ``` 3. **Install Python Libraries:** ```bash pip3 install -r requirements.txt ``` 4. **Download Firebase Credential:** This is created by following the instructions in the [Firebase docs](https://firebase.google.com/docs/admin/setup#initialize_the_sdk_in_non-google_environments). You'll then need to copy over the JSON file, rename it to `firebase-certs.json` and put it in the `/creds` folder. For more details, refer [below](#firebase-credentials) 5. **Get Password Hash Parameters:** You'll need to provide the password hash parameters that are specific to your Firebase project. This is mandatory if you're migrating passwords over to Descope. For more details, refer [below](#password-hash-parameters). 6. **Set up Environment Variables:** Adjust the `.env.example` file and rename it to `.env`. For more details, refer [below](#environment-variables). 7. **Custom Attributes:** The tool depends on a few custom user attributes that will be automatically created within Descope to assist you with the migration. The below outlines the machine names of the attributes that will be created. - `freshlyMigrated` (type: Boolean): This custom attribute will be set to true during the migration. This allows for you to later check this via a conditional during Descope flow execution, see [Post-Migration](#post-migration-verification). - `UUID` (type: String): This is the UUID value of the user in firebase. - All other custom attributes defined in either Firestore or the Realtime Database Schema. The migration tool source code can be found on [GitHub](https://github.com/descope/descope-migration) ##### Firebase Credentials: You'll need to download your Firebase Credential and put it in the `/creds` folder in order to give the Firebase Admin SDK access to your user table. 1. First, you'll need to open Settings > [Service Accounts](https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk), in the Firebase Console. 2. Click `Generate New Private Key`, then confirm by clicking `Generate Key`. ![Firebase Credentials](/assets/firebase_creds.webp) 3. Securely store the JSON file containing the key, in the `/creds` folder, in the root of this repository. Rename it to `firebase-certs.json`. #### Password Hash Parameters: In order to successfully copy over the passwords over your users, you'll need to provide the password hash parameters that are specific to your Firebase project. You can find these in the Firebase Console, under Authentication -> Users. ![Firebase Console](/assets/firebase-console.webp) The Firebase Admin SDK service account does not have the `firebaseauth.configs.getHashConfig` permission by default, which is required to retrieve password hashes. Without this permission, the `passwordHash` and `passwordSalt` fields will not be set when exporting users. ##### Setting up the Required Permission 1. Go to the [Roles page](https://console.cloud.google.com/iam-admin/roles) in the **IAM & Admin panel** in the Google Cloud Console. 2. Select your project from the drop-down. 3. Click **CREATE ROLE**. 4. Click **ADD PERMISSIONS**. 5. Search for `firebaseauth.configs.getHashConfig` permission and select that checkbox. 6. Click **ADD**. 7. Click **CREATE** to finish creating the new role. ##### Adding the Role to Your Service Account 1. In the IAM & Admin panel, select **IAM**. 2. Select the service account from the list of members for editing. 3. Click **ADD ANOTHER ROLE**. 4. Search for the new custom role you previously created. 5. Click **SAVE**. ##### Retrieving Password Hash Parameters Under the three dots in the top right corner, you'll find the option for `Password hash parameters`. Once you've opened these up, copy these over to a `password-hash.txt` file and place it in the `/creds` folder. ![Firebase Password Hash Parameters](/assets/firebase-password-hash-parameters.webp) After that, the migration tool will automatically recognize your password hash parameters and extract the necessary claims from the parameters JSON. ##### Environment Variables: The environment variables that are mandatory, no matter what, are the following: 1. **DESCOPE_PROJECT_ID** - You can get this from [Project Settings](https://app.descope.com/settings/project) in the Console 2. **DESCOPE_MANAGEMENT_KEY** - You can get this from Company Settings -> [Management Keys](https://app.descope.com/settings/company/managementkeys) in the Console There is one optional environment variable, which you will need to provide if you're custom user attributes are stored using Firebase [Realtime Database](https://firebase.google.com/docs/database). This is the Database URL that you can find in your Realtime Database Dashboard. If you're not using Realtime Database to store custom attributes, and instead are using Firestore, or do not wish to migrate over custom attributes you do not need to provide this. 1. **FIREBASE_DB_URL** - You'll need this so that the tool knows what database to request information from. ![Firebase DB URL location](/assets/firebase-db-url-location.webp) #### Running the Migration Script You can use the `-v` or `--verbose` flags to enable more detailed output. This works for both live and dry runs, providing you with additional information. *Dry Run:* Before executing the live migration, you can perform a dry run to estimate the scope of the migration: ```bash python3 src/main.py firebase --dry-run ``` This will output the number of users and other entities to be migrated. *Live Migration:* To start the actual migration, run: ```bash python3 src/main.py firebase ``` Follow the instructions provided by the tool, including whether to migrate custom attributes and from which Firebase database. The tool will first ask if you want to migrate custom attributes over. If you type `y`, then you'll need to provide the tool with the source of the attributes (either Firestore or Realtime Database). After that, the tool will begin migrating your users. The output will include the responses of the created usersas well as the mapping between the various objects within Descope. A log file will also be generated in the format of migration_log_firebase_%d_%m_%Y_%H:%M:%S.log. Any items which failed to be migrated will also be listed with the error that occurred during the migration. ```bash Starting migration of 112 users found via Firebase Admin SDK Still working, migrated 10 users. ... Still working, migrated 110 users. =================== User Migration ============================= Firebase Users found via Admin SDK 112 Successfully migrated 112 users Successfully merged 0 users Created users within Descope 112 ``` ### 2. Additional Migration Steps You can also migrate your Firebase configurations manually in the Descope console, such as Authorized Domains and Self-Registration Allowance. - **Authorized Domains** - you can move these over to the [Approved Domains](https://app.descope.com/settings/project) section in the Descope Console | Firebase Console | Descope Console | | :------------------------------------------------------------------------------------------: | :-----------------------------------------------------------------------------------------: | | ![Firebase Authorized Domain](/assets/authorized-domain-firebase.webp) | ![Descope Authorized Domain](/assets/approved-domain-descope.webp) - **Enable Create (Sign Up)** - you can block self-registration under [Project Settings](https://app.descope.com/settings/project) in the Descope Console | Firebase Console | Descope Console | | :-------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------: | | ![Enable Create Firebase Feature](/assets/enable-create-firebase.webp) | ![Enable Create Descope Feature](/assets/enable-create-descope.webp) ### 3. Testing and Finalizing Migration Once all of these have been completed, it's a good idea to perform comprehensive testing to ensure all functionalities are working as expected, and your user migration went smoothly. Congratulations, you've fully migrated to Descope! If you're doing the Hybrid Migration approach, you can read on to the next section of this documentation. ## Hybrid Migration Hybrid Migration is less common with Firebase Authentication because you can fully use Descope while continuing to use other Firebase services. However, if you have a specific use case, Descope can be used as your authentication service within Firebase Authentication. ### 1. Integrating Descope as an Identity Provider Assuming you've already followed the [Step 1](#full-migration) under Full Migration above, you should already have all of your users in Descope. After successfully importing the user data from Firebase, you can proceed with configuring Descope as an external identity provider. To do this, follow the [guide](/identity-federation/applications/setup-guides/firebase-oidc) on configuring it as an external identity provider for Firebase. ### 2. Testing and Finalizing Migration Once you configured Descope as an external identity provider, you should be able to now login using Descope Flows. Once all of these steps have been completed, it's a good idea to perform comprehensive testing to ensure all functionalities are working as expected, and your user migration went smoothly. ## Post Migration Verification Once you have migrated your users to Descope, you can define your user's login experience. Utilizing the `freshlyMigrated` custom user attribute, you can define a path for your user. ![An example of using the freshlyMigrated attribute within a Descope flow conditional](/assets/descope-auth0-migration-guide-example.webp) You can then go down different paths from this conditional within the flow. If you'd like to verify the user's email or phone, you can, and then proceed with adding passwords (or forcing update of password if you migrated passwords) and passkeys. Then after defining the user's experience, you will need to update the user's properties setting the `freshlyMigrated` attribute to `false`. ![An example of setting the freshlyMigrated attribute to false within flows](/assets/descope-auth0-migration-guide-flow.webp) # Overview (/migrate) Learn how to migrate to Descope from third-parties # Migration to Descope Migrating to Descope from another authentication provider or custom setup is a structured process, designed to allow you to retain security, functionality, and ease of use. This guide covers migration options, supported third-party services, and password migration methods. Follow these steps to ensure a seamless transition to Descope. If you wish to layer Descope on top of your legacy provider instead of migrating fully away from it, you can also configure Descope as an [identity provider](/identity-federation/applications) in your legacy provider. ## Migration Approaches There are two primary strategies to migrate users and data to Descope: - **Full Migration**: Bulk-export users (and associated attributes/roles), import them into Descope, and then gradually switch traffic so Descope is the only source of truth. You can optionally add [session migration](/migrate/session-migration) so users with active sessions at your legacy provider do not have to sign in again. - **JIT (Just-In-Time) Migration**: Do not bulk-export users. Instead, provision users in Descope **when they sign in**, often by verifying their credentials or identity against the legacy provider (for example, via a generic HTTP connector or custom API) and then creating/updating the user in Descope on-the-fly. Each individual migration guide (Auth0, Azure AD B2C, Cognito, etc.) explains how to implement these strategies for that specific provider, including when to combine JIT with full migration or session migration. ### Supported Password Hashing Algorithms Descope supports various hashing protocols to ensure smooth migration of passwords and minimal disruption for users. These protocols include: 1. **Bcrypt** 2. **Argon2** 3. **Django** 4. **Firebase** 5. **PBKDF2** 6. **PHPass** 7. **MD5** (for legacy compatibility) For custom password hashes, explore how Descope's User Management API can handle additional data types and protocols. ## Migration Steps ### 1. **Prepare User Data** - Export data, ensuring you have all necessary fields such as unique identifiers (Login IDs), emails, names, and passwords. - Verify data accessibility to ensure smooth import using Descope's [User Management API](/api/management/users). ### 2. **Create Users in Descope** - Use Descope's **Create User API** or **Batch Create User API** to import your user data. This will create user records in Descope, including role assignments, attributes, and organization mappings as needed. ### 3. **Import Passwords (Optional)** - Descope supports various hashing algorithms, so users can log in with their existing passwords without needing a reset. For unsupported formats, consider a **password reset flow** or handle it via API updates. ### 4. **Handle Social Logins and External Identity Providers (Optional)** - For accounts with social logins (e.g., Google, Facebook), the migration will handle login IDs. Descope will link these IDs without needing further imports. } href="/migrate/auth0" title="Auth0" description="Learn how to migrate your Auth0 users, organizations, permissions, and roles to Descope" /> } href="/migrate/azure-ad-b2c" title="Azure AD B2C" description="Learn how to migrate your Azure AD B2C users to Descope" /> } href="/migrate/cognito" title="AWS Cognito" description="Learn how to migrate your Cognito user groups and custom attributes to Descope" /> } href="/migrate/firebase" title="Firebase" description="Learn how to migrate your Firebase users to Descope" /> } href="/migrate/ping" title="Ping" description="Learn how to migrate your Ping users, environments, permissions, and roles to Descope" /> } href="/migrate/keycloak" title="Keycloak" description="Learn how to migrate your Keycloak users, realms, and identity providers to Descope" /> } href="/migrate/stytch" title="Stytch" description="Learn how to migrate your Stytch users, organizations, SDKs, sessions, and M2M clients to Descope." /> } href="/migrate/custom" title="Custom Data Store" description="Learn how to migrate your users from a custom data store to Descope" /> # From Keycloak (/migrate/keycloak) Learn how to migrate your Keycloak users, realms, and identity providers to Descope using our Custom Data Store migration or JIT provisioning approaches. # Keycloak Migration Guide This guide provides step-by-step instructions for a successful migration from Keycloak to Descope. There are two main paths to migrate Keycloak users to Descope: - **Full Migration** - You completely migrate your users and identity providers from Keycloak into Descope and retire Keycloak. - **Just-in-Time (JIT) Provisioning** - You progressively migrate users into Descope as they log in. Descope verifies their existing Keycloak password via Keycloak's API, then creates their user record locally. ## Keycloak vs. Descope Terminology Keycloak and Descope use similar identity concepts, but they differ in how user isolation works. | **Keycloak Concept** | **Descope Equivalent** | **Description** | |-----------------------|------------------------|----------------| | **Realm** | **Project** | A Keycloak Realm is a fully isolated identity domain. Descope's equivalent isolation boundary is a Project. | | **User** | **User** | A person's identity record. In Descope, users are global within a project and can belong to multiple tenants. | | **Group / Organization** | **Tenant** | Keycloak groups or organizations map to Descope tenants. Tenants define contextual membership and permissions but share the same user base. | | **Role** | **Role** | Keycloak Roles map directly to Descope Roles. | | **Client (Application)** | **Application** | OAuth2/OIDC clients in both systems serve the same purpose. | | **Identity Provider (OIDC/SAML)** | **SSO Connection** | Both systems use external IdPs for authentication and federation. | In short: **Keycloak Realms** are effectively [Projects](/management/project-settings) and **Keycloak Groups/Organizations** are [Tenants](/management/tenant-management). ## Exporting Users from Keycloak You can export users either via the **Admin REST API** or directly from the Keycloak database. ### Export via REST API ```bash kcadm.sh get users -r -o > keycloak_users.json This will output JSON records with usernames, emails, and attributes. #### b. Export via Database Access (Optional) If you have DB access (PostgreSQL or MySQL): ```sql select FIRST_NAME, LAST_NAME, EMAIL, USERNAME, EMAIL_VERIFIED, U.ID as ID, C.SECRET_DATA::jsonb ->> 'value' as PASSWORD, C.SECRET_DATA::jsonb ->> 'salt' as SALT, C.CREDENTIAL_DATA::jsonb ->> 'hashIterations' as HASHITERATIONS, C.CREDENTIAL_DATA::jsonb ->> 'algorithm' as ALGORITHM, CREATED_TIMESTAMP, REALM_ID from USER_ENTITY U, CREDENTIAL C where U.ID = C.USER_ID AND U.REALM_ID = 'RealmID' ``` Export the result as CSV or JSON for easier transformation. ## Importing Users into Descope Use the **Descope Management API** or **Batch Create Users API** to import users. Refer to the [Custom Data Store Migration Guide](/migrate/custom) for detailed JSON formatting and password import examples. Example import payload: ```json { "users": [ { "loginIds": ["alice"], "email": "alice@example.com", "name": "Alice Johnson", "roleNames": ["admin"], "userTenants": ["acme-corp"], "customAttributes": { "source": "keycloak", "department": "engineering" }, "hashedPassword": { "pbkdf2": { "hash": "base64string", "salt": "base64string", "iterations": 27500, "type": "sha256" } } } ] } ``` ### Password Migration If you would like to move fully to passwordless authentication, as long as your users have a verified email address associated with their account, you can skip this step. Keycloak's default password hashing algorithm is **PBKDF2-SHA256** (≈ 27,500 iterations). You can confirm the algorithm from the `CREDENTIAL` table or export file. Descope supports a variety of password hashing algorithms. You can see the full list of supported algorithms [here](/migrate#supported-password-hashing-algorithms). If you export the `hash`, `salt`, and `iterations`, you can import passwords so users don't need to reset them. Otherwise, you'll need to trigger a password reset for the user through Descope Flows post-migration. ## Migrating Roles and Tenants Keycloak Roles map directly to Descope **Roles**, and Groups or organizations in Keycloak should become **Tenants** in Descope. It's worth noting that users in Descope **can** belong to multiple tenants at the same time. ```json "userTenants": ["engineering", "marketing"] ``` ## Migrating Identity Providers If your realm has connected IdPs (e.g., Google, GitHub, Azure AD): - **OIDC IdPs** → Recreate as [Custom OAuth Providers](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider) or [Tenant OIDC SSO Connections](/auth-methods/sso/oidc) - **SAML IdPs** → Recreate as [Tenant SAML SSO Connections](/auth-methods/sso/saml) Specifically with OIDC, reusing client IDs, secrets, and redirect URIs ensures seamless continuity. You'll have to add the Descope Redirect URI to the IdP's list of authorized callback URLs, but other than that, you should be able to reuse the same IdP configuration. Whether you use a Custom OAuth Provider or a Tenant OIDC SSO Connection, is dependent on whether or not all users or just specific Keycloak users (such as part of a group or organization) should be able to use the IdP to login to Descope. Tenant SSO connections are usually reserved for a specific group of users that exist as/within a Descope Tenant. With SAML, if you do not want to have to re-configure your IdP to use the Descope SSO URL and Entity ID, you can refer to our [SSO Migration Guide](/migrate/sso) for more information. If user emails differ between the IdP and Descope, a new user may be created during first login. Use IdP subject identifiers when available for linking. ## Just-in-Time (JIT) Provisioning Migration with Password Validation This method allows Descope to create users dynamically as they log in — **validating their credentials through Keycloak in real-time**. This is ideal when you **don't have access to password hashes**, but still want a smooth, password-based transition. This JIT method only works if all users belong to a single Keycloak realm, or if you can reliably determine which realm to authenticate against. If users span multiple realms and you can't infer their realm from input, this approach may not be viable. ### How It Works 1. A user enters their email/username and password in a Descope Flow. 2. Descope calls your backend webhook. 3. Your backend sends a `POST` request to Keycloak's token endpoint for that realm: ```bash POST https:///realms//protocol/openid-connect/token Content-Type: application/x-www-form-urlencoded grant_type=password client_id= client_secret= username= password= ``` 4. If Keycloak responds `200 OK`, the credentials are valid. 5. Descope creates a user in your project using the Management API with **the same password**. 6. The user is now fully migrated and can authenticate directly through Descope going forward. ### Requirements - **Direct Access Grants** (a.k.a. Resource Owner Password Credentials flow) must be **enabled** for your Keycloak client. - Keycloak must be reachable by your backend. ### Security and Limitations - The password grant flow is deprecated for public clients — only use this for **temporary migration**. - Protect your client credentials and restrict access to this endpoint. - Disable the direct-grant flow once all users have migrated. ### Integration Flow Example Below is an example flow for migrating users JIT with password validation: ![keycloak jit migration flow example](/assets/keycloak-jit-migration-flow-example.webp) 1. User submits credentials. 2. Descope Flow triggers your Generic HTTP Connector POST request to Keycloak's token endpoint. 3. Keycloak will validate the credentials and return a token if successful, with any additional user attributes as well. 4. On success, Descope will invoke the `Sign Up / Password` action and create a new user with pre-existing password. We can also set custom attributes for the user here if we want. 5. After the migration, future logins for this user will happen directly with Descope, not invoking the connector anymore. Your flow might authenticate the user before storing their password, for example with a magic link or OTP once Keycloak validates the credentials. The user then already exists in Descope, and `Sign Up / Password` will fail. Use the **Update Password** action for that case; it requires an authenticated user, which the magic link or OTP step provides. Either action reads the password from the **Login Password** or the **New Password** [screen component](/flows/screens/inputs/passwords#using-it-to-set-a-password). **Login Password** is the better fit for a migration screen, where the user is typing a password they already have rather than choosing a new one. The password must satisfy your project's [password policy](/auth-methods/passwords/settings) regardless of which component collected it. Where your Keycloak realm's policy was more permissive, some users will fail this step, so either match the policies during the migration window or route the failure to a password reset. ### Transitioning Away from Keycloak After the majority of users have logged in once (and thus migrated): - Disable the Keycloak connection. - Switch your apps to use Descope SDKs directly. - Remove the password grant configuration in Keycloak. ## Post-Migration: Handling Freshly Migrated Users A common strategy when migrating users from Keycloak to Descope is to set a custom attribute on migrated users to track their migration status and customize their first login experience. This approach works for **both full migration and JIT migration** scenarios. ### Setting the Migration Attribute **For Full Migration:** When you import users using the Management SDK or API, include a custom attribute like `freshlyMigrated: true` in the user payload: ```json { "loginIds": ["alice"], "email": "alice@example.com", "customAttributes": { "freshlyMigrated": "true", "source": "keycloak" } } ``` **For JIT Migration:** In your migration flow, after successfully validating credentials with Keycloak and creating the user, use the `Update User / Attributes` action to set the `freshlyMigrated` attribute to `true`. ### Customizing Behavior for Migrated Users Once the attribute is set, you can use conditional logic in your Descope flows to provide a tailored onboarding experience for users who just transitioned from Keycloak. For example, you can: - Prompt for MFA enrollment - Ask users to verify or update their email/phone - Request password reset (if passwords weren't migrated) - Show a welcome message explaining the migration ![Example Descope flow using freshlyMigrated attribute](/assets/descope-auth0-migration-guide-example.webp) ### Clearing the Migration Flag Once the user completes the onboarding flow, use the `Update User / Attributes` action to set `freshlyMigrated` to `false`. This ensures they won't see the migration prompts on subsequent logins. ![Setting freshlyMigrated attribute to false in a Descope flow](/assets/descope-auth0-migration-guide-flow.webp) # From Okta CIS (/migrate/okta-cis) This guide covers how to migrate your Okta Customer Identity Solution (CIS) users to Descope. # Okta Customer Identity Solution (CIS) Migration Guide This guide is for customers using **Okta Customer Identity Solution (CIS)**. If you use **Auth0** instead, see the [Auth0 migration guide](/migrate/auth0). Want a faster, guided migration? The **Okta CIS to Descope Migration Skill** in [descope/skills](https://github.com/descope/skills) is a skill for AI agents that interactively walks you through the full migration — user export, attribute mapping, SDK replacement, and more. You can migrate from Okta CIS to Descope in two main ways: - **[Full Migration](#full-migration)** - Export users from Okta (or connect your existing data store) and move them to Descope, then run entirely on Descope. - **[JIT Migration](#jit-migration)** - Provision users in Descope on sign-in or when they use an existing Okta session. Options include verifying the password with Okta (traditional JIT) or [session migration](#session-migration-jit-without-re-login) (JIT without re-login—existing Okta token is exchanged for a Descope session). You can also combine these with **[SSO migration](#sso-migration)** for tenant SSO setups and **[additional considerations](#additional-considerations)** for other migration details. ## Full Migration In a full migration you move your users and identity data to Descope and eventually run only on Descope. Two approaches: - **Export from Okta Users API** - List users via the Okta API, then import into Descope. - **Connect your existing data store** - If Okta sits in front of your own DB (On-prem SCIM Server Agent or Access Gateway), connect that same store to Descope and then sever Okta. ### Users are Owned By Okta (Export from Okta Users API required) Use the Okta [Users API](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/user/other/listusers) to list and export users. This API may be [rate limited](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/user/other/listusers); for large user bases, use pagination and backoff. **Prerequisites** - Access to your Okta CIS tenant with an API token that can list users ([List all users](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/user/other/listusers)) - Your [Descope project ID](https://app.descope.com/settings/project) and a [Descope Management Key](https://app.descope.com/settings/company/managementkeys) **Step 1: Export users from Okta CIS** 1. **Create an API token in Okta** (if you don't have one): - In the [Okta Admin Console](https://help.okta.com/en-us/content/topics/identity-governance/og-overview.htm), go to **Security** → **API** → **Tokens**. - Click **Create Token**, name it (e.g. "Descope Migration"), and copy the token. Store it securely; it won't be shown again. ![Okta Admin Console - Security > API > Tokens](/assets/okta-cis-api-token.webp) 2. **List users via the Okta Users API**: - Call `GET https://${yourOktaDomain}/api/v1/users` with the header `Authorization: SSWS ${api_token}`. - Use query parameters: `limit=200` (max per page) and, for the next page, `after=${userId}` from the `Link` header or the last user's `id`. Repeat until all users are fetched. - See [List all users](https://developer.okta.com/docs/api/openapi/okta-management/management/tags/user/other/listusers) for rate limits and response format. 3. **Map Okta attributes to Descope** - For each user, map: - Okta `profile.login` or `profile.email` → Descope `loginIds` (required; unique per user). - Okta `profile.firstName`, `profile.lastName` → Descope `givenName`, `familyName` or `name`. - Okta `profile.email` → Descope `email`. - Any custom Okta profile fields → Descope [custom attributes](https://app.descope.com/users/attributes) as needed. - Build a JSON array (or CSV) in the format expected by Descope (see [user format guide](/migrate/custom/user-format-json)). **Step 2: Import users into Descope** 1. **Open the Descope Console** - Go to [Users](https://app.descope.com/users) (or use the Management API; see below). 2. **Import via API** (recommended for bulk): - Use the [Create User](/api/management/users/create-user) or [Batch Create User](/api/management/users/batch-create-users) API with your [Management Key](https://app.descope.com/settings/company/managementkeys). - Send the exported JSON (one user per request, or a batch). Set a unique `loginIds` (e.g. email or Okta `login`) per user. - Optionally set a custom attribute `freshlyMigrated` to `true` to be used in your Descope flow to identify users who have been migrated. 3. **Or import via the Console** - If your project supports it, use **Import users** (or equivalent) from the Users page and upload a CSV/JSON that matches the required fields. **Step 3: Verify and cut over** 1. **Verify in Descope** - In the [Descope Users](https://app.descope.com/users) list, confirm that migrated users appear with correct login IDs, emails, and names. 2. **Test sign-in** - Run your application's sign-in flow and confirm that a few migrated users can authenticate. 3. **Confirm import coverage** - Since Okta's Users API doesn't export password hashes, freshly imported users won't have a password set in Descope — checking the `password` filter here won't tell you whether the import succeeded. Instead, run an unfiltered [Search Users API](/api/management/users/search-users) call (or scope it with `fromCreatedTime`) and compare the returned `total` against your expected user count. 4. **Cut over** - Once satisfied, switch your application to use only Descope for authentication and retire Okta CIS for this app. ### Using the Okta On-prem SCIM Server Agent or Access Gateway If you use **Okta On-prem SCIM Server Agent** or **Okta Access Gateway** with a **new Data Store** backed by your own SQL database (MySQL, MS SQL, Oracle, Postgres) or LDAP, your users and their information are already owned by you in that store. You do not need to export from Okta's user API. 1. **Connect the same DB to Descope** - Use an [HTTP Generic Connector](/connectors/connector-configuration-guides/network/generic-http) in your Descope flow to connect to that data store and use it as the originating identity source. Configure the connector to call your store's API or endpoints so that authentication and user lookup are performed against the database or LDAP, and map the response to Descope users and sessions. 2. **Sever the connection to Okta** - Once Descope is successfully authenticating against your data store, remove or disable Okta from the path. Your identity data stays in your DB; only the connection moves from Okta to Descope. ## JIT Migration With **Just-In-Time (JIT) migration**, you do not bulk-export users. Users are provisioned in Descope when they sign in or when they use an existing Okta session. JIT can be **passwordless** (e.g. session migration or email/username lookup plus email verification) or **password-based** (verify the user's password with Okta). Two common approaches are: - **Traditional JIT (password verification)** - Verify the user's password with Okta; if correct, create or link the user in Descope and issue a Descope session. The user must enter credentials again. - **Session migration (JIT without re-login)** - Use the user's existing Okta session token; Descope validates it, provisions the user in Descope just in time, and issues a Descope token. The user only needs to update the app; they do not have to log in again. Session migration is more reliable than traditional JIT for users who already have an active Okta session, as their session will be **automatically migrated** and a Descope token will be issued without re-entering credentials. **Designing your JIT flow** The value of JIT is that **you control the flow**. It is not limited to “collect username and password, then call Okta.” You design the steps in your [Descope flow](/flows) and decide when (and whether) to involve Okta. For example: - **If you have passwords** - - For users new to Descope, you might ask for the password only as a **verification step** (e.g. after they enter email/username, verify with Okta, then create or link the user and issue a Descope session). - You might **look up the user as they type** email or username, then **force verification of the email on file** (e.g. magic link or OTP to that email) instead of asking for a password. - You can combine steps, add conditions, or branch based on whether the user already exists in Descope—so the flow can do a variety of things, not just “take username/password and run the flow.” - **If you go passwordless** - - You can use session migration (existing Okta token) to migrate users who already have an active Okta session. - You can use a flow that looks up the user by email/username and only verifies ownership of the email (e.g. magic link or OTP), then provisions them in Descope without ever asking for a password. The important part is that your backend (or flow) can call the Okta APIs when you need to verify the user, and then create or link the user in Descope and issue a Descope session—according to the path you designed. ### Implementing JIT with Okta APIs (password verification) One way to implement JIT is to verify the user's password with the Okta Authentication API, then provision them in Descope. Example flow: 1. User enters username and password in your app (or in a Descope flow that passes credentials to your backend). 2. Your backend sends the credentials to the Okta [Authentication API](https://developer.okta.com/docs/api/openapi/okta-authn/tag/Authn/) (`/api/v1/authn`) to verify the password. 3. If authentication succeeds, create the user in Descope (if they do not already exist) or link to the existing user, then issue a Descope session (e.g. via [exchange access key](/api/management/users/exchange-user-access-key) or your chosen method). 4. The user is now signed in with Descope; no bulk export is required. **Example request (Okta Authentication API):** ```bash curl -v -X POST \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Authorization: SSWS ${api_token}" \ -H "User-Agent: Mozilla/5.0 (${systemInformation}) ${platform} (${platformDetails}) ${extensions}" \ -H "X-Forwarded-For: 23.235.46.133" \ -d '{ "username": "dade.murphy@example.com", "password": "correcthorsebatterystaple", "options": { "multiOptionalFactorEnroll": false, "warnBeforePasswordExpired": false }, "context": { "deviceToken": "26q43Ak9Eh04p7H6Nnx0m69JqYOrfVBY" } }' "https://${yourOktaDomain}/api/v1/authn" ``` Replace `${yourOktaDomain}` with your Okta domain (e.g. `your-org.okta.com`) and `${api_token}` with a valid [Okta API token](https://developer.okta.com/docs/guides/create-an-api-token/main/). On success, use the response to identify the user and create or link them in Descope, then complete the sign-in with a Descope session. To carry the verified password over so the user can keep signing in with it, store it in Descope as part of the same flow. Use **Sign Up / Password** for users new to Descope, or **Update Password** when the user already exists there, which is the case if your flow signs them in with a magic link or OTP first. Both actions take the password from either the **Login Password** or the **New Password** [screen component](/flows/screens/inputs/passwords#using-it-to-set-a-password); **Login Password** suits a migration screen better, because the user is entering an existing password rather than picking a new one. The password must satisfy your project's [password policy](/auth-methods/passwords/settings) regardless of component collected it. If Okta's policy was more permissive, expect some users to fail at this step. As a workaround, align the policies for the migration window, or send the failure to a password reset path. Storing a password this way is what makes the `password: false` filter under [Tracking migration progress](#tracking-migration-progress) a meaningful signal for these users. ### Session migration (JIT without re-login) **Session migration** is a form of JIT: the user must **update** to a new version of your web or mobile app, but they **do not have to log in again**. The app sends their existing Okta session token to Descope; Descope validates it, pulls the user's information from Okta CIS, provisions the user in Descope just in time, and **issues a Descope token**. The user's session is **automatically migrated**—no password re-entry, no interruption. To roll this out, deploy a new version of your app that uses the Descope SDK with session migration. Users who already have a valid Okta session are migrated when they open the updated app; their pre-existing Okta token is exchanged for a Descope session and their user record is created or updated in Descope as needed. See the [Session Migration](/migrate/session-migration) guide for setup, prerequisites, and SDK usage (React, NextJS, WebJS, Kotlin, Swift). ### Tracking migration progress Since JIT migration happens gradually as users sign in, you'll want a way to see how many users have completed migration and how many are still pending. This is especially relevant for Okta CIS, since Okta's Users API does not export password hashes — passwords can only be captured as users authenticate. You can use the `password` filter on the [Search Users API](/api/management/users/search-users) (or the SDK's [`searchAll`](/management/user-management/sdks#search-users) function) to check migration coverage: - **`password: true`** - Returns users who already have a password set in Descope (migrated). - **`password: false`** - Returns users who do not yet have a password set in Descope (still pending migration). This filter only reflects **password-based** JIT. Users migrated through [session migration](#session-migration-jit-without-re-login) or any passwordless path (magic link, OTP, social login) never get a password set in Descope, so they'll always show up under `password: false` even after they've fully migrated. If your JIT flow mixes password-based and passwordless paths, don't rely on `password: false` alone to measure overall completion — pair it with another signal, such as `fromCreatedTime`/`fromModifiedTime` or a custom attribute you set on successful migration. **Example: Find users still pending migration** ```bash curl -i -X POST \ __BaseURL__/v2/mgmt/user/search \ -H 'Authorization: Bearer __ProjectID__:' \ -H 'Content-Type: application/json' \ -d '{"password": false}' ``` Compare the `total` field returned for a `password: true` search against a `password: false` search to track how many users remain. This is a native alternative to manually tracking migration status with a custom attribute like `freshlyMigrated`. ## SSO migration If your Okta CIS setup includes SSO (SAML/OIDC) for tenants or organizations, you can migrate those SSO configurations to Descope without forcing tenant admins to re-configure their IdPs. Descope can consume the existing IdP response (e.g. same ACS URL) and complete authentication so that end users keep a seamless experience. For the full process—implementing SSO with Descope, setting up tenants, DNS redirect, and testing—see [SSO Migration](/migrate/sso). ## Additional considerations - **Passkeys and TOTP cannot be migrated** - With Okta CIS, you cannot migrate passkeys or TOTP seeds to Descope. Users who had passkeys or authenticator apps (TOTP) in Okta will need to **reprovision** them in Descope (e.g. enroll a new passkey or set up TOTP again in your Descope flow after migration). - **JIT provisioning and user attributes** - With JIT provisioning (password verification or [session migration](#session-migration-jit-without-re-login)), you control how users are created or updated in Descope. Configure whatever user attributes you need from Okta (or your IdP) and **map them to Descope user attributes** accordingly when you create or update the user via the Management API or session-migration flow. ### Backend Session Validation Strategy Whether you do a **full migration** or a **JIT migration**, plan for a period when some users have Descope sessions and others still have Okta sessions. Your backend should support **both** token types during the transition: - **Validate Descope OAuth tokens** for users who have already migrated or signed in via Descope. - **Validate Okta JWTs** for users who still have active Okta sessions. When a request arrives, inspect the token (e.g. issuer or `kid`) to determine the provider, then validate accordingly. This lets you roll out the migration gradually without forcing everyone to re-authenticate at once. For more information on this implementation pattern, see the docs [here](/migrate/session-migration#step-1-dual-token-validation-in-your-backend). # From Ping (/migrate/ping) This guide will cover how to migrate your Ping users to Descope. # Ping Migration Guide This guide is designed to help customers migrate from PingOne to Descope. Descope’s migration tool streamlines the process of moving users, tenants, and roles between identity providers (IdPs), ensuring a smooth and consistent transition. Because PingOne uses separate APIs for each environment, performing a full migration is the most efficient and reliable approach. The provided migration script leverages the Descope SDK to transfer users, tenants, roles, and permissions from PingOne into Descope, ensuring that users are correctly associated with their respective tenants and roles in the new system. ## Full Migration ### Prerequisites Ensure you have the following before starting: - Access to your Ping Admin Console - Access to your Descope Console ### 1. Importing from Ping #### Creating a Ping Worker Application Follow these steps to set up a Ping Worker App, which you will need to complete the migration: 1. You will need to create a Worker app inside of your PingOne admin console. To do so, navigate to Applications --> Applications. ![Ping Application Console Home View](/assets/ping-application-console-home.webp) 2. Click the **+** icon to create a new application, and select **Worker** as the application type. ![Ping Application Create App](/assets/ping-console-create-app.webp) 3. In the Roles tab of your new application, select **Grant Roles**, and assign the **Organization Admin** and **Environment Admin** roles both at the Organization level. ![Ping Application Add Org Admin Role](/assets/ping-console-assign-org-admin.webp) ![Ping Application Add Env Admin Role](/assets/ping-console-assign-env-admin.webp) The Roles tab of your application should now look like this: ![Ping Application Roles Tab](/assets/ping-console-app-role.webp) For the next section, refer to the **Overview** tab of your application: ![Ping Application Overview](/assets/ping-console-app-overview.webp) #### Configure Local Environment You will need the following to set up your environment for migration: 1. Client ID assigned to your Worker app, found under the **Overview** tab of that application. 2. Client Secret assigned to your Worker app, found under the **Overview** tab of that application. 3. Environment ID of the Ping environment that contains the admin Worker application you created previously, found under the **Overview** tab of that application. 4. PingOne API authentication path for your particular geographical region. As an example, the PingOne top-level domain for the United States is `https://auth.pingone.com/v1`. For information on top-level domains for other regions, visit [this doc](https://apidocs.pingidentity.com/pingone/platform/v1/api/#working-with-pingone-apis) 5. Descope Project ID, which can be found [here](https://app.descope.com/settings/project). 6. Descope Management Key, you can create one [here](https://app.descope.com/settings/company/managementkeys) if needed. #### Required Custom User Attributes Before running the migration, you must manually create the following custom user attributes in the **Descope Console** under **Users →** [Custom Attributes](https://app.descope.com/users/attributes). These attributes are essential for supporting the migration process and ensuring smooth post-migration handling. | Attribute Name | Type | Description | |------------------|---------|-------------| | `freshlyMigrated` | Boolean | Set to `true` during migration to indicate the user was migrated. You can use this flag in Descope Flows to apply conditional logic post-migration. | | `mfaEnabled` | Boolean | Set to `true` if the user had MFA enabled in Ping; otherwise, `false`. | | `userId` | Text | Stores the original `userId` assigned to the user in Ping. | | `populationId` | Text | Stores the ID of the Ping population the user belonged to, if applicable. | | `environmentId` | Text | Stores the ID of the Ping environment the user belonged to, if applicable. | The attribute machine names must match exactly as listed above for the migration tool to function correctly. #### Setting Up the Migration Script 1. Clone the Repo: ```bash git clone git@github.com:descope/descope-migration.git ``` 2. Create a Virtual Environment ```bash python3 -m venv venv source venv/bin/activate ``` 3. Install the Necessary Python libraries ```bash pip3 install -r requirements.txt ``` 4. Setup Your Environment Variables You can change the name of the `.env.example` file to `.env` to use as a template. Then populate with the items generated within the [prerequisites section](#prerequisites) of this guide. ```bash # Required, this is the Client ID of your PingOne Worker app PING_CLIENT_ID= # Required, this is the Client Secret of your PingOne Worker app PING_CLIENT_SECRET= # Required, this is the ID of the PingOne environment that contains your Worker app PING_ENVIRONMENT_ID= # Required, this is the top-level API domain for your geographical region PING_API_PATH= # Required, this is your Descope Project ID DESCOPE_PROJECT_ID= # Required, this is your Descope Management Key DESCOPE_MANAGEMENT_KEY= ``` The migration tool source code can be found on [GitHub](https://github.com/descope/descope-migration) #### Running the Migration Script You can use the `-v` or `--verbose` flags to enable more detailed output. This works for both live and dry runs, providing you with additional information. **Dry Run** You can dry run the migration script which will allow you to see the number of users, tenants, roles, etc which will be migrated from Ping to Descope. ```bash python3 src/main.py ping --dry-run ``` The output would appear similar to the following: ```bash Would migrate 112 users from PingOne to Descope Would migrate 2 roles from PingOne to Descope Would migrate MyNewRole with 2 associated permissions. Would migrate Role with 0 associated permissions. Would migrate 2 environments from PingOne to Descope tenants. Would migrate Tenant 1 with 5 associated users. Would migrate Tenant 2 with 4 associated users. ``` **Live Run** To live migrate your Ping users, follow the below example. ```bash python3 src/main.py ping ``` The output will include the responses of the created users, organizations, roles, and permissions as well as the mapping between the various objects within Descope. A log file will also be generated in the format of `migration_log_ping_%d_%m_%Y_%H:%M:%S.log`. Any items which failed to be migrated will also be listed with the error that occurred during the migration. ```bash Starting migration of 112 users found via PingOne API Starting migration of 2 roles found via PingOne API Starting migration of MyNewRole with 2 associated permissions. Starting migration of Role with 0 associated permissions. =================== User Migration ============================= PingOne Users found via API 112 Successfully migrated 110 users Successfully merged 2 users Users migrated, but disabled due to one of the merged accounts being disabled 1 Users disabled due to one of the merged accounts being disabled Failed to migrate 2 Users which failed to migrate: facebook|122094272078100956 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} facebook|10226222057950897 Reason: {"errorCode":"E011002","errorDescription":"Request is missing required arguments","errorMessage":"Missing email or phone","message":"Missing email or phone"} Created users within Descope 108 =================== Role Migration ============================= PingOne Roles found via API 2 Successfully migrated 2 roles Created roles within Descope 2 =================== Permission Migration ======================= PingOne Permissions found via API 2 Successfully migrated 2 permissions Created permissions within Descope 2 =================== User/Role Mapping ========================== Successfully role and user mapping Mapped 1 user to MyNewRole Mapped 2 user to Role =================== Tenant Migration =========================== PingOne environments found via API 2 Successfully migrated 2 tenants =================== User/Tenant Mapping ======================== Successfully tenant and user mapping Associated 5 users with tenant: Tenant 1 Associated 4 users with tenant: Tenant 2 ``` ### 2. Password Migration PingOne does not support exporting of hashed passwords. As a result, you have two options when migrating to Descope, both of which can be handled through Descope Flows. 1. **Require Password Reset on First Login** You can configure a Flow to prompt users to reset their password the first time they sign in through Descope. After resetting, users will authenticate using their newly set password for future logins. Learn more on our [Flows](https://docs.descope.com/flows) page. 2. **Move to Passwordless Authentication** Alternatively, you can adopt a fully passwordless approach. Descope supports a variety of passwordless authentication methods. Explore them on our [Authentication Methods](https://docs.descope.com/auth-methods) page. This option should only be used if you have a verified email address for all users. Otherwise, we recommend enforcing a one time password reset for your users. ### 3. Merging Identities Across Environments If a user has multiple identities across distinct Ping environments, you can use Descope’s multi-tenancy capabilities to consolidate these identities into a single user identity. During migration, the script will: - Assign all associated loginId values to the consolidated Descope user. - Map the unified user to multiple tenants, preserving the roles and permissions the user holds within each tenant. As a note, if a user has roles assigned in a Ping environment but does not exist in that environment’s user directory, those role relationships will not be migrated to Descope, as they cannot be mapped to an existing user. ![An example of user multitenancy](/assets/ping-migration-multitenancy.webp) ## Post Migration Verification Once the migration tool has ran successfully, you can review the migrated items from Ping in the Descope Console: - [Users](https://app.descope.com/users), - [Roles](https://app.descope.com/authorization/rbac) - [Tenants](https://app.descope.com/tenants) Be sure to verify the created records against the migration tool’s output. Next, you can define your users’ login experience. By leveraging the freshlyMigrated custom user attribute, you can create conditional paths within your Descope Flows. ![An example of using the freshlyMigrated attribute within a Descope flow conditional](/assets/descope-ping-migration-guide-example.webp) From this conditional, you can guide users through different experiences — for example, verifying their email or phone number, setting a password (or requiring a password update if passwords were migrated), or enabling passkeys. Once you’ve finalized the user experience, remember to update the user’s profile by setting the freshlyMigrated attribute to false. ![An example of setting the freshlyMigrated attribute to false within flows](/assets/descope-ping-migration-guide-flow.webp) # Session Migration (/migrate/session-migration) Seamlessly migrate active user sessions from existing authentication providers to Descope without requiring re-authentication. # Session Migration You can use **session migration** when moving from **Auth0**, **Okta** (including Okta Customer Identity Solution), or a custom-built solution to Descope. Migrating should not require users to re-authenticate. In many applications, especially those involving mobile, desktop, or smart devices, logging a user out during a migration introduces unnecessary friction and disrupts the user experience. Descope supports **session migration** for these providers: your client sends a session token from your existing provider (Auth0, Okta, or another) to Descope; Descope verifies it and issues a new Descope session token for the same user. Users continue their sessions without noticing any change in the underlying authentication system. ## How It Works With Auth0, Okta, or another token-issuing provider: 1. A user makes a request with an active session token from your existing authentication provider (e.g. an Auth0 or Okta access token or JWT). 2. Your frontend will use the Descope SDK to send the token to Descope for validation. 3. Upon successful validation, Descope will extract the unique user identifier (e.g., email or user ID). 4. Based on that user identifier, Descope will issue a new session token for that user. 5. The new Descope token is returned to the client, replacing the previous session. ## Prerequisites Currently, session migration is in beta and is only supported with users who have already been imported into Descope. Just in time user creation is not supported at this time. Before implementing session migration, ensure the following: * All users who authenticate using session migration **must** already exist in Descope. * Your backend is capable of validating tokens from Descope, as well as your existing authentication provider. ## Step-by-Step Implementation ### Step 1. Dual Token Validation in Your Backend During the migration period, it is recommended for your backend to be capable of validating both legacy and Descope tokens. This is a transitional strategy that allows you to gradually move users to Descope while maintaining your existing authentication as a fallback. A typical implementation involves checking the token's issuer and key ID (`kid`) to determine the provider, then validating it accordingly. **Example high-level logic:** ```ts function handleRequest(req) { const token = extractToken(req); // First try to validate as a Descope token if (isDescopeToken(token)) { validateDescopeToken(token); return continueRequest(); } // If not a Descope token, try legacy token validation if (isLegacyToken(token)) { const userId = validateLegacyToken(token); const descopeToken = issueDescopeToken(userId); return respondWithToken(descopeToken); } // If neither token is valid, proceed with normal authentication flow return proceedWithNormalAuth(); } ``` You can use your legacy provider's SDK to validate legacy tokens—for example, **Auth0**'s `jsonwebtoken` or **Okta**'s JWT verifier. This approach ensures a smooth transition by: 1. Prioritizing Descope tokens for users who have already migrated 2. Converting legacy tokens to Descope tokens when encountered 3. Falling back to normal authentication for users who haven't migrated yet ### Step 2. Generate Descope Session Token This action requires the user to already exist in Descope. If the user is not present, the API will return an error. To implement session migration on the client side, use the `getExternalToken` prop in the Descope AuthProvider component. This function should return a valid token from your external provider (e.g. Auth0 or Okta), which the SDK will use to authenticate the user with Descope. ```sh title="Terminal" npm i --save @descope/react-sdk ``` ```tsx import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( { // Return current Auth0 or Okta session/access token return 'my-external-token'; }} > ); }; ``` ```sh title="Terminal" npm i --save @descope/nextjs-sdk ``` ```tsx import { AuthProvider } from '@descope/nextjs-sdk'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( { // Return current Auth0 or Okta session/access token return 'my-external-token'; }} > {children} ); } ``` ```sh title="Terminal" npm i --save @descope/web-js-sdk ``` ```js import descopeSdk from '@descope/web-js-sdk'; const sdk = descopeSdk({ projectId: 'my-project-id', persistTokens: true, autoRefresh: true, // Pass this function to migrate session from Auth0, Okta, or another provider to Descope. getExternalToken: async () => { // Return current Auth0 or Okta session/access token return 'my-external-token'; }, }); ``` ```kotlin suspend fun migrateUserAuthentication() { // Assume we have an active login from an authentication that was done with another auth provider val externalToken = otherAuthProvider.authToken ?: return // Exchange the external token and get a Descope authentication in return val authResponse = Descope.auth.migrateSession(externalToken) // We now have an AuthenticationResponse as if the user went through a Descope sign-in call val session = DescopeSession(authResponse) Descope.sessionManager.manageSession(session) } ``` ```swift func migrateUserAuthentication() async throws { // Assume we have an active login from an authentication that was done with another auth provider guard let externalToken = otherAuthProvider.authToken else { return } // Exchange the external token and get a Descope authentication in return let authResponse = try await Descope.auth.migrateSession(externalToken: externalToken) // We now have an AuthenticationResponse as if the user went through a Descope sign in call let session = DescopeSession(from: authResponse) Descope.sessionManager.manageSession(session) } ``` The SDK will automatically handle the token exchange process, converting the external token into a Descope session token. This allows for a seamless transition without requiring users to re-authenticate. ### Step 3. Populate Descope with Users Prior to Migration Session migration requires that the users already exist in Descope. Descope will not automatically provision users during this process. #### Options for populating users: * **Bulk import**: Use the [User Import Tool](/migrate/custom#example) or Management API to import your existing user base. If you're migrating from **Auth0**, see the [Auth0 migration guide](/migrate/auth0); from **Okta CIS**, see the [Okta CIS migration guide](/migrate/okta-cis). * **Automated sync**: * **Auth0**: Use [Auth0 Actions](https://auth0.com/docs/customize/actions) to create users in Descope during login or registration events. * **Okta**: Use [Okta Workflows](https://help.okta.com) or SCIM provisioning to push users into Descope. * **Manual provisioning**: For static user lists or low-volume use cases. It is strongly recommended to automate user creation in Descope for any new accounts created between your initial export and the session migration cutover. ### Handling the New Descope Token If you're using Descope's client SDKs (React, Next.js, WebJS, Kotlin, Swift), the SDK will automatically handle session management for you after successful session migration. The SDK will automatically store and manage the new Descope tokens, so you don't need to manually handle token storage or session management - it works just like completing a flow or logging in via any other SDK authentication method. ### Finalizing the Migration Once your application is stable and all users are actively receiving Descope tokens, you can: * Remove legacy token validation logic from your backend. * Fully enforce Descope-issued session tokens across all endpoints. * Revoke or expire legacy sessions if needed. ## Testing and Verification Before launching session migration in production: * Test the flow using both legacy and Descope tokens across various clients. * Verify that session tokens are issued with expected claims and expiration times. ## Monitoring Session Migration Descope logs audit events for both successful and failed migrations: `ExternalSessionMigrationSuccess` when a Descope session is issued, and `ExternalSessionMigrationFailure` when the exchange fails. The failure event's **Data** section includes `error_message` along with the external provider and user attributes Descope attempted to match. Because session migration does not provision users, a valid external token will still fail the exchange if the user has not yet been imported into Descope. See [Session Migration Audit Detail](/audit-trails-and-integrations#session-migration-audit-detail) for the fields logged on failure. ## Limitations * Descope does not support JIT user creation during session migration. You must pre-provision users. * Session migration does not carry over legacy tokens' claims (e.g., roles, groups). You must recreate any needed claims in Descope's user profile or token generation logic. For this, you can use a [JWT Template](/management/token/jwt-templates). * This migration flow only applies to token-based sessions (JWTs, access tokens) and may not work with opaque session cookies unless decoded by your legacy provider. ## Summary Session migration enables a seamless transition to Descope by allowing your backend to recognize and convert existing sessions from **Auth0**, **Okta**, or other authentication providers. This is especially critical for high-availability or consumer-facing applications where user disruption must be minimized. By properly preparing your backend and provisioning your user base in advance, you can switch from Auth0, Okta, or a custom provider to Descope with zero impact on the end-user experience. This strategy is commonly used in mobile and enterprise scenarios, where maintaining session continuity is essential. # SSO & SCIM Migration (/migrate/sso) Migrate SSO authentication and SCIM provisioning to Descope from any provider without requiring tenants to reconfigure their IdP. # SSO & SCIM Migration Seamless SSO and SCIM migration is only supported for Growth and Enterprise customers. See the Descope [pricing page](https://www.descope.com/pricing) for more information. If you have a previous SSO or SCIM setup with a different authentication provider or a home-grown solution, usually, the tenant's IT management team is forced to re-configure the setup within their IdP to match the new provider, repointing the SAML/OIDC app, or the SCIM base URL and token. This can cause a lot of friction and unnecessary time consumption, especially when this process requires reaching out to the customers, changing, and testing the authentication or provisioning. To prevent this friction, Descope supports the ability to consume and eventually migrate the current customer's setup, for both authentication and provisioning. This creates a totally seamless experience for your end users, who keep signing in through their pre-configured IdPs, without forcing your tenant's IT admins to re-configure their SAML/OIDC or SCIM settings on their end at all. In this article, we will cover all of the steps required to eventually migrate your customer tenants to Descope: first for SSO, then for SCIM, since both are handled by the same Cloudflare Worker. ## Solution Overview The following chart demonstrates your current implementation for Single-Sign-On: ![Old SP Setup](/assets/old-sp-sso-migration.webp) And this chart, demonstrates the implementation, post migration: ![Descope Migration Solution Overview](/assets/new-sp-sso-migration.webp) 1. When the end user starts the SSO authentication, a Descope relay state will be created. 2. Once the user is redirected to the IdP, authentication happens as usual. 3. Once the authentication is complete, the IdP response returns to the same SP ACS URL the customer had set previously in the IdP's settings. 4. Using a DNS provider, the response will be redirected to Descope, passing all the needed parameters to complete the authentication. 5. Descope will handle the final response and authenticate the user. 6. The user will be authenticated and logged in. ## Prerequisites * A Descope project with Descope API/SDK access. * A custom domain set up. * A DNS provider. * If you're also migrating SCIM provisioning, for each tenant being migrated: * A [SCIM access key](/management/tenant-management/scim#creating-scim-access-keys) * The connection ID, tenant slug, or org ID segment the previous provider used in its SCIM URLs for that tenant (usually visible in the SCIM app configuration on the IdP side) ## Setup Process Setup overview: 1. Implementing SSO authentication with Descope Flows / SDK 2. Setting up the Descope tenants with the SSO settings 3. Setting up the DNS redirect (and, optionally, SCIM provisioning) 4. Testing the Integration 5. Moving Forward With Descope ### Implementing SSO authentication with Descope Flows / SDK. First, SSO with Descope should be implemented in the application. * [Authenticate with SSO Using Flows](/auth-methods/sso/with-flows). * Authenticate with SSO Using SDK - [client side](/auth-methods/sso/with-sdks/client), [server-side](/auth-methods/sso/with-sdks/backend) or [mobile](/auth-methods/sso/with-sdks/mobile). Not sure? Follow this [guide](/auth-methods/sso) to get a general perspective on the implementation. To ensure a smooth migration, with the ability to "A/B test" and rollback in case things go wrong, the authentication with Descope should be set up with a specific and customized logic that will ensure only specific tenants (customers) will use Descope. Here is a simple example (using `React`): ```javascript function login (tenant) { if (migrationTenants.includes(tenant)) {
} else { // old SP logic } } ``` ### Setting up the Descope tenants with the SSO settings After making sure that the authentication works, set up the customer tenants. Set up a Descope tenant for each migrating customer. Follow the instructions below to match the required protocol. #### SAML 1. Create the tenants. * Using the [UI](https://app.descope.com/tenants) * Using the [API](/api/management/tenants/create-tenant) * Using the [SDK](/management/tenant-management/sdks) 2. Acquire the settings from the previous identity provider. These settings are tenant (customer) specific and should match the same values your customer had provided for the previous provider. The parameters should match exactly what the customer had set, including fields that are not listed above. To allow Descope to accept and communicate with the customer's IdP, the default Descope 'EntityId' and 'ACS URL' need to be changed. These values, referred to commonly below as `spEntityId` and `spAcsUrl`, should correspond to what is already set in the customer's IdP. Use the API or SDK to set the following values: ```json curl -X POST "__BaseURL__/v1/mgmt/sso/saml" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "tenantId": "xxxxx", "settings": { ... "spACSUrl": "https://mydomain.com/saml", "spEntityId": "my-custom-entityid" }, "domains": [ "mydomain.com" ] }' ``` ```javascript const tenantId = "xxxxx"; const manualSettings = { idpUrl: "https://url.com", idpCert: "cert", entityId: "entity", spAcsUrl: "https://domain.com/saml", spEntityId: "my-entity-id" } const metadataSettings = { idpMetadataUrl: "URL", spAcsUrl: "https://domain.com/saml", spEntityId: "my-entity-id" } // Manual const resp = await descopeClient.management.sso.configureSAMLSettings(tenantId, manualSettings); // Metadata URL const resp = await descopeClient.management.sso.configureSAMLByMetadata(tenantId, metadataSettings); ``` ```go // Load all tenant SSO settings ssoSettings, err := descopeClient.Management.SSO().LoadSettings(context.Background(), "tenant-id") // Configure tenant SSO by SAML settings tenantID := "tenant-id" // Which tenant this configuration is for idpURL := "https://idp.com" entityID := "my-idp-entity-id" idpCert := "" spAcsUrl := "https://domain.com/saml" spEntityId := "my-entity-id" redirectURL := "https://my-app.com/handle-saml" // Global redirect URL for SSO/SAML domain := "domain.com" // Users logging in from this domain will be logged in to this tenant samlSettings := &descope.SSOSAMLSettings{ IdpURL: idpURL, IdpEntityID: entityID, IdpCert: idpCert, AttributeMapping: &descope.AttributeMapping{Email: "myEmail", ..}, RoleMappings: []*RoleMapping{{..}}, SpAcsUrl: spAcsUrl, SpEntityId: spEntityId } err = descopeClient.Management.SSO().ConfigureSAMLSettings(context.Background(), tenantID, samlSettings, redirectURL, domain) //* Deprecated (use ConfigureSAMLSettings(..) instead) *// err := descopeClient.Management.SSO().ConfigureSettings(context.Background(), tenantID, idpURL, entityID, idpCert, redirectURL, domain) // Alternatively, configure using an SSO SAML metadata URL samlSettings := &descope.SSOSAMLSettingsByMetadata{ IdpMetadataURL: "https://idp.com/my-idp-metadata", AttributeMapping: &descope.AttributeMapping{Email: "myEmail", ..}, RoleMappings: []*RoleMapping{{..}}, SpAcsUrl: spAcsUrl, SpEntityId: spEntityId } err = descopeClient.Management.SSO().ConfigureSAMLSettingsByMetadata(context.Background(), tenantID, samlSettings, redirectURL, domain) ``` ```python # You can load all tenant SSO settings sso_settings_res = descope_client.mgmt.sso.load_settings("tenant-id") # You can Configure SSO SAML settings for a tenant manually. settings = SSOSAMLSettings( idp_url="https://dummy.com/saml", idp_entity_id="entity1234", idp_cert="my certificate", attribute_mapping=AttributeMapping( name="name", given_name="givenName", middle_name="middleName", family_name="familyName", picture="picture", email="email", phone_number="phoneNumber", group="groups" ), role_mappings=[RoleMapping(groups=["grp1"], role="rl1")], sp_acs_url="https://domain.com/saml", sp_entity_id="my-entity-id" ) descope_client.mgmt.sso.configure_saml_settings( tenant_id, # Which tenant this configuration is for settings, # The SAML settings redirect_url="https://your.domain.com", # Global redirection after successful authentication domains=["tenant-users.com"] # Users authentication with these domains will be logged in to this tenant ) # You can Configure SSO SAML settings for a tenant by fetching them from an IDP metadata URL. settings = SSOSAMLSettingsByMetadata( idp_metadata_url="https://dummy.com/metadata", attribute_mapping=AttributeMapping( name="myName", given_name="givenName", middle_name="middleName", family_name="familyName", picture="picture", email="email", phone_number="phoneNumber", group="groups" ), role_mappings=[RoleMapping(groups=["grp1"], role="rl1")], sp_acs_url="https://domain.com/saml", sp_entity_id="my-entity-id" ) descope_client.mgmt.sso.configure_saml_settings_by_metadata( tenant_id, # Which tenant this configuration is for settings, # The SAML settings redirect_url="https://your.domain.com", # Global redirection after successful authentication domains=["tenant-users.com"] # Users authentication with these domains will be logged in to this tenant ) ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // You can get SSO settings for a specific tenant ID try { SSOSettingsResponse resp = ss.loadSettings("tenant-id"); } catch (DescopeException de) { // Handle the error } // Configure SSO - SAML String tenantId = "tenant-id"; // Which tenant this configuration is for String spACSUrl = "https://mydomain.com/saml"; String spEntityId = "custom-entity-id"; .... List domains = Arrays.asList("domain.com"); // Users logging in from this domain will be logged in to this tenant // Using Manual Configuration SSOSAMLSettings manualSettings = new SSOSAMLSettings(....., spACSUrl, spEntityId); try { ss.configureSAMLSettings(tenantId, manualSettings, domains); } catch (DescopeException de) { // Handle the error } // Using Metadata URL SSOSAMLSettingsByMetadata metadataSettings = new SSOSAMLSettingsByMetadata(....., spACSUrl, spEntityId); try { ss.configureSAMLSettingsByMetadata(tenantId, metadataSettings, domains); } catch (DescopeException de) { // Handle the error } ``` 3. Verify the settings by viewing the created [tenants](https://app.descope.com/tenants) -> Select the tenant -> Authetication Methods -> SSO. Or by using the [API](/api/management/tenants/sso/load-sso-settings). #### OIDC 1. Create the tenants. * Using the [UI](https://app.descope.com/tenants) * Using the [API](/api/management/tenants/create-tenant) * Using the [SDK](/management/tenant-management/sdks) 2. Acquire the settings from the previous identity provider. These settings are tenant (customer) specific and should match the same values your customer had provided for the previous provider. The parameters should match exactly what the customer had set, including fields that are not listed above. 3. Set up the SSO OIDC settings, by following this [guide](/auth-methods/sso/oidc). 4. Verify the settings by viewing the created [tenants](https://app.descope.com/tenants) -> Select the tenant -> Authetication Methods -> SSO. Or by using the [API](/api/management/tenants/sso/load-sso-settings). ### Setting Up the DNS Redirect After creating the customer tenants, set up the DNS redirect. #### CloudFlare Using CloudFlare as the DNS provider, a "Worker" is needed to process and redirect the requests using custom logic. Follow these instructions to create and deploy a new Cloudflare Worker to handle the redirect. You can have additional logic inside the Worker and customize it to fit your use case better. * Go to https://dash.cloudflare.com/ * Create an Account level token: * Manage Account > Account API Tokens > Create token > "Edit Cloudflare Workers" * Copy the created token and export it as an environment variable locally: ``` export CLOUDFLARE_API_TOKEN= ``` * Clone the worker template, from our official template from [here](https://github.com/descope/cf-sso-redirect-worker). * Update `wrangler.toml` * Line 27 - set your old backend host (old-auth.example.com). * Lines 14-15 to your pattern and zone. * Run and deploy: ``` npm i && npm run deploy ``` #### SCIM Provisioning (Optional) If a tenant also has an existing SCIM provisioning setup with the previous provider, the same worker can proxy that traffic to Descope. The IdP keeps sending SCIM requests to the same base URL and token it always has, while the worker rewrites the path and swaps the token so Descope receives a valid SCIM request for the right tenant. This means the tenant's IT admin never needs to touch the SCIM app configuration on the IdP side. If SCIM provisioning is not yet configured for these tenants in Descope, set that up first: see [SCIM Management](/management/tenant-management/scim) and the IdP-specific guides for [Azure](/management/tenant-management/scim/azure-scim) or [Okta](/management/tenant-management/scim/okta-scim). Alongside `wrangler.toml`, the worker reads a `src/projectConfig.json` file that maps each incoming hostname to a Descope project, with independent `sso` and `scim` blocks: ``` cp src/projectConfig.example.json src/projectConfig.json ``` `src/projectConfig.json` is listed in `.gitignore` because it contains SCIM bearer tokens. Never commit it with real values. ```json { "login.example.com": { "newCname": "auth.example.com", "projectId": "YOUR_DESCOPE_PROJECT_ID", "sso": { "enabled": true, "logOnly": false }, "scim": { "enabled": true, "logOnly": false, "tenants": { "": { "tenantId": "", "token": "Bearer :" } } } } } ``` | Field | Default | Description | |---|---|---| | `enabled` | `false` | Enables SCIM proxying for this hostname. | | `logOnly` | `false` | When `true`, logs the rewrite the worker would have made but forwards the original request unchanged. Use this to validate detection before going live. | | `tenants` | `"*"` | Proxies every request as-is, forwarding the original `Authorization` header unchanged (use only if the previous provider issued one shared token). A map performs a per-tenant token swap, described below. | **Mapping tenants**: for a real migration, `tenants` should be a map, not `"*"`, so a single worker can handle many customer tenants, each with its own Descope SCIM token. Each key must equal the path segment that appears immediately before the SCIM resource type in the incoming URL, regardless of what that segment represented for the previous provider (connection ID, tenant slug, org ID, etc.): ``` /scim/v2/connections/con_OVM407qBECcvwRSG/Users → key is "con_OVM407qBECcvwRSG" /scim/v2/tenants/tenant_xyz/Groups → key is "tenant_xyz" /scim/v2/orgs/org_abc/Users → key is "org_abc" ``` Each entry maps that segment to a Descope tenant and its [SCIM access key](/management/tenant-management/scim#creating-scim-access-keys): ```json "tenants": { "con_OVM407qBECcvwRSG": { "tenantId": "T2abc123", "token": "Bearer :" }, "tenant_xyz": { "tenantId": "T2xyz456", "token": "Bearer :" } } ``` * Requests whose segment **is found** in the map have their `Authorization` header replaced with the mapped Descope token before being forwarded. * Requests whose segment is **not found** in the map are rejected with `401 Unauthorized`. Add every tenant being migrated to the map before enabling SCIM proxying for that hostname. The worker also normalizes the path itself, stripping the provider-specific segment so Descope receives its standard SCIM path: ``` /scim/v2/connections/con_abc/Users/123 → /scim/v2/Users/123 /scim/v2/tenants/tenant_xyz/Groups → /scim/v2/Groups ``` Supported SCIM resource types: `Users`, `Groups`, `Schemas`, `ServiceProviderConfig`, `ResourceTypes`, `Bulk`, `Me`. Before flipping a tenant over, set `logOnly: true` on its `scim` block and trigger a sync from the IdP (or wait for its next cycle). Confirm in the worker logs that the request is matched to the correct tenant and would be rewritten correctly, then set `logOnly: false` to start forwarding for real. ### Testing the Integration After completing the migration steps, test the integration with your own tenants or test accounts. ### Moving Forward With Descope Migration works? here are details on what's next. Once migration is set, it will serve your customer as long as the same values are used or valid. For reasons like a pro-active change of settings or expiry of a certificate, perform a seamless migration to Descope with the [Tenant Self Service Provisioning](/auth-methods/sso). For home-grown implementations, read our documentation about [Tenant Management](/management/tenant-management) to update the settings accordingly. If you also enabled SCIM proxying, confirm requests are arriving by checking the [audit log](https://app.descope.com/audits) for `SCIMEvent` entries scoped to the tenant, as described in [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices#verifying-provisioning-via-audit-logs). Eventually, the users should be able to seamlessly sign in with their respective IdPs, without having to force the administrators to change any settings on their end. # From Stytch (/migrate/stytch) Learn how to migrate Stytch users, organizations, SDKs, sessions, and M2M clients to Descope. # Stytch Migration Guide Want a faster, guided migration? The **Stytch to Descope Migration Skill** in [descope/skills](https://github.com/descope/skills/tree/main/skills/stytch-to-descope) inventories the Stytch features and authentication touchpoints in your application, then produces a migration plan before making code changes. This guide covers a **full migration**, where Descope becomes the identity provider and source of truth for users, organizations, sessions, and machine identities. A complete migration includes four connected parts: - **Identity data** - Consumer Users, B2B Organizations and Members, profile attributes, roles, and permissions. - **Application integration** - Stytch UI and SDKs, protected routes, session validation, refresh, and logout. - **B2B configuration** - Organization-specific authentication settings, SSO, SCIM, and member authorization. - **Machine identities** - M2M clients, client credentials, scopes, token audiences, and secret rotation. You can roll out the migration in phases, but every Stytch dependency must be migrated before you retire the Stytch project and credentials. ## Stytch and Descope Concepts | Stytch | Descope | Migration notes | | --- | --- | --- | | Project and its Test/Live environments | Project per environment | Use separate Descope projects for development, staging, and production. | | Consumer User | [User](/management/user-management) | A project-level identity with one or more login IDs. | | Organization | [Tenant](/management/tenant-management) | One Stytch Organization normally maps to one Descope Tenant. | | Member | User with a tenant association | A Descope User can belong to multiple tenants through `userTenants`, with different roles in each tenant. | | `organization_id` in a Member Session | `dct` and `tenants` claims | `dct` identifies the active tenant. `tenants` contains each tenant's roles and permissions. | | Stytch UI or headless authentication | [Descope Flow](/flows) | A hosted or embedded Flow defines sign-up, sign-in, MFA, recovery, and other authentication journeys. | | Opaque `session_token` and signed `session_jwt` | Signed session and refresh JWTs | Descope uses a session JWT and refresh JWT. The Next.js SDK stores them in the `DS` and `DSR` cookies by default. | | RBAC Resource, Action, Permission, and Role | [Permission and Role](/authorization/role-based-access-control) | Map each `resource_id` and action pair to a stable Descope permission name, then group permissions into roles. | | Organization SSO Connection | Tenant SSO | Recreate each SAML or OIDC connection for the corresponding tenant. | | M2M client and client credentials | [Resource](/resources), confidential [Inbound App](/identity-federation/inbound-apps), and [Policy](/policies) | Preserve OAuth scopes and audiences through the client credentials grant. Use an Access Key only for simpler internal service authentication. | ## Full Migration ### Prerequisites Before exporting data, ensure you have: - A Stytch Project ID and secret with access to the Consumer or B2B resources you need to export. - An inventory of every Stytch environment and whether it uses Consumer Auth, B2B Auth, or both. - An inventory of authentication methods, Organizations, Members, RBAC settings, SSO connections, SCIM directories, M2M clients, webhooks, and application code that reads Stytch IDs or token claims. - A non-production Descope project for testing, plus its [Project ID](https://app.descope.com/settings/project). - A Descope [Management Key](https://app.descope.com/settings/company/managementkeys). Keep it on the server and never expose it to a browser or mobile application. - A cutover and rollback plan, including how you will handle writes that occur between the initial export and cutover. ### 1. Export Data from Stytch Stytch uses different export APIs for [Consumer](https://stytch.com/docs/api-reference/consumer/api/overview) and [B2B](https://stytch.com/docs/api-reference/b2b/api/overview) projects. All three Search APIs use cursor pagination. Request up to 1,000 records per page, save each response, and continue with `results_metadata.next_cursor` until it is empty. Choose the API host that matches your Stytch credentials: | Stytch credentials | API host | | --- | --- | | `project-test-*` and `secret-test-*` | `https://test.stytch.com` | | `project-live-*` and `secret-live-*` | `https://api.stytch.com` | Using Test credentials against the Live host, or Live credentials against the Test host, returns a `project_not_found` error even when the credentials themselves are valid. #### Consumer Auth Use the Stytch [Search Users](https://stytch.com/docs/api-reference/consumer/api/users/search-users) endpoint to export all Consumer Users: ```bash curl --request POST \ --url "${STYTCH_API_URL}/v1/users/search" \ --user "${STYTCH_PROJECT_ID}:${STYTCH_SECRET}" \ --header "Content-Type: application/json" \ --data '{"limit":1000}' ``` Export each User's login identifiers, verification state, name, status, roles, metadata, authentication registrations, and original `user_id`. Stytch also provides a linked [user export utility](https://github.com/stytchauth/stytch-node-export-users) for writing Consumer Users to CSV or JSON. #### B2B Auth Export Organizations before Members because every Member and tenant-scoped role depends on the Organization mapping. | Data | Stytch endpoint | Export details | | --- | --- | --- | | Organizations | [Search Organizations](https://stytch.com/docs/api-reference/b2b/api/organizations/search-organizations) | Save the Organization ID, name, slug, domains, authentication settings, custom roles, SSO references, SCIM reference, and metadata. | | Members | [Search Members](https://stytch.com/docs/api-reference/b2b/api/members/search-members) | Pass one or more exported `organization_ids`. Save the Member ID, Organization ID, email, status, name, roles, verification state, registrations, and metadata. | The Search APIs have different rate limits. Stytch currently documents 150 requests per minute for Consumer Users, 100 requests per second for Organizations, and 100 requests per minute for Members. Process `429` responses with backoff and use the current endpoint documentation as the source of truth when you run the export. #### Password Migration Stytch's Search APIs do not export password hashes. Request a password hash export from [Stytch Support](mailto:support@stytch.com) and verify that its algorithm and parameters match [Descope's supported password formats](/migrate/custom#password-algorithms). If a compatible hash export is unavailable, use one of these options, both of which can be handled through Descope Flows. 1. **Require Password Reset on First Login** You can configure a Flow to prompt users to reset their password the first time they sign in through Descope. After resetting, users will authenticate using their newly set password for future logins. Learn more on our [Flows](https://docs.descope.com/flows) page. 2. **Move to Passwordless Authentication** Alternatively, you can adopt a fully passwordless approach. Descope supports a variety of passwordless authentication methods. Explore them on our [Authentication Methods](https://docs.descope.com/auth-methods) page. This option should only be used if you have a verified email address for all users. Otherwise, we recommend enforcing a one time password reset for your users. ### 2. Map Stytch Data to Descope Create a durable mapping file for every Stytch ID used by your application. Preserve it until application records, webhooks, SSO/SCIM configuration, and operational tools no longer depend on the legacy IDs. | Stytch field or object | Descope field or object | Mapping notes | | --- | --- | --- | | Consumer `emails[].email` or Member `email_address` | `loginIds`, `email` | Use a canonical, case-normalized email as a login ID when email is the primary identifier. | | `emails[].verified` or `email_address_verified` | `verifiedEmail` | Never mark an unverified address as verified during migration. | | `phone_numbers[].phone_number` or `mfa_phone_number` | `loginIds`, `phone` | Normalize phone numbers to E.164 and preserve `verifiedPhone` accurately. | | Consumer `name` or Member `name` | `givenName`, `middleName`, `familyName`, `name` | Preserve the original display name where available. | | User or Member status and lock state | `status` and migration policy | Decide how pending, invited, deleted, and locked records should behave before transforming them. | | `trusted_metadata`, `untrusted_metadata`, and `external_id` | Predefined `customAttributes` or `externalIds` | Define custom attribute keys and types in Descope before importing users. Do not blindly copy untrusted metadata into privileged fields. | | `user_id`, `member_id`, and `organization_id` | Migration mapping file and optional custom attributes | Preserve legacy IDs for traceability and idempotent re-runs; do not use them as human login IDs. | | Organization | Tenant | Reusing the Stytch `organization_id` as the Descope tenant ID can simplify downstream mapping when that ID is suitable for long-term use. | | Member | User plus `userTenants` entry | Associate the User with the mapped tenant and its tenant-scoped role names. | | Organization `custom_roles[].permissions[]` | Tenant-level Roles and Permissions | Convert each Resource and Action pair to a stable permission such as `documents.read`. | Stytch Members are scoped to Organizations, while Descope Users are project-level. If the same verified email represents one person in multiple Stytch Organizations, create one Descope User and aggregate all tenant memberships in `userTenants`. Do not merge records solely because their email strings match: first resolve conflicting profile data, verification state, status, or evidence that the records represent different people. Example transformed B2B user: ```json { "loginIds": ["ada@example.com"], "email": "ada@example.com", "name": "Ada Lovelace", "verifiedEmail": true, "status": "enabled", "userTenants": [ { "tenantId": "organization-live-acme", "roleNames": ["admin"] } ], "customAttributes": { "freshlyMigrated": true } } ``` The optional `freshlyMigrated` attribute lets a Descope Flow route migrated users through password reset, contact verification, or authenticator re-enrollment and clear the flag when the migration journey is complete. ### 3. Import into Descope Test the transformation with representative Consumer Users, single-tenant Members, multi-tenant Members, and disabled or invited records in a non-production project. Then import dependencies in this order: 1. **Permissions** - Create the permission names your application checks. See [RBAC with SDKs](/authorization/role-based-access-control/with-sdks). 2. **Tenants** - Create one tenant for each mapped Stytch Organization. See [Tenant Management](/management/tenant-management/sdks). 3. **Roles** - Create project-level Consumer roles and tenant-level Organization roles after their permissions and tenants exist. 4. **Users and memberships** - Use the [Batch Create Users API](/api/management/users/batch-create-users) or the Management SDK's [`createBatch`](/management/user-management/sdks#batch-create-users) method. Include all mapped tenant IDs and tenant-scoped role names in `userTenants`. Use `createBatch`, not an invitation operation, so the bulk load does not send unexpected invitation emails. Batch requests can partially succeed, so save and inspect `createdUsers`, `failedUsers`, and `additionalErrors` after every request. Correct the underlying data and retry only failed records. See the [user JSON formatting guide](/migrate/custom/user-format-json) for Console imports. Use the API or Management SDK when you need to preserve tenant memberships, roles, password hashes, or precise status values. ### 4. Cut Over 1. Freeze relevant writes in Stytch or keep both systems synchronized during the final migration window. 2. Run a delta export for users, Organizations, Members, roles, and profile changes made after the initial export. 3. Import and verify the delta, then switch the application and API services to Descope. 4. Keep Stytch available for rollback until the [post-migration checks](#post-migration-verification) pass. 5. Disable Stytch sign-up and authentication only after traffic, audit events, and support signals confirm the cutover. Existing Stytch sessions are not imported into Descope. A full cutover normally requires users to sign in once through Descope. If you need a phased transition, use the [Session Migration](/migrate/session-migration) guide to design dual-token validation or session handoff before switching all traffic. ## Replace Stytch SDKs ### SDK and Configuration Mapping Replace each Stytch surface with the Descope SDK for the same application layer: | Stytch integration | Descope replacement | Responsibility | | --- | --- | --- | | `@stytch/react`, `@stytch/nextjs`, or `@stytch/vanilla-js` | `@descope/react-sdk`, `@descope/nextjs-sdk`, or `@descope/web-js-sdk` | Render a Flow, hold and refresh the browser session, and provide current-user/session helpers. | | Stytch UI or frontend headless calls | Hosted or embedded [Descope Flow component](/client-sdk/descope-components) | Sign-up, sign-in, OAuth, passwordless methods, MFA, recovery, and conditional authentication logic. | | Stytch backend SDK or direct API calls | Matching Descope backend SDK | Validate sessions and enforce roles/permissions on protected APIs. | | Stytch user, Member, and Organization management calls | Descope Management SDK with a Management Key | Manage users, tenants, roles, SSO, SCIM, and other administrative resources. | | `STYTCH_PROJECT_ID` and frontend public token | `DESCOPE_PROJECT_ID` or `NEXT_PUBLIC_DESCOPE_PROJECT_ID` | Public project identifier used to initialize client and backend authentication SDKs. | | `STYTCH_SECRET` | No authentication-flow equivalent | Descope client authentication does not require a project secret. Use `DESCOPE_MANAGEMENT_KEY` only for server-side management operations. | Inventory and replace imports, environment variables, callback routes, cookie readers, session middleware, user/member lookups, Organization switching, authorization checks, logout handlers, and webhook verification before removing Stytch packages. ### Next.js Example For a Next.js application, replace the Stytch packages with the Descope SDK: ```bash npm uninstall @stytch/nextjs stytch npm install @descope/nextjs-sdk ``` Wrap the application with `AuthProvider`, then render the migrated authentication journey as a Descope Flow: ```tsx title="app/layout.tsx" import { AuthProvider } from '@descope/nextjs-sdk'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```tsx title="app/sign-in/page.tsx" 'use client'; import { Descope } from '@descope/nextjs-sdk'; import { useRouter } from 'next/navigation'; export default function SignInPage() { const router = useRouter(); return router.replace('/dashboard')} />; } ``` Configure authentication methods, branding, MFA, and conditional logic in the [Flow Builder](/flows), rather than recreating each Stytch frontend authentication call in application code. Protect routes with `authMiddleware()` and preserve the same public-route allowlist used by the Stytch integration: The Descope Next.js SDK's middleware support requires **Next.js 13 or later**. As of Next.js 16, the `middleware.ts` file convention is deprecated and renamed `proxy.ts` (same behavior and `config`/`matcher`). `middleware.ts` still works today, but if you're on Next.js 16+, use the `proxy.ts` tab below, or run Next's [migration codemod](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#migration-to-proxy): `npx @next/codemod@canary middleware-to-proxy .` ```ts title="middleware.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, redirectUrl: '/sign-in', publicRoutes: ['/', '/sign-in'], }); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], }; ``` ```ts title="proxy.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, redirectUrl: '/sign-in', publicRoutes: ['/', '/sign-in'], }); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], }; ``` Use `session()` in App Router Server Components and route handlers: ```tsx title="app/dashboard/page.tsx" import { session } from '@descope/nextjs-sdk/server'; export default async function DashboardPage() { const currentSession = await session(); if (!currentSession) { return

Access denied

; } return

Signed in as {currentSession.token.sub}

; } ``` Use `useSession()` and `useUser()` for client state, and use the SDK's logout helper so the refresh token is revoked: ```tsx title="app/components/user-menu.tsx" 'use client'; import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; export function UserMenu() { const sdk = useDescope(); const { isAuthenticated, isSessionLoading } = useSession(); const { user, isUserLoading } = useUser(); if (isSessionLoading || isUserLoading) return

Loading...

; if (!isAuthenticated) return null; return (

{user?.name ?? user?.email}

); } ``` See the [Next.js SDK guide](/getting-started/nextjs) for Pages Router session access and additional integration options. ### Session and Claim Differences Stytch's `session_token` is opaque and its `session_jwt` is a signed representation of the same underlying Session. Descope instead issues a signed session JWT and a refresh JWT. In Next.js, the SDK stores these in `DS` and `DSR` cookies by default and validates the session through `authMiddleware()` or `session()`. Account for these differences when replacing Stytch session code: - **User identity** - Read the user ID from `sub`. Use `useUser()` or a user-management call for profile data. - **Tenant context** - Replace request-time `organization_id` reads with `dct` for the active tenant and `tenants` for all memberships and tenant-scoped roles or permissions. - **Profile claims** - Descope session JWTs do not include fields such as `email`, `name`, or `picture` by default. Read them from the User object or add the required claims with a [JWT Template](/management/token/jwt-templates). - **Audience** - If an API currently validates Stytch's `aud` claim, add the intended audience to the JWT Template and pass the expected audience to the backend SDK's session-validation method. - **Refresh** - Use the client SDK's `refresh()` helper when profile, role, or tenant changes must appear immediately; otherwise the next automatic refresh obtains current claims. - **Logout** - Client SDK logout handles revocation and local state. For backend-managed logout, revoke the refresh token and clear both session and refresh cookies. ## Migrate M2M Clients Stytch M2M clients use the OAuth 2.0 client credentials grant and issue access tokens containing a client subject, scopes, an audience, expiration, and optional custom claims. Preserve that OAuth model with Descope [Resources](/resources), confidential [Inbound Apps](/identity-federation/inbound-apps), and [Policies](/policies): 1. Create a Resource for each protected API. Use its identifier as the token audience and define the API's scope catalog. 2. Create a confidential Inbound App for each Stytch M2M client and store its new client secret securely. 3. Create a Policy that grants the Inbound App the required scopes on the Resource through the `client_credentials` grant. 4. Configure an Inbound App JWT Template for any required custom claims. 5. Update the service to request tokens from Descope and update receiving APIs to validate the Descope issuer, JWKS, audience, expiration, and scopes. | Stytch M2M | Descope | | --- | --- | | M2M client | Confidential Inbound App | | `client_id` and `client_secret` | Inbound App client ID and secret | | Client credentials grant | Inbound App `client_credentials` grant permitted by a Policy | | M2M scopes | Resource scopes granted to the Inbound App by the Policy | | Token `aud` | Resource identifier | | Custom token claims | Inbound App JWT Template | | Secret rotation | Inbound App client-secret rotation | | Stytch token validation | Validate the Descope issuer, signature, `aud`, expiration, and scopes | Follow the [Inbound Apps client credentials guide](/identity-federation/inbound-apps/using-inbound-apps) for the token endpoint and request format. During cutover, issue the new credentials, update one service at a time, temporarily accept both issuers where necessary, and revoke each Stytch M2M client only after its traffic has moved to Descope. Use [Descope Access Keys](/management/m2m-access-keys) only when an internal service needs a Descope-issued JWT without OAuth scope or audience enforcement. If an existing Stytch client depends on `client_credentials`, `scope`, or `aud`, use Resources, an Inbound App, and a Policy. ## Additional Stytch Features | Stytch feature | Descope equivalent | Migration action | | --- | --- | --- | | Enterprise SSO | Tenant [SAML or OIDC SSO](/management/tenant-management/sso) | Recreate each Organization connection on its mapped tenant. Use the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) for customer-admin configuration. | | SCIM | Tenant-scoped [SCIM](/management/tenant-management/scim) | Repoint each external directory and test provisioning, deprovisioning, groups, and role mapping before disabling Stytch. | | B2B RBAC | [Descope RBAC](/authorization/role-based-access-control) | Convert each Resource and Action pair into a permission, recreate roles, and update authorization checks. | | First-party Connected Apps | [Federated Apps](/identity-federation/applications) | Recreate clients, redirects, claims, and token-validation expectations for applications your organization controls. | | Third-party Connected Apps | [Inbound Apps](/identity-federation/inbound-apps) | Inventory OAuth clients, redirects, PKCE, consent, scopes, audiences, refresh behavior, and JWKS dependencies. | | Admin Portal UI | [Admin Portal](/widgets/admin-portal) and [Widgets](/widgets) | Replace member, role, tenant, SSO, and SCIM management screens with hosted or embedded Descope experiences where possible. | | MFA and step-up authentication | [Flows](/flows) | Recreate enrollment and step-up policy in a Flow; route migrated users through factor re-enrollment when required. | | Fraud & Risk, Device Fingerprinting, and Protected Auth | [Fingerprinting](/fingerprinting) and [fraud connectors](/connectors/connector-configuration-guides/fraud) | Rebuild allow, challenge, block, and notify decisions as Flow conditions using the selected risk signals. | | Trusted Auth Tokens | [Inbound Apps authorization server](/identity-federation/inbound-apps/authorization-server) | Review issuer, JWKS, audience, subject, claim mapping, and provisioning, then use the JWT Bearer grant when it matches the exchange. | | Webhooks and event streaming | [Audit Trail](/audit-trails-and-integrations) and [Audit Webhook](/connectors/connector-configuration-guides/network/audit-webhook) | Update event names, signature verification, payload parsing, retries, and downstream side effects before cutover. | ## Post-Migration Verification ### Data and Authorization - [ ] Compare exported and imported Consumer User counts, and resolve every failed or skipped record. - [ ] Compare Stytch Organization counts with Descope Tenant counts and review the ID mapping file. - [ ] Compare Member counts and verify representative single-tenant and multi-tenant users have every expected `userTenants` association. - [ ] Review pending, invited, disabled, deleted, and locked records against the migration policy. - [ ] Confirm verified email and phone flags were preserved without promoting unverified identifiers. - [ ] Confirm project-level and tenant-level roles, permissions, and application authorization checks match Stytch behavior. - [ ] Verify legacy Stytch IDs and required metadata remain available in the mapping file or approved custom attributes. ### Authentication and Application Integration - [ ] Test every enabled login method, including password reset or passwordless fallback and required factor re-enrollment. - [ ] Confirm protected routes reject unauthenticated requests while intended public routes remain public. - [ ] Verify session and refresh tokens are created, refreshed, revoked, and cleared correctly on logout. - [ ] Confirm APIs reject expired, invalid, and wrong-issuer tokens. - [ ] Confirm `sub`, `dct`, `tenants`, roles, permissions, custom profile claims, and `aud` are present where required. - [ ] Test tenant discovery and switching for users with one, multiple, and no tenant memberships. - [ ] Search source files, dependency manifests, environment configuration, deployment configuration, and tests for stale Stytch imports, credentials, hosts, callback routes, cookie names, and claim reads. ### M2M and Enterprise Features - [ ] Request a Descope client-credentials token for every migrated M2M service and validate its issuer, subject, audience, scopes, expiration, and custom claims. - [ ] Confirm each API rejects a client that lacks the required Policy or scope. - [ ] Test secret rotation and verify services no longer send Stytch M2M credentials before revoking them. - [ ] Test every migrated SAML/OIDC connection, domain-routing rule, attribute mapping, and group-to-role mapping. - [ ] Test SCIM user create, update, deactivate, group membership, and role mapping from each connected directory. - [ ] Confirm audit events and external webhook delivery cover required authentication, administration, and provisioning actions. # Sessions in Descope (/sessions) Learn how Descope handles session tokens and refresh tokens for secure session management. # Sessions A **session** is the authenticated period between when a user signs in and when that login ends. During a session, your app can recognize the user across page views, API calls, and actions without asking them to authenticate on every request. Descope represents a session with two signed JWTs: If you use Descope as an [OIDC provider](/getting-started/oidc-endpoints), the session token is called an **access token** in OAuth terminology. - **Session token** — short-lived; sent with API requests and validated by your **backend** (or API gateway). - **Refresh token** — longer-lived; used by the Client or Mobile SDK to obtain a new session token when the current one expires. Tokens are signed with a [private key](/additional-security-features-in-descope/jwk-rotation). Your server validates the session token using the public key or the Descope Backend SDK. ## What is a Session? A session groups a user's interactions with your application for a period of time. While the session is active, the client holds tokens that prove the user already authenticated. Your backend checks those tokens on protected routes instead of running the full login flow again. On the web, the refresh token is often stored in an [HttpOnly cookie](/security-best-practices/refresh-token-storage) so it survives page reloads and browser restarts until it expires. Mobile apps store both tokens in the device's secure storage (Keychain or EncryptedSharedPreferences). See [Session management](/sessions/management) for platform-specific behavior. A session **ends** when any of the following occurs: - The user **logs out** — the refresh token is revoked on the server. - The **refresh token expires** — configured in [Project Settings → Session Management](/management/project-settings#refresh-token-timeout). - **Session inactivity timeout** — no activity for the configured period; see [session inactivity](/management/project-settings#session-inactivity). - The session is **revoked** server-side (for example, an admin disables the user or you call the logout API). When a session ends, the user must authenticate again through your [Flow](/flows) or OIDC login. ## How Sessions Work After authentication, the Client or Mobile SDK stores and manages both tokens. On each API call, the client sends the **session token**; your server [validates](/sessions/validation) it. When the session token expires, the SDK silently exchanges the **refresh token** for a new one. If refresh fails, the user is redirected to sign in again. Session timeouts, refresh rotation, and cookie delivery are configurable at the [project](/management/project-settings#session-inactivity) or [tenant](/management/tenant-management/tenant#session-management) level. ## SSO and Multiple Sessions When a user signs in with [SSO](/auth-methods/sso), more than one session can exist at once: | Session | Where it lives | Purpose | | --- | --- | --- | | **Application session** | Your app (Client/Mobile SDK) | Tells your app and API that the user is signed in | | **Descope session** | Descope | Tracks authentication for your project; used for refresh and logout | | **IdP session** (SSO only) | The identity provider (Okta, Azure AD, etc.) | Lets the user sign in to Descope without re-entering credentials if the IdP session is still valid | If the user already has an active IdP session, SSO can feel seamless — they may not see a login prompt. Your application session still depends on Descope issuing tokens to your client after the flow completes. ## Common Use Cases **Protected areas of a public site** — A storefront may allow anonymous browsing but require login for account settings or order history. Use the session token to gate those routes and API endpoints. **Always-authenticated apps** — Dashboards and internal tools typically validate the session token on every API request. Pair short [session token timeouts](/management/project-settings#session-token-timeout) with refresh for security without frequent password prompts. **SPAs and mobile apps without a custom backend** — The Client or Mobile SDK manages token storage and refresh. If you still call your own APIs, those APIs must [validate](/sessions/validation/backend) the session token. For live authorization data that may change mid-session, use [Token introspection](/sessions/introspection). **Step-up and MFA** — Sensitive actions can require a fresh authentication step. Descope issues a short-lived step-up token; see [Step Up](/mfa-and-step-up/step-up). ## What's in the Token Session and refresh tokens share the same claim payload. For a full claim reference, such as mandatory vs optional claims, `tenants`, `roles`, custom claims, and more, see the [Token Claims Management](/management/token) docs. Control what custom claims are included in your JWTs with [JWT Templates](/management/token/jwt-templates) and/or the [Custom Claims flow action](/flows/actions/custom-claims). Generally, changes to custom claims (such as user attributes or roles) in the JWT are updated whenever a session token is refreshed. When you need state that may have changed between refreshes — for example, an active tenant mid-session — use [Token introspection](/sessions/introspection) or the [Management SDK](/management/user-management/sdks). ## Related Documentation - [Token Claims Management](/management/token) — claim reference and configuration - [JWT Claims security best practices](/security-best-practices/custom-claims) - [Refresh token rotation](/additional-security-features-in-descope/refresh-token-rotation) - [Backend SDK](/backend-sdk) — authentication and session validation with Descope SDKs # Token Introspection (/sessions/introspection) Validate access tokens and read live user claims, roles, and permissions using Descope UserInfo endpoints for Federated and Inbound Apps. # Token Introspection This pattern is typically referred to as token introspection. Descope implements it through the standard OIDC `/userinfo` endpoint, rather than a dedicated OAuth 2.0 Token Introspection endpoint (RFC 7662). For basic JWT validation, see our [Session Validation](/sessions/validation) docs. Verifying a token's signature confirms that Descope issued it and that it has not expired. It says nothing about the user's current roles, permissions, tenant, or custom attributes. Those reflect whatever was true at the moment the token was issued. When you need the live state of the user's roles, permissions, tenant, and custom attributes on every request, you can call the UserInfo endpoint with the same access token the client sent you. Descope will validate the token and returns the user's current claims, which you then use in your authorization logic. Therefore, you should use token introspection with UserInfo when: - Roles, permissions, or tenant assignments may have changed since the token was issued - You need custom attributes or template output that are not in the JWT - You must read the active tenant mid-session (for example, after a `switch_tenant` tool call) UserInfo is not the only way to read current user state. The [Management SDK or API](/management/user-management/sdks) also works, but it requires a management key, which is a privileged server credential. UserInfo authenticates with the access token the client already sent, which is why MCP servers and third-party APIs prefer it. It's worth noting that this pattern applies to tokens issued through [Federated Applications](/identity-federation/applications/oidc-apps), [Inbound Apps](/identity-federation/inbound-apps), and [Agentic Clients](/agentic-identity-hub/core-components/clients). The endpoint path you use depends on which app type issued the token, and the paths are not interchangeable. ## UserInfo Endpoints by App Type Federated Applications and Inbound Apps use different UserInfo routes. Send each app type's access token to its matching endpoint. | App type | UserInfo endpoint | Issued by | API reference | | --- | --- | --- | --- | | **[Federated Application](/identity-federation/applications/oidc-apps)** (OIDC) | `{baseUrl}/oauth2/v1/userinfo` | Standard OIDC authorization code, client credentials, or device flows against a Federated App | [OIDC endpoints](/identity-federation/applications/oidc-apps/oidc-endpoints) | | **[Inbound App](/identity-federation/inbound-apps)** | `{baseUrl}/oauth2/v1/apps/userinfo` | Inbound App `/oauth2/v1/apps/token` flows | [Get UserInfo](/api/third-party-apps/user-info-get), [Post UserInfo](/api/third-party-apps/user-info-post) | | **Inbound App (project-scoped)** | `{baseUrl}/oauth2/v1/apps/{projectId}/userinfo` | Same Inbound App tokens; path advertised in some [MCP discovery](/agentic-identity-hub/core-components/mcp-servers/discovery-url) documents | Same behavior as `/oauth2/v1/apps/userinfo` | Replace `{baseUrl}` with your project base URL (for example `https://api.descope.com`, a [regional host](/how-to-deploy-to-production/public-static-ips), or a [custom domain](/how-to-deploy-to-production/custom-domain#configure-custom-domain)). An access token from an Inbound App will not work against `/oauth2/v1/userinfo`, and a Federated Application access token will not work against `/oauth2/v1/apps/userinfo`. If you are unsure which type you have, check the app's discovery document or the token issuer. ## Federated Application `/userinfo` Endpoint For [Federated Applications](/identity-federation/applications/oidc-apps) configured in [Applications](https://app.descope.com/applications), the OIDC UserInfo endpoint is: ```http GET {baseUrl}/oauth2/v1/userinfo Authorization: Bearer ``` ```bash curl -X GET "__BaseURL__/oauth2/v1/userinfo" \ -H "Authorization: Bearer " ``` The response includes standard OIDC profile claims, plus [custom claims](/identity-federation/applications/oidc-apps#custom-claims) and [tenant roles and permissions](/identity-federation/applications/oidc-apps#tenant-roles-and-permissions) depending on the requested scopes. See [Using OIDC Endpoints](/identity-federation/applications/oidc-apps/oidc-endpoints) for the full flow. ## Inbound App `/userinfo` Endpoint For [Inbound Apps](/identity-federation/inbound-apps), UserInfo lives under the `/apps` path: ```http GET {baseUrl}/oauth2/v1/apps/userinfo Authorization: Bearer ``` ```bash curl -X GET "__BaseURL__/oauth2/v1/apps/userinfo" \ -H "Authorization: Bearer " ``` - A successful response means Descope accepts the token for that app. Use the returned claims for authorization. - A non-success response means the token is invalid or revoked, so reject the request. The response reflects the user's current profile and claims as Descope resolves them for that token, following your [JWT template](/management/token/jwt-templates) and granted scopes. **HTTP methods:** Both GET and POST are supported: - [Get UserInfo](/api/third-party-apps/user-info-get) - [Post UserInfo](/api/third-party-apps/user-info-post) Some clients, including MCP discovery metadata, advertise the project-scoped variant `{baseUrl}/oauth2/v1/apps/{projectId}/userinfo`. It accepts the same Inbound App access tokens. ## Operational considerations Every UserInfo call will add a degree of latency to your validation process. If your risk model tolerates authorization data lagging Descope by some seconds or minutes, cache responses for a short TTL. A signature-only gateway ([JWT authorizers](/sessions/validation/jwt-authorizers)) validates tokens at the edge but does not return live claims. When your server needs current authorization data, call UserInfo. Otherwise, validate locally and accept some staleness. # Admin Portal (/widgets/admin-portal) Learn how to use the Descope Admin Portal to provide users and tenant admins with a hosted identity management experience built from Descope widgets. # Admin Portal The **Admin Portal** is a hosted experience that can surface a combination of user-facing and admin-facing widgets, including the [User Profile Widget](/widgets/users#user-profile-widget), the [Applications Portal Widget](/widgets/users#applications-portal-widget), and admin capabilities from [Admin Widgets](/widgets/admins) such as user management, role management, and access key management. Any authenticated user can open the Admin Portal, regardless of tenant membership or admin role. Use it as a hosted landing page where all of your customers' users launch their SSO apps and manage their profile. Admin-only widgets stay visible only to users who hold the right permissions. To let tenant admins configure their own SSO/SCIM connection, use the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), the self-service configuration experience. New to SSO? See [Getting Started with SSO](/auth-methods/sso/getting-started). ## Admin Portal Settings In the Descope Console, go to [Widgets](https://app.descope.com/widgets) and open the [Admin Portal](https://app.descope.com/widgets/adminportal) tab. This page is where you define what the hosted portal contains and how it behaves. You can also manage enablement, style, and the widget list with Terraform — see [Terraform → Admin Portal](/managing-environments/terraform#admin-portal). ![Admin Portal settings in Descope Console](/assets/admin-portal.webp) Users don't need a tenant to open the portal. Some hosted widgets, such as user management, role management, and access key management, are tenant-scoped and only appear for users who belong to a tenant. User Profile and Applications Portal aren't tied to a tenant, so every authenticated user sees them. If a user is part of multiple tenants, you can pre-select a tenant to use with the Admin Portal, with the `tenant` query parameter. An example URL is: `?tenant=` The portal is styled and branded according to your project's styling and branding configuration. You can also customize the title of the portal. - **URL**: Shows the hosted portal link that you can share or embed. - **Widgets to host in the portal**: Controls which widgets appear in the left navigation, and the order selected here determines the order in the portal menu. - **Sign-In flow**: Determines which flow is used when a user is redirected to authenticate for the portal. - **Style**: Applies the selected style and theme configuration. You can also set the portal's appearance with the `theme` query parameter, using either light or dark. For example, `?theme=light` loads the portal in light mode. - **Portal title**: Sets the title shown in the hosted portal experience. You can disable the Admin Portal, which will make the admin portal URL inaccessible, and prevent users from accessing the portal. You can re-enable it at any time. ## What Users See in the Hosted Admin Portal The hosted portal presents a left-side navigation with the widgets you configured, and a main content area for the selected widget. For example, a tenant admin may see user management, role management, access key management, audit logs, applications portal, user profile, and tenant profile, depending on enabled widgets and role permissions. ![Hosted Admin Portal example](/assets/admin-portal-widgets.webp) Even when a widget is listed in the portal configuration, users only see actions they are authorized to perform. This makes the portal suitable for mixed audiences where different admins have different permission scopes. You can restrict which users of the Admin Portal see a given widget by configuring [required permissions](/widgets#restricting-access-to-widgets) on that widget. This applies to any widget hosted in the portal, whether it's a [User Widget](/widgets/users) or an [Admin Widget](/widgets/admins). ### Collapsing the Navigation Menu On desktop, the left navigation can be collapsed and expanded using a toggle in the toolbar. It's open by default, and your preference is remembered in the browser for future visits. You can set the navigation's initial state with the `sidebarOpen` query parameter. For example, `?sidebarOpen=false` loads the portal with the navigation collapsed. On mobile, the navigation appears as a drawer and is always collapsed by default. ## Selecting a Tenant If a user belongs to more than one tenant, they must select an organization from the Select an Organization page before accessing the portal. ![Admin Portal Select an Organization page](/assets/admin-portal-tenant-selection.webp) After the user selects a tenant, everything in the portal is scoped to that tenant. Users who belong to only a single tenant skip this page and are taken directly into the portal. To switch to a different tenant after making a selection, users can open the profile menu and select **Switch Organization**. ### Users Without a Tenant B2C users who aren't associated with any tenant skip tenant selection and land in the portal directly. They see only the widgets that don't require a tenant, like User Profile and Applications Portal. User management, role management, access key management, audit, tenant profile, and outbound applications all need a tenant to scope to, so they stay hidden even when configured for the portal. # Admin Widgets (/widgets/admins) Learn about Descope admin widgets that enable administrators to manage users, roles, access keys, and audit logs. # Admin Widgets Descope Admin Widgets provide powerful components that enable administrators to manage their organization's users, roles, access keys, and audit logs. These widgets allow administrators to handle various management tasks directly within your application. To use the admin widgets, a user needs a role that carries the "User Admin" permission for that tenant. The built-in "Tenant Admin" role includes this permission. To narrow access further, set [required permissions](https://github.com/widgets#limiting-widget-visibility-with-required-permissions) on an individual widget so only admins holding those permissions can see and use it. ## Overview Admin Widgets are designed to handle different aspects of administrative management: 1. **User Management Widget**: Enables administrators to manage user accounts, including creating, editing, and managing user access. 2. **Role Management Widget**: Allows administrators to create and manage roles and their associated permissions. 3. **Access Key Management Widget**: Provides tools for managing machine-to-machine access keys. 4. **Audit Widget**: Offers visibility into user actions and system events through comprehensive audit logs. 5. **Tenant Profile Widget**: Allows administrators to manage the profile attributes of their tenant. ## User Management Widget The User Management Widget provides administrators with comprehensive tools to manage user accounts within their organization. This widget enables administrators to: - Create new user accounts - Edit existing user information - Set, update, or clear a user's recovery email and recovery phone number - Activate or disable user accounts - Reset user passwords - Remove user passkeys - Delete user accounts - See how each user was provisioned (SSO via SAML/OIDC, or SCIM) You can also add functionality to perform custom logic on users by adding a [custom button](/widgets/flows#configuring-component-flows) to the widget, and modifying the [flow for the button](/widgets/flows#configuring-component-flows). This includes running [bulk actions on multiple selected users](/widgets/flows#batch-user-actions) or [editing roles for a single selected user](/widgets/flows#editing-a-single-users-roles). Custom fields will also appear in the user management table. A recovery email or phone number set through this widget is marked verified immediately, since the value comes from a trusted admin. This differs from the self-service [User Profile Widget](/widgets/users#user-profile-widget), where the end-user must verify a new recovery value before it's shown as set. ![Descope user management widget](/assets/user-management-widget.webp) ### Filtering Columns You can add, remove, and sort the columns shown in the widget from the Design tab. ![Descope user management widget - filtering columns](/assets/user-management-widget-columns.webp) ### Configuring Cross-Tenant Attribute Edits By default, when a Tenant Admin views a user from a different tenant than their own (cross-tenant), the widget only lets them view that user's profile and assign or remove their tenant roles—not edit the user's other attributes. To also allow editing user attributes across tenants: 1. Go to the [Widgets page](https://app.descope.com/widgets) in the Descope Console and open the **User Management** widget. 2. Click the gear/settings icon and toggle on **Allow cross-tenant attribute edits**. 3. Save the widget. ![Cross-tenant-attribute](/assets/cross-tenant-attribute.webp) ```js import { UserManagement } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html ``` ```js ``` ```js import { UserManagement } from '@descope/nextjs-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html { console.log('Widget is ready'); }} > ``` ## Role Management Widget Delegating permission and role creation to tenant admins requires having delegation roles created and associated to the tenant admin. An example can be shown [here](/authorization/role-based-access-control/examples/b2b-rbac#delegating-role-creation-and-permission-assignment). The Role Management Widget enables administrators to create and manage roles within their organization. This widget allows administrators to: - Create new roles - Modify existing role permissions - Delete roles You cannot modify any underlying behavior of the role management widget components, as there are no [default flows](/widgets/flows#configuring-component-flows) to modify. #### Notes - The `Editable` field is determined by the user's access to the role - meaning that project-level roles are not editable by tenant level users. - You need to pre-define the permissions that the user can use, which are not editable in the widget. ![Descope role management widget](/assets/role-management-widget.webp) ```js import { RoleManagement } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html ``` ```js ``` ```js import { RoleManagement } from '@descope/nextjs-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html { console.log('Widget is ready'); }} > ``` ## Access Key Management Widget The Access Key Management Widget provides tools to manage [Access Keys](/management/m2m-access-keys) for machine-to-machine authentication. This widget enables users to: - Create new access keys - Activate or deactivate existing access keys - Delete access keys ### Supported Use Cases The Access Key Management Widget automatically adapts its behavior based on the user's permissions and tenant association. Users with a valid JWT, who are not associated with a tenant, can generate and manage access keys for themselves, with keys automatically bound to their user identity. For users associated with a tenant (including Tenant Admins), the widget allows them to generate and manage their own access keys within the tenant context. However, users with the **Tenant Admin** role have additional capabilities—they can generate and manage access keys on behalf of other users in their tenant, with these keys bound to the target user rather than the admin who created them. The widget automatically determines which functionality to display based on whether the user has the **User Admin** or **Tenant Admin** permission, their tenant association, and their role and permission level. You cannot modify any underlying behavior of the access key management widget components, as there are no [default flows](/widgets/flows#configuring-component-flows) to modify. ### Configuring Access Keys Widget Usage The three use cases described above aren't all enabled at once. You can configure the widget itself with whichever ones it supports: 1. On the [Widgets](https://app.descope.com/widgets) page, find the **User Access Key Management** widget, click the three-dot menu (`⋮`), and select **Settings**. 2. Under **Access Keys Widget Usage**, choose which use case(s) the widget should support: - **Tenant administrators** - allows tenant admins to manage the access keys for their entire tenant - **Tenant users** - allows tenant users to manage their own access keys - **Application users** - allows application users (not associated with a tenant) to manage their own access keys 3. Save the widget. Only access keys created through this widget are automatically bound to a user (see [Associating Access Key to Users](/management/m2m-access-keys#associating-access-key-to-users)). This matters if an [Access Key JWT Template](/management/token/jwt-templates#access-key-jwt-templates)'s custom claims reference a `user.*` dynamic value, such as `{{user.userId}}`. That value can only resolve for access keys that have a bound user. ![Descope access key management widget](/assets/access-key-management-widget.webp) ```js import { AccessKeyManagement } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html ``` ```js ``` ```js import { AccessKeyManagement } from '@descope/nextjs-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html { console.log('Widget is ready'); }} > ``` ## Audit Widget The Audit Widget provides administrators with visibility into user actions and system events. This widget includes all tenant authentication events and custom audit events, allowing administrators to: - Monitor user activities - Track system events - Review authentication attempts - View custom audit events - Export audit events You cannot modify any underlying behavior of the audit widget components, as there are no [default flows](/widgets/flows#configuring-component-flows) to modify. When creating the widget, you can configure the **Visible event types**, where you can define a list of events you would like to include in the widget. By default, all audit events will be visible. ![Descope audit management widget](/assets/audit-management-widget.webp) ### Filtering Columns You can filter the columns that are displayed in the audit widget by selecting or removing **Columns** under the Design tab on the right, under Content. ![Descope audit management widget - filtering columns](/assets/audit-management-widget-filtering-columns.webp) ```js import { AuditManagement } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html ``` ```js ``` ```js import { AuditManagement } from '@descope/nextjs-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html { console.log('Widget is ready'); }} > ``` ## Tenant Profile Widget The Tenant Profile Widget enables tenant administrators to manage their tenant's properties and configuration. This widget allows administrators to: - Update the tenant name - Modify custom attributes for the tenant - Manage email domains associated with the tenant - Configure SSO enforcement settings, including modifying an SSO exclusion list - Access SSO configuration, including generating and revoking a link for the tenant's [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) - Configure the tenant's password policy - Configure the tenant's session management settings You can modify the behavior of the tenant profile widget functions by modifying the [default flows](/widgets/flows#configuring-component-flows) for each component in the widget. You cannot, however, add custom buttons to the widget. ![Descope tenant profile widget](/assets/tenant-profile-widget.webp) ### Custom Action Icons Each attribute row in the Tenant Profile Widget uses Descope's default icons for its Edit and Delete buttons. You can override these with your own icons on a per-attribute basis (for example, using different icons for SSO Exclusions than for Enforce SSO) from the widget editor's Design tab. 1. Open the **Tenant Profile Widget** in the Console, select the attribute row to customize, and go to the **Design** tab. 2. Expand the **Edit button** or **Delete button** accordion (they're independent, so you can customize one without the other). 3. Click the pencil icon, then upload a custom icon for the button. You can upload separate icons for light and dark themes. ![Descope tenant profile widget - custom action icons](/assets/tenant-profile-widget-action-icons.webp) Each uploaded icon must be under 20KB. The same accordion also includes an **Edit button text** or **Delete button text** field, which sets the button's label. This is a separate setting that does not affect the icon. ```js import { TenantProfile } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html ``` ```js ``` ```js import { TenantProfile } from '@descope/nextjs-sdk'; ... { console.log('Widget is ready'); }} /> ``` ```html { console.log('Widget is ready'); }} > ``` # Widget Flows (/widgets/flows) Learn how Descope Widgets are powered by Flows, and how to customize, extend, and pass inputs into the flows running underneath widget components. # Widget Flows Descope Widgets are powered by [Flows](/flows) running underneath each component. This means you have full control over the logic and user experience for every widget action. ## Customizing Widget Behavior Each widget component has associated flows that handle the underlying operations. You can modify these flows to customize the logic for actions like: - **Email Updates**: Control the verification process when users update their email, including how verification codes are sent and validated - **Passkey Management**: Customize the logic for adding or removing passkeys, including any additional security checks - **Password Changes**: Add custom validation rules, strength requirements, or notification logic when users reset their passwords - **Authentication Factors**: Control the configuration of additional authentication factors, like SMS OTP through the phone attribute - **Profile Updates**: Add custom validation or approval workflows for profile changes - **User Creation/Management**: Customize approval processes, notification logic, or validation rules for admin widgets ![Widget behavior customization](/assets/widget-behavior-customization.webp) ## Configuring Component Flows Every widget consists of components that have associated flows you can modify. There are several ways to configure and customize these flows: 1. **Component Flows**: Each widget component has flows that handle its operations. You can modify these flows directly to customize the behavior, such as changing how email verification works or how profile updates are processed. 2. **Custom Button Actions**: Some widgets (like the [User Management Widget](/widgets/admins#user-management-widget)) allow you to add custom buttons. Newly added buttons include an empty flow that you can access under the **Behavior** tab in order to fully customize any logic you need for a button. For bulk operations on multiple selected users, see [Batch User Actions](#batch-user-actions) below. To edit roles for a single selected user, see [Editing a Single User's Roles](#editing-a-single-users-roles) below. 3. **Behavior Settings**: You can enable additional built-in capabilities from the **Behavior** tab in the widget settings. For example, in the [User Profile Widget](/widgets/users#user-profile-widget), the [passkeys section](/widgets/users#passkeys) lists every passkey registered to the user; enabling the ability to remove passkeys lets users select and remove a specific one, which activates a flow that you can then modify to customize the removal process. To edit a flow, click on the component or button you want to customize, navigate to the **Behavior** tab in the settings panel on the right, and click **Edit** next to the relevant flow to open the flow editor. ![Widget behavior settings](/assets/widget-behavior-settings.webp) ## Batch User Actions In the [User Management Widget](/widgets/admins#user-management-widget), administrators can select multiple users from the table and run a custom button flow to perform bulk operations on all selected users using `User / Batch` Actions. | Available Batch Actions | Functionality | |-----------------------------------------|--------------------------------------------------------------------------| | User / Batch / Delete | Delete selected users | | User / Batch / Set Roles | Set roles for selected users (replaces existing roles) | | User / Batch / Add Roles | Add roles to selected users | | User / Batch / Remove Roles | Remove roles from selected users | | User / Batch / Add Applications | Add federated applications to selected users | | User / Batch / Delete Applications | Remove federated applications from selected users | | User / Batch / Update Status | Update status of selected users | | User / Batch / Remove TOTP seed | Remove the TOTP seed for selected users | ### Build the Flow When the custom button is clicked, the widget launches its associated flow with context about the selected users. To process them in bulk: 1. Open the button's flow from the widget editor (**Behavior** tab → **Flows** → **Edit**). 2. Add any screens or inputs needed to collect additional information from the administrator. 3. Add a `User / Batch` Action to the flow. 4. Configure the action, such as deleting users, updating their status, assigning roles, or adding applications. 5. Optionally add a confirmation screen, audit event, connector action, or webhook at the end. ![Batch user action widget flow](/assets/widget-batch-user-flow.webp) ### Using Role or Application Selectors Some batch actions require the administrator to choose roles or applications before the batch action can run. For actions involving roles, such as Set Roles, Add Roles, or Remove Roles, add a screen to the flow with a **Roles Selector** input component. The values which will appear in the dropdown can be populated under the **Behavior** tab → **Values**. The selected roles will then be passed into the relevant User / Batch role action. Similarly, for actions involving applications, such as Add Applications or Delete Applications, add a screen to the flow with an **Applications Selector** input component. The values which will appear in the dropdown can be populated under the **Behavior** tab → **Values**. The selected applications will then be passed into the relevant User / Batch application action. ![Role selector screen](/assets/widget-flows-role-selector.webp) ## Passing Flow Inputs to Widgets Widget flows run with empty inputs by default, which means any `{{form.*}}` or `{{client.*}}` dynamic references in a flow step will silently resolve to an empty string unless you pass values through the widget. Use the `form` and `client` properties to supply those values from your application. A common use case is customizing the `refreshCookieName` on a widget's End step. Without passing the value in, the cookie falls back to the default name (`DSR`). With `form`: ```jsx import { UserProfile } from '@descope/react-sdk'; ``` ```jsx import { UserProfile } from '@descope/nextjs-sdk'; ``` ```html ``` When a flow step is configured with `refreshCookieName = {{form.cookieName}}`, it will receive `my-refresh-cookie` instead of resolving to empty. Widget-internal values (such as passkey or device IDs set by the widget itself) take precedence over caller-supplied `form` and `client` values when the same key is present in both. ## Editing a Single User's Roles You can add a custom button that lets an administrator edit one selected user's roles, without exposing other attributes like email or name. This suits B2B use cases where a tenant admin manages roles within their tenant but shouldn't touch anything else about the user. To build this, add a screen to the button's flow with a **Roles Selector** input component. When the administrator selects exactly one user, the selector pre-populates with that user's current roles instead of starting empty, so the administrator only needs to add or remove from there. Connect the selector to a role-update action that applies the selection. Instead of replacing the user's full role set, this action compares the new selection against the user's existing roles and adds or removes only the difference. ## Example Use Case: Requiring MFA Before Editing Sensitive Attributes Each attribute row in the [User Profile Widget](/widgets/users#user-profile-widget) (Name, Phone, Email, and so on) has its own flow behind its Edit action. To require step-up verification before an edit is allowed: 1. Open the flow behind the attribute you want to protect from the widget's **Behavior** tab. 2. At the start of the flow, add a **Condition** that checks whether the current session already carries the [`su` step-up claim](/mfa-and-step-up/step-up#how-step-up-authentication-works). 3. If the session isn't stepped up, route to a screen with an [authentication method action](/flows/actions/authentication-methods) (for example, OTP, passkeys, or an authenticator app) marked as step-up. On success, the session is updated with the `su` claim. 4. Only after the step-up check passes should the flow continue to the actual attribute-update action. Because each attribute has its own flow, you can require step-up selectively, for example on Email and Phone, while leaving lower-risk attributes like profile picture untouched. ## Example Use Case: Notifying Both the Old and New Email on Change Since a user's email is often used as a login identifier, it's important to alert them whenever their email changes. If an attacker compromises an account and changes the email, the legitimate user should be notified at both the old and new addresses to make sure they are aware of the change. To alert a user at both addresses whenever their login email changes: 1. Open the flow behind the [Email attribute's](/widgets/users#user-profile-widget) Edit action from the widget's **Behavior** tab. 2. Near the start of the flow, before the new email is verified and applied, add a [`Load User`](/flows/actions/load-user) action so the current (soon to be old) address is available as `{{user.email}}`. Reference or store that value here, since after the update runs later in the flow, `{{user.email}}` will reflect the new address. 3. After the new email is verified and the update action completes, add two messaging actions using a [Messaging Connector](/connectors/connector-configuration-guides/messaging) (such as SMTP, SendGrid, or AWS SES) — one addressed to the old email you captured in step 2, and one addressed to the now-current `{{user.email}}` — each explaining that the account's login email changed. 4. Optionally, add a [`Generate Audit Event`](/flows/actions/generate-audit-event) action alongside the notifications to log the change for compliance and security review. # Widgets (/widgets) Learn about Descope widgets, a way to use designated components to delegate operations to your customers. # Widgets Descope Widgets are embeddable components designed to facilitate the delegation of operations to your application's users. Through the console, you can fully customize the display and functionality of your widgets according to your user's needs. These widgets can be utilized in both B2B and B2C contexts, allowing your users and admins to perform various role, user, and project level management from within your application. Currently only the [User Profile Widget](/widgets/users#user-profile-widget) is optimized to also work on mobile devices. The other widgets are designed to work on desktop browsers only. ## Creating A Widget You can create a widget from the [widgets page](https://app.descope.com/widgets) of your Descope console, starting from one of our widget templates or by importing one from a JSON file. From the widget template library, you can filter widgets based on whether they are for end users or admins and based on use case. You can also preview what the template looks like from the library before creating the widget. ![Descope widgets in console](/assets/widget-templates.webp) After you create a widget, you can edit its design and logic. You can add/remove/alter buttons and text within the widget, as well as modify the design of the widget container and the components within. Within the User Profile widget, you can include custom attributes, and mark fields as `Read-only` and/or `Mandatory`. To edit a specific part of the widget, simply click on it and utilize the Design & Behaviors toolbar on the right: ![Descope widget editor](/assets/widget-editor.webp) Inside preview mode you can see how the widget will look like on the client side, including actions and light/dark mode: ![Descope widgets in preview action](/assets/add-user-modal-widgets.webp) Read more about [User Widgets](/widgets/users), [Admin Widgets](/widgets/admins), the hosted [Admin Portal](/widgets/admin-portal), and how to customize [Widget Flows](/widgets/flows). ## Restricting Access to Widgets By default, any user with the **User Admin** permission for a tenant can see and use every [Admin Widget](/widgets/admins) hosted in your app or in the [Admin Portal](/widgets/admin-portal), such as user management, role management, and access key management. [User Widgets](/widgets/users) like the User Profile and Applications Portal widgets don't require the User Admin permission: any authenticated member of the tenant sees them by default. For finer control, such as letting one admin see the User Management widget but not the Audit widget, or hiding the Applications Portal widget from some users, configure **required permissions** on a per-widget basis. This applies to both User Widgets and Admin Widgets. ### Configuring Required Permissions 1. Go to the [Widgets page](https://app.descope.com/widgets) in the Descope Console and open the settings for the widget you want to restrict. 2. In the widget settings dialog, find the **Required permissions** field and select one or more of your project's existing [permissions](/authorization/role-based-access-control). 3. Save the widget. ![Required permissions field in widget settings](/assets/widget-required-permissions.webp) Leaving this field empty keeps the widget's default behavior: Admin Widgets are available to anyone with the User Admin permission and User Widgets are available to any authenticated member of the tenant. ### How Enforcement Works - A user must hold **all** of the permissions selected for a widget to see it and perform its actions. For Admin Widgets, the User Admin permission is also required. Missing even one required permission hides the widget. - Enforcement applies both when the widget is embedded directly in your app and when it's hosted in the [Admin Portal](/widgets/admin-portal), where the widget is simply omitted from the left navigation for users who don't qualify. - If a permission referenced by a widget is later deleted from the project, it's automatically removed from that widget's required permissions list. Required permissions are configured by [Descopers](/management/company-settings#descopers) with the right permissions, from the Descope Console. Widget permissions are not something end users or tenant admins can set for themselves. ## Flows within Widgets Descope Widgets are powered by [Flows](/flows) running underneath each component, giving you full control over the logic and user experience for every widget action. This includes customizing widget behavior, configuring component flows, running batch actions on multiple users, passing flow inputs into widgets, and more. See [Widget Flows](/widgets/flows) for details. ## Customizing a Widget You can customize the Widget Component by passing in the following props. These customizations can be applied to any of the available widgets: - `theme`: theme can be "light", "dark" or "os", which auto selects a theme based on the OS theme. Default is "light" - `styleId`: style Id can be the id of the style you wish to run your widget with - `form`: key/value pairs forwarded into widget flows as [flow inputs](/flows/dynamic-keys/flow-inputs). Any `{{form.*}}` references configured in a widget flow will resolve to these values. - `client`: key/value pairs forwarded into widget flows as client metadata. Any `{{client.*}}` references configured in a widget flow will resolve to these values. - `debug`: debug can be set to true to enable debug mode - `locale`: locale can be [any supported locale](/management/localization#language-support) that the widget is translated to. If not provided, the locale comes from the browser. `form` and `client` are currently supported in the React and Next.js SDKs, and in the HTML Web Component. For example, this is how you would customize the User Management Widget: ```js import { UserManagement } from '@descope/react-sdk'; ... ``` ```html ``` ```js ``` ```js import { UserManagement } from '@descope/nextjs-sdk'; ... ``` ```html ``` ## Disabling and Activating Widgets When you are not actively using a widget, you can disable it from the [Descope Console](https://app.descope.com/widgets). This can be done by selecting the widget(s) using the checkboxes on the left, then clicking the **Disable** button at the top of the table, or by clicking the three dots on the right and selecting **Disable** from the dropdown menu. To re-enable the widget(s), follow the same steps but select **Activate** instead. ![Disable/ Activate widgets](/assets/disable-widgets.webp) ## Exporting a Widget Widgets can be exported from the [Descope Console](https://app.descope.com/widgets) as a JSON file. This file will include the the design of the widget along with the actions of each button included in the widget. Each button's action is designed as a flow that runs using information from the widget. All of these flows being used by the widget are also exported in the file. The widget's JSON data can be imported to another Descope project's widgets to copy a custom widget. ## Show Code Within the [Widgets page](https://app.descope.com/widgets) of the Descope Console, you can generate a frontend code snippet by clicking the three dots at the right of the widget and selecting the **Show Code** option. You can select between various frontend frameworks to integrate the selected widget into your app. ## SDKs You can use the below SDKs to implement widgets in your app. | Language | GitHub Location | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | React | | Angular | | Nextjs | | Vue.js | # User Widgets (/widgets/users) Learn about Descope user widgets that enable end-users to manage their profiles, authentication methods, and application access. # User Widgets Descope User Widgets provide self-service components that empower your end-users to manage their own profiles and authentication methods, and access their applications. These widgets enable a seamless user experience by allowing users to handle their own account settings and preferences directly within your application. If a User Widget is hosted in the [Admin Portal](/widgets/admin-portal) (for example, the Applications Portal Widget), you can restrict which users see it there by configuring [required permissions](/widgets#restricting-access-to-widgets) on that widget. ## Overview User Widgets are designed to give end-users control over their own data and access: 1. **User Profile Widget**: Enables users to manage their personal information, profile pictures, and authentication methods like passkeys, passwords, and TOTP. 2. **Applications Portal Widget**: Provides users with a personalized dashboard of applications they have access to, allowing them to quickly navigate between different services. 3. **Outbound Applications Widget**: Provides users with a list of outbound applications they are managing tokens with, and allowing them to connect to each of them directly from a widget. 4. **Tenant Switcher Widget**: Allows users who belong to more than one tenant to choose which tenant is active for their session, so tenant-scoped roles, data, and configuration apply in the correct context. ## User Profile Widget The widget enables your end users to: - Update user profile picture - Update user personal information (name, phone, email, given name, middle name, family name) - Update a recovery email and recovery phone number - Update authentication methods (passkey, password, TOTP) - See and sign out of all trusted devices - Logout You can modify the behavior of the user profile widget functions by modifying the [default flows](/widgets/flows#configuring-component-flows) for each component in the widget. You cannot, however, add custom buttons to the widget. Setting a recovery email or phone number requires the user to verify it through a confirmation link before it takes effect. Until verified, the value won't show as set. See [Adding a Recovery Email](/flows/use-cases/recovery-email) for how the underlying flow works. The `onReady` callback is optional and can be used to handle the widget's ready state. ![Descope user profile widget](/assets/user-profile-widget.webp) ### Passkeys The widget lists all of the passkeys registered to the user's account (for example, one per device), letting users keep track of every passkey they've added. Users can only remove a passkey from the widget if that capability is enabled in the widget page's own passkey behavior settings. ![Multiple passkeys](/assets/multiple-passkeys.webp) ### Custom Action Icons Each attribute row in the User Profile Widget uses Descope's default icons for its Edit and Delete buttons. You can override these with your own icons on a per-attribute basis (for example, using different icons for Name than for Email) from the widget editor's Design tab. 1. Open the **User Profile Widget** in the Console, select the attribute row to customize, and go to the **Design** tab. 2. Expand the **Edit button** or **Delete button** accordion (they're independent, so you can customize one without the other). 3. Click the pencil icon, then upload a custom icon for the button. You can upload separate icons for light and dark themes. ![Descope user profile widget - custom action icons](/assets/user-profile-widget-action-icons.webp) Each uploaded icon must be under 20KB. The same accordion also includes an **Edit button text** or **Delete button text** field, which sets the button's label. This is a separate setting that does not affect the icon. ### Trusted Devices Currently as an admin, you cannot see a user's trusted devices. Your end users can only see their own trusted devices through this widget. Users can see all of the [trusted devices](/mfa-and-step-up/mfa/adaptive-mfa#trusted-device) associated with their user account, and sign out of any of them. ![Descope user profile widget - trusted devices](/assets/user-profile-widget-trusted-devices.webp) ```js import { UserProfile } from '@descope/react-sdk'; ... { console.log('Widget is ready'); }} onLogout={() => { // add here you own logout callback window.location.href = '/login'; }} /> ``` ```html ``` ```js ``` ```js import { UserProfile } from '@descope/nextjs-sdk'; { console.log('Widget is ready'); }} onLogout={() => { // add here you own logout callback window.location.href = '/login'; }} /> ``` ```html ``` ## Applications Portal Widget The Applications Portal Widget provides a place for users to access all of the [Federated Applications](/identity-federation/applications) they are associated with. The widget automatically displays only the apps that the user has access to. The widget doesn't support [custom flows](/widgets/flows#configuring-component-flows) for its underlying behavior. ![application portal widget](/assets/application-portal-light.webp) ```js import { ApplicationsPortal } from '@descope/react-sdk'; { console.log('Widget is ready'); }} /> ``` ```js ``` ```js ``` ```js import { ApplicationsPortal } from '@descope/nextjs-sdk'; { console.log('Widget is ready'); }} /> ``` ```html // replace x.x.x with the latest release of the widget: https://www.npmjs.com/package/@descope/applications-portal-widget ``` ### Opening Apps in a New Tab By default, apps open in a new browser tab when a user clicks a tile. Open the **Applications Portal Widget** in the [Widgets page](https://app.descope.com/widgets) of the Descope Console, select the apps list component, go to the **Behavior** tab, and toggle **Open in New Tab** off to open apps in the current tab instead. ## Outbound Applications Widget The Outbound Applications Widget provides a list of [Outbound Applications](/identity-federation/outbound-apps) a user is associated with, and allows them to connect to each of them directly from a widget. The user will see outbound apps that are associated with their tenant, or available to the whole project, as well as whether or not they have already connected to the app. ![Descope outbound applications widget](/assets/outbound-applications-widget.webp) You cannot modify any underlying behavior of the outbound applications widget components, as there are no [default flows](/widgets/flows#configuring-component-flows) to modify. ```js import { OutboundApplications } from '@descope/react-sdk'; { console.log('Widget is ready'); }} /> ``` ```js ``` ```js ``` ```js import { OutboundApplications } from '@descope/nextjs-sdk'; { console.log('Widget is ready'); }} /> ``` ```html // replace x.x.x with the latest release of the widget: https://www.npmjs.com/package/@descope/outbound-applications-widget ``` ## Tenant Switcher Widget The Tenant Switcher Widget lets signed-in users switch their [active tenant](/client-sdk/auth-helpers#additional-helper-functions) when they are associated with multiple tenants. After a selection, the session reflects the chosen tenant (for example, the `dct` claim on the JWT), similar in effect to choosing a tenant with the [Switch Tenant](/flows/screens/inputs/tenantselect-component) flow component. Create the widget from the **Tenant Switcher** template on the [Widgets](https://app.descope.com/widgets) page in the Descope Console, then pass that widget's ID to `widgetId` (the examples below use the template default `tenant-switcher-widget`). You cannot modify any underlying behavior of the tenant switcher widget components, as there are no [default flows](/widgets/flows#configuring-component-flows) to modify. ![Descope tenant switcher widget](/assets/tenant-switcher-widget.webp) ```js import { UserProfile } from '@descope/react-sdk'; { // add here you own logout callback window.location.href = '/login'; }} /> ``` ```js ``` ```js ``` ```js import { UserProfile } from '@descope/nextjs-sdk'; { // add here you own logout callback window.location.href = '/login'; }} /> ``` ```html // replace x.x.x with the latest release of the widget: https://www.npmjs.com/package/@descope/user-profile-widget ``` # Deleting Your Account (/support/delete-company) Here you will find details on how to delete your account and company. # Deleting Your Account You can delete your Descope account and company directly from the Descope console. This is a **self-service feature** that allows you to permanently remove your account, company, and all associated data. Self-service deletion is available only for free accounts and permanently deletes your company, meaning the action cannot be undone and your data cannot be recovered. ## Delete Your Account and Company To delete your account and company: 1. Navigate to [Company Settings](https://app.descope.com/settings/company) 2. Scroll to the bottom of the page 3. Click the **Delete Company** button. 4. In the confirmation dialog: - Read the warning carefully - **this action cannot be undone** - Type your company name exactly as shown to confirm - Click **Delete** to proceed ![Delete company](/assets/delete-account.webp) ### What Gets Deleted When you delete your company, the following data is **permanently removed**: - Your company account - All projects within the company - All user accounts associated with the company (Descopers) - All authentication flows and configurations - All analytics and audit logs - All integrations and settings If you want to keep your company but remove specific projects, you can delete them from the **[Project Settings](/management/project-settings#project-management)**. # Feature Requests (/support/descope-fr-portal) Here you will find details on how to use the Descope Feature Request Portal. # Feature Requests The **Descope Feature Request Portal** exists for customers to submit their ideas for new features or upvote others' ideas that they like. The Descope product team then triages each idea that a customer submits. Updates on a feature request are then emailed to the user who opened the request and other customers who have voted for it. The Descope FR portal is available within the [Descope Console](https://app.descope.com/). Descopers must be authenticated to Descope to interact with the FR portal. ## Descope Feature Request Lifecycle Below is a schematic overview of the feature request's lifecycle: ![Testing connectors in Descope](/assets/descope-fr-lifecycle.webp) Let's break it down and deep dive into each state: - When a user opens a feature request, its status is set to **New**. - When it gets picked up by the product team for review, its status changes to **Triaged**. - When the product team reviews the request and finds it relevant for development, its status changes to **Planned**. - If the feature proposes an idea that Descope handles, its status changes to **Already Exists**. - If a fellow Descoper has already suggested the feature, its status will change to **Duplicate**. - If the feature is invalid, the product team will provide a proper explanation, and its status will change to **Closed**. - If the feature contradicts the current Descope vision, its status changes to **By Design**. - When the feature enters a sprint's plan (gets picked up by the Descope team for design and development) - its status changes to **In Progress**. - After a feature has gone successfully through all internal cycles - it will enter a queue that will be part of the next deployment. In that case, the status changes to **To Be Shipped**. - Once the feature is officially out and available in the Descope console, its status will change to **Completed**. ## How to Submit a Request 1. Go to the [Descope Console](https://app.descope.com/). In the Help Center, which is on the top-right corner of the console, click the 'Feature Requests' tab: ![Feature Request - Step 1](/assets/fr-submit-1.webp) 2. The Descope FR Portal will open up. Within the portal, you can review existing feature requests and see their current status, votes, and comments. It is essential to ensure the idea doesn't exist before submitting a new feature request. ![Feature Request - Step 2](/assets/fr-submit-2.webp) 3. When you navigate to an existing feature request, you can see its description and any comments left on it. You can comment on it and upvote it. Notice that for customer privacy - we don't expose the customer names, and they all appear as 'Someone'. ![Feature Request - Step 3](/assets/fr-submit-3.webp) 4. If your idea hasn't already been logged, open a new feature request by clicking the 'New Idea' button at the top-right corner of the FR Portal. ![Feature Request - Step 4](/assets/fr-submit-4.webp) 5. When submitting a new idea, you need to provide: - A clear title of the feature request is needed. - A description of the use case this feature request should solve. - Association of the idea to relevant product features of Descope ('Flows', 'Authentication Methods', etc). - Then, submit it by clicking the 'New Idea' button at the bottom. ![Feature Request - Step 5](/assets/fr-submit-5.webp) # Collect Debugging Information (/support/generate-debug-info) Learn how to generate debugging information when reporting issues to the Descope team. # Collect Debugging Information This guide will cover how to capture debugging information that the Descope team may need as they assist you with issues you may have with the product. It is always good to gather as much information as possible about a problem when it occurs to facilitate a swift resolution. ## Flows Within the flow builder in the Descope console, you can run and debug your flows. Capturing the errors that are presented and sharing them with the Descope team when reporting an issue is crucial. It is also best to [export](/management/flows#exporting-and-importing-flows) and share the flow with Descope as well. You can also capture applicable debug information live using the [flow debug flag](/handling-flow-errors/debug-flows). ## Configuration If you are troubleshooting issues with Custom Domains, templates, connectors, etc, please capture screenshots of the configuration and details of the applicable items potentially causing the problem. ## HAR and Console Logs It is critical to capture a HAR file and console logs when reporting issues with your application related to Descope. The below covers generating a HAR file and console log to be shared with the Descope team. When sharing a HAR file with the Descope team, go to the [Descope console](https://app.descope.com/), click the `?` icon in the top right of the UI, click `Get Help`, and start a new conversation, or open the relevant existing conversation, and upload the HAR file within the conversation. If it pertains to a flow, please also share an [export](/management/flows#exporting-and-importing-flows) of the flow. When troubleshooting, check if the same behavior reproduces within an incognito browser as well. ```text 1. Open Chrome and open the Developer Tools by selecting the ⋮ button > More Tools > Developer Tools. 2. Select the Network tab. You must keep it open while you reproduce the issue. 3. Find the round record button in the upper left corner of the tab, and make sure it is red. If it is grey, click the button once to start recording. 4. If not already selected, check the Preserve log box. 5. Now navigate to the page and reproduce the issue you have faced. 6. Click the download button in the header section above the timeline, filter within the row with the record, and preserve log configurations. Once you've clicked the download button, save the file with your desired filename. 7. Now navigate to the console tab. 8. Right click within the console log and click Save as. 9. Zip the HAR file and Console log and share with the Descope team. ``` ```text 1. Open Firefox and open the Developer Tools by selecting the three bars at the top right > More Tools > Web Developer Tools. 2. Select the Network tab. You must keep it open while you reproduce the issue. 3. Now navigate to the page and reproduce the issue you have faced. 4. Right click anywhere in the network trail and select Save All as HAR. 5. Now navigate to the console tab. 6. Right click within the console log and click Save all Messages to File. 7. Zip the HAR file and Console log and share with the Descope team. ``` ```text 1. Open Safari and select the Develop menu from the top of the screen - If you don't see the Develop menu in the menu bar, choose Safari > Settings, click Advanced, then select “Show features for web developers”. 2. Click Show Web Inspector. 3. Now navigate to the page and reproduce the issue you have faced. 4. Right click anywhere in the network trail and select Export HAR. 5. Now navigate to the console tab. 6. Select the messages within the console log, then right click and select Save Selected. 7. Right click within the console log and click Save all Messages to File. 8. Zip the HAR file and Console log and share with the Descope team. ``` ```text 1. Open Edge and then open Developer tools: To open DevTools, right-click the webpage, and then select Inspect. Or, press Ctrl+Shift+J (Windows, Linux) or Command+Option+J (macOS). DevTools opens. 2. Select the Network tab. You must keep it open while you reproduce the issue. 3. Find the round record button in the upper left corner of the tab, and make sure it is red. If it is grey, click the button once to start recording. 4. If not already selected, check the Preserve log box. 5. Now navigate to the page and reproduce the issue you have faced. 6. Right click anywhere in the network trail and select Save all as HAR with content. 7. Now navigate to the console tab. 8. Right click within the console log and click Save as. 9. Zip the HAR file and Console log and share with the Descope team. ``` ## Generating Video Sometimes a video of the behavior is worth a thousand words. When applicable, you may want to also provide the team with a video reproducing the issue along with the above mentioned items. There are a variety of tools that you can use to generate a screen recording. Some examples are listed below, but there are many more out there: - [QuickTime](https://support.apple.com/en-ie/guide/quicktime-player/qtp97b08e666/mac#:~:text=In%20the%20QuickTime%20Player%20app%20on%20your%20Mac%2C%20choose%20File,clicks%20in%20the%20screen%20recording.) - [Jam](https://jam.dev/) - [Microsoft Stream](https://support.microsoft.com/en-gb/office/microsoft-stream-screen-recorder-e98d8791-2b82-4dc7-889a-959724e3cbad) - Record as a meeting within your meeting provider like Zoom or Google Meets # Support (/support) Here you will find details on how to contact the Descope Support team # Support We are always here to help you with questions and issues or to chat about authentication and hear your ideas. ## Service Status ## Get in Touch We are sorry that you are experiencing a problem with the Descope integration. Our engineers like nothing more than to help other engineers. Feel free to contact us via one of the methods below. } /> } /> } className="pointer-events-none" /> # Account and Project Notifications (/support/mail-subscription) Learn how to subscribe to email-based project and account notifications with Descope. # Descope Account and Project Notifications Descopers can manage mail subscription about their accounts and projects. This guide outlines the different types of emails Descope may send, how to manage them via the Descope Console, and how to suppress certain emails using project-level configuration. ## Types of Emails You Might Receive - **Account Updates (Company):** Emails related to your overall Descope account — including license changes, trials, critical system alerts and more. - **Project Notifications:** Emails triggered by specific project activity — including usage limits, project misconfigurations, SMS overages, connector failures and more. ## How to Manage Email Subscriptions ### User Profile Email Settings You can choose which categories of emails you receive: 1. Go to the [Descope Console](https://app.descope.com/) 2. Click your **Name/Email** (in the top right) → **Settings** 3. Under **Mail Subscription**, choose one of the following options: - **All** – Receive both Account and Project notifications - **Account Updates** – Only receive account-related emails - **Project Notifications** – Only receive project-specific alerts - **None** – Opt out of all emails ![Mail Subscription Console](/assets/mail-subscription-console.webp) ### Excluding Project Related Emails Even if you have project notifications turned on in the section mentioned above, you can disable emails for a specific project with tags. Only project notification emails will be affected and all other emails will still be sent. 1. Go to the [Project Settings](https://app.descope.com/) section in the Descope Console 2. Add `disable-journey-emails` as a tag, under the **Tags** section ![Mail Subscription Project](/assets/mail-subscription-project.webp) # create-descope-app (/cli/create-descope-app) Use the create-descope-app CLI to create a new Descope application. # create-descope-app The `create-descope-app` CLI tool allows you to create a new Descope application using a default template. It is a quick and easy way to get started with Descope. Basic usage: ```bash npx create-descope-app [project-name] [options] ``` ## Reference | Option | Description | | --- | --- | | `-h` or `--help` | Display help for the command | | `-v` or `--version` | Display version number | | `-t` or `--template` | Use a specific template when creating the app | ## Examples To create a new app using a template, run the following command in your terminal: ```bash npx create-descope-app@latest ``` This command will prompt you to enter a project name and select a template. ```bash What is your project named? my-app Would you like to use TypeScript? No / Yes ? Please select a language/framework: (Use arrow keys) ❯ nextjs react angular remix html flutter swift ? Select a template for nextjs: (Use arrow keys) ❯ next-js-sample-app nextjs-demo-app-router nextjs-hackathon-template ``` Once you've entered your project name and selected a template, the CLI will create a new project with your chosen configuration. # descope (/cli/descope) Streamline DevOps and Easily Perform Common Tasks with Descope's Command Line Tool # descope The descope Command Line Interface (CLI) helps manage your Descope project by leveraging its management APIs. It enables handling tasks like creating users, managing tenants, generating access keys, and modifying project settings through the command line. With support for exporting, validating, and importing snapshots, the CLI simplifies project configuration and transfer. Its ability to output JSON makes it ideal for automation in scripts and CI/CD workflows, saving time and reducing errors. To see more about our CI/CD check out our [Github CI/CD guide](/managing-environments/manage-envs-in-github) and [Github CI/CD Template](https://github.com/descope/project-cicd-template). ## Prerequisites The repository for the descope CLI can be found [here](https://github.com/descope/descopecli). 1. Install the CLI tool according to your device, instructions can be found in the repository. 2. You will need the Project ID for the project you want to manage, this can be found in [Project Settings](https://app.descope.com/settings/project) 3. You will also need to generate a management key within [Company Settings](https://app.descope.com/settings/company/managementkeys). To set your Project ID and Management Key for the CLI, use the commands below. If you want to update them permanently add them to your terminal profile, such as `.zshrc` or `.bashrc`. ``` export DESCOPE_PROJECT_ID='__ProjectID__' export DESCOPE_MANAGEMENT_KEY='' ``` If you need help on any command or sub-command you can put the `-h` flag at the end of the full command to get more information and flags. ## Entity Management With the CLI you can create and manage different Descope entities. You can create: [Users](/management/user-management), [Access Keys](/management/m2m-access-keys), [Tenants](/management/tenant-management), and [Inbound Applications](/identity-federation/inbound-apps). ### Access-Keys These are the available commands for creating and managing access keys: ``` activate Activate an access key create Create a new access key deactivate Deactivate an access key delete Delete an access key load Load details about an access key load-all Load all access keys ``` When creating an access key there are several attributes you can pass in. You can get these attributes with the `-h` flag: ``` descope access-key create -h ``` All the information about the command is returned: ``` Usage: descope access-key create [-d description] [-e time] [-t tenants] [-u userId] Flags: -d, --description string an optional description for the access key -e, --expires int the access key's expiry time (unix time in seconds, default 0 to not expire) -t, --tenants strings a comma separated list of tenant ids for the access key -u, --userId string an optional user id to adopt authorizations from -j, --json use JSON output format ``` As an example say we wanted to create an access key that expires Nov 21, 2026 which is 1795345322 Unix Time, is associated with a tenant, and takes all the project and tenant level roles and permissions of one of our users: ``` descope access-key create exampleKey -e 1795345322 -u 'U2piAnrm6sV1OKyGTUejqddWVSai' -t 'acmecorp' -j ``` The access key is returned, it is in the `cleartext` field: ``` { "accessKey": { "boundUserId": "U2piAnrm6sV1OKyGTUejqddWVSai", "cleartext": "K2pjAnGVR7ez6pVqSI9mCul88IppLc0FpV9WQVnDBwRARNr5BaU56hnN61Gvm5LoWD2YW7S", "clientId": "UDJsTm50UzhRcDdVOTVOaXVyWUZRaEsxbkRUUjpLMnBqQW5HVlI3ZXo2cFZxU0k5bUN1bDg4SXBw", "createdBy": "K2pB49mv9n7WCVbbcC7KKkXM5jo4", "createdTime": 1733269275, "expireTime": 1795345322, "id": "K2pjAnGVR7ez6pVqSI9mCul88Ipp", "keyTenants": [ { "tenantId": "acmecorp", "tenantName": "AcmeCorp" } ], "name": "exampleKey", "status": "active" }, "ok": true } ``` You can see your newly created access key in the [Access Key](https://app.descope.com/accesskeys) tab. ![Newly Created Access Key](/assets/access-key-created-cli.webp) To activate or deactivate an access key: ``` descope access-key activate -j descope access-key deactivate -j ``` To load a specific access key: ``` descope access-key load -j ``` To load all access keys: ``` descope access-key load-all -j ``` To delete an access key: ``` descope access-key delete -j ``` ### Tenants These are the available commands for creating and managing tenants: ``` create Create a new tenant delete Delete a tenant load Load details about a tenant load-all Load all tenants ``` When creating a Tenant we can pass the following flags: ``` Usage: descope tenant create Flags: -i, --id string an optional custom id for the tenant -d, --domains strings a comma separated list of self provisioning domains for the tenant -j, --json use JSON output format ``` As an example let's create a tenant with self provisioning domains and a custom tenant id: ``` descope tenant create 'TestTenant' -i 'testtenant' -d 'example.com','example.org' -j ``` To load the tenant we just created we can use the `load` command: ``` descope tenant load 'testtenant' -j ``` We can see the name, self-provisioning domains, authentication methods, and created time: ``` { "ok": true, "tenant": { "id": "testtenant", "name": "TestTenant", "selfProvisioningDomains": [ "example.com", "example.org" ], "authType": "none", "createdTime": 1733339035 } } ``` We can see our tenant and its settings in the [Tenants Page](https://app.descope.com/tenants). ![Newly Created Tenant](/assets/tenant-created-cli.webp) To load all tenants: ``` descope tenant load-all -j ``` To delete a tenant: ``` descope tenant delete -j ``` ### Users These are the available commands for managing and creating users and test users: ``` activate Activate a user create Create a new user deactivate Deactivate a user delete Delete a user load Load details about a user load-all Load all users password Commands for managing user passwords roles Commands for managing user roles test Commands for creating and managing test users ``` The following flags are available for user creation: ``` Usage: descope user create [-e email] [-p phone] [-n name] [-t tid,...] Flags: -e, --email string the user's email address -p, --phone string the user's phone number -n, --name string the user's display name -t, --tenants strings a comma separated list of tenant ids for the user -j, --json use JSON output format ``` As an example let's say we wanted to create a user associated with tenants, we can do the following: ``` descope user create 'john@example.com' -p '+11234567890' -t 'acmecorp','testtenant' -j ``` They will be set as invited in the descope console: ![Newly Created User](/assets/user-created-cli.webp) Their status can be manually changed from `Invited` to `Active` using the `activate` sub-command. ``` descope user activate 'john@example.com' -j ``` Now if we load the user we can see their status is set to `enabled`: ``` descope user load -l 'john@example.com' -j ``` ``` { "ok": true, "user": { "phone": "+11234567890", "userId": "U2poJTufi0fAPgCEySTao5qmtk6j", "loginIds": [ "john@example.com" ], "verifiedPhone": true, "userTenants": [ { "tenantId": "acmecorp", "tenantName": "AcmeCorp" }, { "tenantId": "testtenant", "tenantName": "TestTenant" } ], "status": "enabled", "createdTime": 1733426505 } } ``` To load a user by `userId` instead of `loginId`: ``` descope user load -u 'U2poJTufi0fAPgCEySTao5qmtk6j' -j ``` To load all users with pagination: ``` descope user load-all -l 50 -p 0 -j ``` ``` Flags: -l, --limit int the number of results for pagination (max 100) -p, --page int the number of page for pagination (default 0) ``` To delete a user: ``` descope user delete -l 'john@example.com' -j descope user delete -u 'U2poJTufi0fAPgCEySTao5qmtk6j' -j ``` To deactivate a user: ``` descope user deactivate 'john@example.com' -j ``` #### User Roles We can manage a user's roles through the `roles` sub-command. These are the available commands for roles: ``` add Add roles to a user remove Remove roles from a user set Set the roles for a user ``` Adding a role will append to current roles. Setting roles will overwrite any existing roles. Here is an example of adding a tenant-level role to a user: ``` descope user roles add john@example.com -t 'acmecorp' -r 'Tenant Admin','acmeRole' -j ``` If we wanted to add project-level roles we can run the command again with no input for tenant. To set roles (overwrites existing roles): ``` descope user roles set john@example.com -r 'Admin','User' -j descope user roles set john@example.com -t 'acmecorp' -r 'Tenant Admin' -j ``` To remove roles: ``` descope user roles remove john@example.com -r 'Admin' -j descope user roles remove john@example.com -t 'acmecorp' -r 'Tenant Admin' -j ``` #### User Passwords We can set active and temporary passwords for a user as well as expire their existing password using the `password` sub-command: ``` expire Expire a user's password set-active Set an active password for a user set-temporary Set a temporary password for a user ``` We can set a temporary password which will require a user to change their password on the next authentication: ``` descope user password set-temporary 'michael.rimboim@descope.com' 'a8 re7f9E' -j ``` This can be accomplished by having a replace password section of the flow like below, the user will be able to use this temporary password to create a new one: ![Password reset flow](/assets/temp-password-flow-cli.webp) ![Password reset screen](/assets/temp-password-screen-cli.webp) To set an active password: ``` descope user password set-active 'john@example.com' 'MySecurePassword123!' -j ``` To expire a user's password: ``` descope user password expire 'john@example.com' -j ``` #### Test Users Using the CLI we can programmatically create test users and generate logins for these users with the `test` sub-command: ``` create Create a new test user delete-all Delete all existing test users in the project generate Commands for generating logins for test users ``` Creating a test user works the same as creating a regular user: ``` descope user test create test@descope.com -p "+19999999999" -j ``` You can generate a test verification code for a test user. Let's create a SMS OTP we can use and reuse for this test user: ``` descope user test generate otp sms test@descope.com ``` We get back the OTP code we can use in place of a real OTP code during verification. The `generate` sub-command supports the following methods: ``` otp Generate an OTP for a test user using email, sms, or voice magic-link Generate a magic link for a test user using email or sms enchanted-link Generate an enchanted link and a pendingRef which is used to poll for a valid session ``` Examples: ``` descope user test generate otp email test@descope.com -j descope user test generate otp sms test@descope.com -j descope user test generate otp voice test@descope.com -j ``` For magic link: ``` descope user test generate magic-link email test@descope.com -j descope user test generate magic-link sms test@descope.com -j descope user test generate magic-link email test@descope.com -u 'https://example.com/callback' -j ``` ``` Flags: -u, --redirect-url string override the redirect URL configured for enchanted link in the project configuration ``` For enchanted link: ``` descope user test generate enchanted-link test@descope.com -j descope user test generate enchanted-link test@descope.com -u 'https://example.com/callback' -j ``` To delete all test users: ``` descope user test delete-all -j ``` ### Inbound Applications These are the available commands for managing inbound apps: ``` create Create a new inbound app update Update an existing inbound app delete Delete an inbound app load Load details about an inbound app load-all Load all inbound apps secret Commands for managing inbound app secrets ``` When creating an inbound app, you must provide a flow hosting URL and at least one permission scope (you may provide more than one if needed): The following example demonstrates how to provide multiple permission scopes using the `-p` flag. Only one permission scope is required, but you can specify additional scopes as needed. ``` Usage: descope apps inbound create [-f flow-hosting-url] [-p permission-scopes] Flags: --description string an optional description for the inbound app -f, --flow-hosting-url string the flow hosting URL for the inbound app -c, --callback-url strings can be used multiple times to add approved callback URLs. For example: -c 'https://example.com' -p, --permission-scope strings can be used multiple times to add permission scopes, where each value is expected to be a comma separated list with the scope name, description, and a list of roles separated by colons. For example: -p 'write,Allow writing files,User|Reader|Writer' -p 'guest,Guest user with no roles,-' -a, --attribute-scope strings can be used multiple times to add attribute scopes, where each value is expected to be a comma separated list with the scope name, description, and a list of user attributes separated by colons. For example: -a 'contact,Fetch user contact details,displayName|email|phone' -j, --json use JSON output format ``` Example of creating an inbound app: ``` descope apps inbound create 'MyApp' -f 'https://app.example.com' -p 'read,Read access,User|Reader' -p 'write,Write access,Writer|Admin' -c 'https://app.example.com/callback' -j ``` The inbound app will be created with the following settings: ``` { "app": { "id": "TPA36iegoe4RLOGDuazm4SPVwZCXN8", "name": "MyApp", "description": "", "logo": "", "loginPageUrl": "https://app.example.com", "clientId": "UDM2aWUzUHBVeENrb2ZFUnVPSmhWNXkxODdNZzpUUEEzNmllZ29lNFJMT0dEdWF6bTRTUFZ3WkNYTjg=", "approvedCallbackUrls": [ "https://app.example.com/callback" ], "permissionsScopes": [ { "name": "read", "description": "Read access", "values": [] }, { "name": "write", "description": "Write access", "values": [] } ], "attributesScopes": [], "jwtBearerSettings": {} }, "ok": true, "secret": "PxpQ1gF9pKDfMuFqsVC5HyyxCpI7feWZmH9OFN6ylxn" } ``` ![Newly Created Inbound App](/assets/inbound-app-created-cli.webp) To update an inbound app: ``` descope apps inbound update [-f flow-hosting-url] [-p permission-scopes] ``` To load an inbound app: ``` descope apps inbound load -j ``` To load all inbound apps: ``` descope apps inbound load-all -j ``` To delete an inbound app: ``` descope apps inbound delete -j ``` #### Inbound App Secrets The `secret` sub-command allows you to manage secrets for inbound apps: ``` load Load the secret for an inbound app rotate Generates a new secret for an inbound app ``` To load the secret for an inbound app: ``` descope apps inbound secret load -j ``` To rotate the secret for an inbound app: ``` descope apps inbound secret rotate -j ``` ## Project Configuration The descope CLI allows you to manage [audit events](#audit-events), [Flows](#flows), [Project Management](#project-management), [Project Snapshots](#project-snapshots), and [Themes](#themes). ### Audit Events You can use the `audit search` command to do a fuzzy search of audit logs from the past 30 days: Here is an example of searching for all audit logs that include the word Mozilla: ``` descope audit search "Mozilla" -j ``` To store logs longer than 30 days you must stream them to your own service, read more [here](/audit-trails-and-integrations). ### Flows Using the CLI we can programmatically import and export flows: ``` convert Convert a flow between formats export Export a flow to a JSON file or standard output import Import a flow from a JSON file list Lists all flows in a project ``` Here is an example of exporting a flow to a JSON file: ``` descope flow export "sign-up-or-in" > sign-up-or-in.json ``` To export a flow to a specific file: ``` descope flow export "sign-up-or-in" sign-up-or-in.json ``` To list all flows: ``` descope flow list -j ``` To import a flow from a JSON file: ``` descope flow import "sign-up-or-in" sign-up-or-in.json ``` The `convert` command allows you to convert flows between different formats: ``` Usage: descope flow convert [targetPath] [-s] Flags: -s, --skip skip unsupported file types -j, --json use JSON output format ``` The convert command supports: - Converting from snapshot format (directory with metadata.json, contents.json, and screen files) to exported format (single JSON file) - Converting from console format (flow and screens structure) to exported format - Converting from exported format to snapshot format Example: ``` descope flow convert flow-snapshot/ sign-up-or-in.json descope flow convert sign-up-or-in-console.json sign-up-or-in.json descope flow convert sign-up-or-in.json flow-snapshot/ -s ``` ### Project Management Use these commands to clone, list, and delete projects in your company: ``` clone Clone an existing project along with all settings and configurations list Lists all projects in a company delete Delete an existing project ``` For exporting and importing project configuration between environments, see [Project Snapshots](#project-snapshots). #### Clone Project To clone an existing project: ``` Usage: descope project clone [-e environment] [--tags tag,...] Flags: -e, --environment string an optional environment for the new project, only valid value is production --tags strings a comma separated list of tags for the new project -j, --json use JSON output format ``` Example: ``` descope project clone P2Z1234567890123456789012345 'My New Project' -e production --tags 'production','backend' -j ``` #### List Projects To list all projects in a company: ``` descope project list -j ``` #### Delete Project To delete a project: ``` Usage: descope project delete [-f] Flags: -f, --force skips the prompt and deletes the project immediately -j, --json use JSON output format ``` The `--force` flag is required when using `--json` to delete a project. Example: ``` descope project delete P2Z1234567890123456789012345 -f ``` ### Project Snapshots The `snapshot` subcommand exports, imports, and validates a portable copy of your project configuration. These commands are useful for CI/CD and promoting changes between environments. ``` export Export a snapshot of all the settings and configurations of a project import Import a snapshot into a project validate Validate a snapshot before importing into a project ``` See the [Project Snapshot](/how-to-deploy-to-production/project-snapshot) guide for what an export includes (flows, connectors, Resources / MCP servers, styles, and more) and how the directory is laid out. To learn more about project import/export and how to handle project secrets properly with our GitHub CI/CD template, see [here](/managing-environments/manage-envs-in-github), or Gitlab CI/CD template [here](/managing-environments/manage-envs-in-gitlab). #### Export Snapshot ``` Usage: descope project snapshot export [-p path] Flags: -p, --path string the path to write the snapshot into --no-assets don't extract assets from snapshot files -j, --json use JSON output format ``` If no path is specified, the snapshot will be exported to a directory named `project-`. Example: ``` descope project snapshot export P2Z1234567890123456789012345 -p ./my-snapshot ``` #### Import Snapshot ``` Usage: descope project snapshot import [-p path] Flags: -p, --path string the path to read the snapshot from --secrets-input string the path to a JSON file with required secrets -j, --json use JSON output format ``` If no path is specified, the snapshot will be read from a directory named `project-`. Example: ``` descope project snapshot import P2Z1234567890123456789012345 -p ./my-snapshot --secrets-input secrets.json ``` #### Validate Snapshot ``` Usage: descope project snapshot validate [-p path] Flags: -p, --path string the path to read the snapshot from --secrets-input string the path to a JSON file with required secrets --secrets-output string the path to a JSON file to write missing secrets in case validation fails --failures-output string the path to write a list of failures in case validation fails -j, --json use JSON output format ``` Example: ``` descope project snapshot validate P2Z1234567890123456789012345 -p ./my-snapshot --secrets-output missing-secrets.json --failures-output failures.txt ``` If validation fails, the command will exit with status code 2. The `--secrets-output` flag can be used to generate a template file with all missing secrets that need to be provided. ### Themes Using the CLI tool you can manage the export and import of the project theme. These are the available commands: ``` export Export the project theme to a JSON file or standard output import Import the project theme from a JSON file ``` Exporting the theme gives a JSON file with all the styles that exist in a project. ``` descope theme export testTheme.json ``` To export to standard output: ``` descope theme export -j ``` To import a theme: ``` descope theme import testTheme.json ``` # CLI (/cli) Use the Descope CLI to build applications on top of the Descope platform. # CLI Descope comes with **two** Command Line Interface (CLI) tools: - `create-descope-app`: Quickly create a new Descope application from an [example](https://github.com/descope-sample-apps) template. - `descope`: Easily manage your project with the Descope Management APIs. # Custom Domain (/how-to-deploy-to-production/custom-domain) Guide describing how to configure CNAME and manage sessions within cookies with Descope. # Custom Domain This guide walks you through configuring a custom domain for your Descope project so you can manage sessions in cookies on a hostname under your application. By default, Descope returns session and refresh tokens in the response body, and client SDKs typically store them in local storage. That works fine for getting started, but in production you'll usually want the **refresh token** in an `HttpOnly` cookie instead. Refresh tokens are long-lived, and keeping them out of JavaScript helps mitigate [XSS](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting) attacks. Because Descope cookies use [`SameSite=Strict`](https://owasp.org/www-community/SameSite) by default, the cookie domain has to be a subdomain of your site — which means you'll need a CNAME pointing at Descope. For more on `SameSite` and domain scope, see [Cross-Site Cookies](/security-best-practices/crossite-cookies). For why cookies are the recommended storage approach, see [Storing Refresh Tokens](/security-best-practices/refresh-token-storage). If you want the broader picture of how Base URLs relate to **region** (US, EU, AP, CA, UK, SG, SA), custom domains, and **[private cloud](/how-to-deploy-to-production/private-cloud)** deployments, see [Multi-Region Support](/management/project-settings/multi-regional#descope-base-urls). Configuring a Custom Domain is a Pro+ tier feature. Learn more about upgrading your tier within our [pricing overview](https://www.descope.com/pricing). ## Configure Custom Domain Each company has a limit of 10 total number of custom domains across all projects within a company. If you've reached that limit and need more, contact [Descope Support](/support). The steps below assume your app lives at `app.example.com` and you are **not** sharing cookies across multiple subdomains. If you do need to share cookies (for example between `app.example.com` and `docs.example.com`), the same flow still applies — you'll just use a different hostname. See [Sharing Cookies Across Multiple Subdomains](#sharing-cookies-across-multiple-subdomains) for those details. ### Create a DNS Record Start by creating a **CNAME** that maps a hostname you own (for example `auth.app.example.com`) to Descope's regional target. Pick the row that matches your project's region: | Region | CNAME target | | :----- | :------- | | US | `cname.descope.com` | | EU | `cname.euc1.descope.com` | | AP | `cname.aps2.descope.com` | | CA | `cname.cac1.descope.com` | | UK | `cname.euw2.descope.com` | | SG | `cname.aps1.descope.com` | | SA | `cname.sae1.descope.com` | A common setup looks like this: - **Name**: `auth.app.example.com` - **Type**: CNAME - **TTL**: keep the default - **Value**: the regional target from the table (for an EU project, that would be `cname.euc1.descope.com`) One thing to keep in mind: this hostname is only for DNS. After verification, you'll point the SDK at your custom HTTPS origin (for example `https://auth.app.example.com`), not at `cname.descope.com`. If you're on a [private cloud](/how-to-deploy-to-production/private-cloud) Descope environment, reach out to your CS manager for the correct CNAME target. ![Descope custom domain cname host record](/assets/cname-dns-record.webp) ### Configure App URL Next, open [Project Settings](https://app.descope.com/settings/project) → **General** and set **App URL** to where your application actually lives — for example `https://app.example.com`. This is the value Descope uses by default when figuring out cookie domain scope. You only need to change it if your app runs on a different origin than what's already set. ![Descope configure custom domain cname within Descope project](/assets/configure-app-url.webp) ### Add and Verify the Custom Domain Once the App URL is in place, you'll see a **Configure Custom Domain** section below it. Add the CNAME hostname you created in DNS, then click **Refresh** until the status shows **Setup complete**. When verification finishes, the Console also gives you code snippets for setting `baseUrl` in your frontend — which is the next step. ![Descope configure custom domain cname within Descope project](/assets/setup-complete.webp) ## Base URL for the Descope SDK With the domain verified, initialize the Descope SDK with `baseUrl` set to your custom HTTPS origin — the same hostname you added in DNS (for example `https://auth.app.example.com`). Don't use the regional `cname.*.descope.com` target here; that only belongs in DNS. ```javascript ``` ```html ``` ```js ``` If you also want static assets served from your custom domain instead of `static.descope.com`, you can set `baseStaticUrl` as well. See [Base URL configuration](/client-sdk/descope-components#base-url-configuration). For more background on regional vs custom Base URLs, see [Descope Base URLs](/management/project-settings/multi-regional#descope-base-urls). ## Manage Tokens in Cookies Now that the domain is ready, you can tell Descope to deliver tokens as cookies. Head to **Token Response Methods** on the [Session Management](https://app.descope.com/settings/project/session) page, and choose **Manage in cookies** for the refresh token (and optionally for the session token as well). Out of the box, Descope uses the defaults below. You usually don't need to change them unless you're running into cookie name conflicts or sharing cookies across subdomains: | Setting | Default | Where to change it | | --- | --- | --- | | Refresh cookie name | `DSR` | [End action](/flows/actions/end-action#refresh-cookie-name) in your flow | | Session cookie name | `DS` | [End action](/flows/actions/end-action#session-cookie-name) in your flow | | Cookie domain | Derived from your **App URL** (for example `app.example.com`) | [Session Management](https://app.descope.com/settings/project/session) — see also [Affected Settings](#affected-settings) | | `SameSite` / cookie policy | Project cookie settings | [Cross-Site Cookies](/security-best-practices/crossite-cookies) and [Token Response Methods](/management/project-settings#token-response-methods) | Custom cookie names are especially useful when multiple Descope projects share the same root domain and would otherwise both try to write `DS` / `DSR`. ![Descope configure custom domain cname within Descope project](/assets/set-strict-cookies.webp) ### Testing Cookie Storage If you wish to store the cookie values with a different name, you can do so by modifying the [End action](/flows/actions/end-action) in your flow. Once that's configured, run through a login flow and confirm the cookies show up on your app domain. By default you should see `DSR` (the refresh token) as an `HttpOnly` cookie. The session token (`DS`) often remains available to the client unless you've also chosen to manage the session token in cookies. If you're testing locally against a custom domain and run into cookie issues, see [Local Testing Tokens](/unit-testing/local-testing-tokens). ## OAuth callback domain After your custom domain is set up, you can use it as the OAuth callback domain too. On each OAuth provider — under [Authentication Methods](https://app.descope.com/settings/authentication/social) or [Tenants](https://app.descope.com/tenants) — set the callback domain to your custom hostname (for example `auth.app.example.com`). ![Descope custom domain for OAuth](/assets/custom-domain-for-oauth.webp) ## Sharing Cookies Across Multiple Subdomains If you have more than one subdomain that needs to stay logged in together — say `app.example.com` and `docs.example.com` — you have two main options. The right choice depends on whether you want cookies shared automatically, or you'd rather keep refresh isolated on an auth host. ### Option 1: Cookie domain on `example.com` Use this when you want a seamless login experience across every subdomain. Set the cookie / custom domain scope to `example.com` instead of a single subdomain like `app.example.com`. The browser will then send the auth cookies with requests to any host under `*.example.com`. ### Option 2: Cookie domain on `auth.example.com` Use this when you want to keep the refresh token off your product subdomains, but still let them refresh sessions. Store the refresh token only on `auth.example.com`. Your other apps (`app.example.com`, `docs.example.com`, and so on) don't read that cookie directly — they call `auth.example.com` when they need a new session, and the browser includes the cookie on those requests automatically. ### Example settings Here's how those two approaches look side by side for `example.com`: | Setting | Strict (app only) | Shared across subdomains | | --- | --- | --- | | CNAME | `auth.app.example.com` | `auth.example.com` | | Cookie domain | `app.example.com` | `example.com` | | Base URL | `https://auth.app.example.com` | `https://auth.example.com` | ## Affected Settings When you configure a custom domain, several places in the Console update to match. Most of these are covered in the steps above, but here's a quick rundown of everything that's affected: 1. **Approved Domains** — Domains that are allowed to interact with your Descope project via the SDK and APIs. ![Approved domain with custom domain](/assets/approved-domains-custom-domains.webp) 2. **Cookie Domain** — The domain scope for cookies that hold the refresh token. This defaults from your App URL; widen it to the parent domain only if you need [shared cookies](#sharing-cookies-across-multiple-subdomains). ![Cookie Domain for refresh token with custom domain](/assets/cookie-domain-custom-domains.webp) 3. **OAuth Callback URL and Callback Domain** — The redirect URL and callback domain used during OAuth flows. After you change these in Descope, update the matching values in your OAuth provider (Google, Facebook, and so on). ![Descope custom domain for OAuth](/assets/oauth-callback-custom-domains.webp) 4. **Passkey Top-Level Domain** — The top-level domain passkeys are associated with. ![Passkey top level domain](/assets/passkey-custom-domains.webp) 5. **Base URL** — The host your SDKs and clients use when talking to Descope APIs. ![Base URL for Custom Domain](/assets/base-url-custom-domains.webp) 6. **SAML ACS URL** — The Assertion Consumer Service URL in SAML SP configuration for tenants. ![SAML ACS URL with custom domain](/assets/acs-url-custom-domains.webp) # Deploy to Production (/how-to-deploy-to-production) Guide describing things to handle when deploying to Production with Descope. # Deploy to Production This document outlines the key considerations and steps for deploying Descope in a production environment. If your company runs a [private cloud deployment](/how-to-deploy-to-production/private-cloud) of Descope, start with that guide for environment-specific base URLs, networking, and feature enablement. The checklist below still applies, but CNAME targets, static IPs, and some feature URLs differ from public SaaS. ## Pre Deployment Checklist - Project Level This section of the pre-deployment checklist covers configurations that should be done at the Descope project level. ### 1. Set Up Custom Domain Configure your custom domain, including creating a DNS record, updating base & base static url in your code, etc. For more details on how to set up a custom domain, check out our [custom domain guide](/how-to-deploy-to-production/custom-domain). ### 2. Configure Approved Domains In project settings, set up your list of approved domains that are allowed for redirect. If the value is left empty, the application will not perform validation, which could allow malicious actors to exploit vulnerabilities in your application. Descope also automatically protects against cross-company domain misuse by validating that flows are not running on domains belonging to other companies. This security feature works automatically without any configuration. For more details on how to set up approved domains and understand cross-company domain protection, refer to [this doc](/management/project-settings#approved-domains). ### 3. Configure SMS/Email Messaging Connectors Descope provides the first 100 SMS messages and used for authentication messages per month. In order to prevent authentication issues when this limit is reached, it is critical to set up [messaging connectors](/connectors/connector-configuration-guides/messaging) before deploying to production. Additionally, setting up messaging connectors for both SMS messages and emails allows you to create [custom messaging templates](/flows/actions/email-sms-templates-in-flows) with your own branding. ### 4. Disable Static OTP Codes If you have enabled [static OTP codes](/test-users#test-user-verifiers) for your test users, disable them before deploying to production to prevent unauthorized access using test credentials. If you must have static OTP enabled in production for mobile review purposes, you should track audit events for usage of static OTP to prevent malicious users from authenticating with it. ### 5. Configure Custom OAuth Providers Descope comes pre-configured with a default Descope authentication account for each OAuth provider to make developing and testing easier. We also provide the first 100 OAuth logins per month across all default providers for testing. However, you cannot customize the OAuth sign-in page with your branding when using the default provider, nor can you use the default provider in production. Therefore, it's important to make sure to [set up custom providers](/auth-methods/oauth/providers) to use in your production environment. ### 6. Configure Your Cookie Policy Configure your cookie policy, as described in our [cross-site cookie policy guide](/security-best-practices/crossite-cookies), to ensure cookies are handled the best way for your use case. ### 7. Block Self-Registration Sign Up [Block user sign up](/management/project-settings#sign-up) if your application wants to restrict users from creating accounts on their own. Instead, users will be able to sign in only after they have been invited, or if they are part of a domain that has [self-registration enabled on the tenant level](/management/tenant-management/tenant#email-domain). ### 8. Configure Audit Log Streaming Descope stores audit history for a limited time of 30 days. If you wish you have access to audits for an extended period, we offer [connectors](/connectors/connector-configuration-guides/analytics) to replicate the audit trail. We offer an [audit webhook connector](/connectors/connector-configuration-guides/network/audit-webhook) to send audit events to your own api, as well as audit streaming connectors, like [Sumo Logic](/connectors/connector-configuration-guides/audit-and-troubleshooting/sumologic), [Datadog](/connectors/connector-configuration-guides/audit-and-troubleshooting/datadog), [AWS S3](/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-s3), and more, to collect logs and analytics. ### 9. Create Production Project Typically, you want to have separate Descope projects for your development, staging, and production environments. To learn how to clone your development project and set up additional environments, check out our [managing environments doc](/how-to-deploy-to-production/managing-environments). ### 10. Review Your Access & Management Keys If you are utilizing [access keys](/management/m2m-access-keys) or [management keys](/management#management-keys), make sure that you use the expiry mechanism and rotate the keys regularly. Consider limiting the scopes of your keys by setting permitted IPs and roles. ### 11. Disable Descope Components Not In Use When you are not actively using a Flow or an authentication method in Descope, you can disable it from the Console. To disable a flow, navigate to the [Flows tab](https://app.descope.com/flows) of the Descope console, and select disable next to the relevant flow. To disable a widget, navigate to the [Widgets tab](https://app.descope.com/widgets) of the Descope console, and select disable next to the relevant widget. #### Disabling SDKs and APIs By default, SDKs and APIs are enabled. They should be disabled if you are using Flows, and not using client SDKs. Under each authentication method, you can disable API and SDK access for that method. ![Disabling SDKs and APIs Project Settings](/assets/disabling-sdks-and-apis.webp) If you're using Flows, and not using client SDKs, you should disable API and SDK access for all authentication methods. If you're using backend SDKs, you should also disable API and SDK access, however, you can use a Management Key to authenticate with the Descope Auth APIs. ### 12. Preventing User Enumeration Make sure your authentication flows do not leak information that could help attackers guess whether or not specific user accounts exist. To mitigate this risk, you can follow the steps in our [preventing user enumeration guide](/security-best-practices/preventing-user-enumeration). ## Pre Deployment Checklist - Application Level This section of the pre-deployment checklist covers configurations that should be done at your application level. ### 1. Remove Console Log Messages Make sure all console log messages are removed to ensure no critical information like emails or passwords is leaked. The `logger` component in Descope SDKs contains these messages and can be set to ignore the info logs. ### 2. Limit API Calls To avoid hitting the API rate limit, ensure your app is optimized to make the least number of requests possible based on the need. Consider using Descope SDKs, which have a higher rate limit, to make requests wherever possible. To learn more about rate limits, refer to our [rate limit doc](/rate-limiting). ### 3. Set Up Automated Testing Ensure that you have thoroughly tested all essential use cases of your application and integrations across all devices that your end users may utilize. We have guides for automated testing with [Cypress](/unit-testing/e2e-testing-guides/e2e-cypress) and [Playwright](/unit-testing/e2e-testing-guides/e2e-playwright). # Managing Environments (/how-to-deploy-to-production/managing-environments) Learn how to manage your Descope environments and migrate your configurations between non-production and production environments. # Managing Environments Descope allows you to create many projects to fit your needs. You could have multiple projects for different businesses, or you could have a development, staging, and production projects for the various stages of your development and testing. Descope makes it easy to manage your projects and migrate configurations between projects. ## Creating Projects Within the Descope console, clicking the project drop down in the top right allows you to create new projects by clicking the `+ Project` button at the bottom of the list of your projects. When you create a new project, you can provide the project with a name and choose whether it is `production` or `non-production`. You can later change whether the project is `production` or `non-production` from the [Project Settings](https://app.descope.com/settings/project) page within the console. Note that selecting `production` or `non-production` does not effect the features within the project. This is an internal identifier for Descope to see which projects are marked as `production`. ![Creating a new project within Descope](/assets/create-new-project.webp) ## Managing Secrets in Environments When working with environments, it's essential to note Descope's behavior in handling API keys and other related secrets around connectors and OAuth provider configurations. The below outlines how Descope manages secrets and credentials during project cloning, exporting, and importing. - During the cloning of a project, secrets are cloned to the new project with all of their current configuration and settings. - While exporting a project, the exported project zip will contain the connector or OAuth provider configuration; however, the credentials will be removed to prevent the export of secrets. - When you export a project and import it into another project that already has the same connectors or OAuth providers configured, the destination project will keep its existing secrets—unless you explicitly override them by editing the secrets in the exported ZIP file. All other connector or OAuth provider settings will be updated during the import. For details on overriding secrets, refer to the documentation on [secret placeholders](/how-to-deploy-to-production/managing-environments#secret-placeholders). ### Importing Secrets When you export your project, you will receive a zip file with the contents, with a structure like the example below: The connectors directory includes the configuration of each connector associated your Project. ```json { "configuration": { "authentication": { "apiKey": {}, "basic": {}, "method": "bearerToken" }, "baseUrl": "https://api.sendgrid.com/v3/mail", "headers": [ { "key": "Content-Type", "value": "application/json" } ], "insecure": false }, "contentVersion": "", "description": "Sendgrid Generic HTTP", "id": "xxxx", "name": "Sendgrid Generic", "status": "enabled", "templateId": "http" } ``` ```json { "authMethod": "", "common": { "description": "MS SMTP", "displayName": "MS SMTP", "id": "xxxx" }, "fromEmail": "user@email.com", "fromName": "User", "host": "smtp.office365.com", "password": "", "port": 587, "username": "user@email.com" } ``` ```json { "accountSid": "xxxxx", "apiKey": "", "apiSecret": "", "authToken": "", "common": { "description": "", "displayName": "My Twillio", "id": "xxxxx" }, "fromPhone": "xxxxx", "messagingServiceSid": "xxxx", "selectedAuthProp": "methodAuthToken", "selectedProp": "messagingServiceSid" } ``` To import the secrets within a new project, add them to the downloaded files within the zip, zip the entire directory back up, and then import them to your new project. This will then import the secrets into your project and allow the connectors to be immediately used without further configuration of the connectors after the import. ### Secret Placeholders When you export a project with connectors or OAuth providers configured with secrets, you will see that the secrets will have a placeholder of `PLACEHOLDER_VALUE`. There are two options when handling placeholders within the secrets of the configuration files. - If you leave `PLACEHOLDER_VALUE` in the exported configuration file, the secrets in the project you are importing will be retained. - If you would like to override the configured secrets within the project, you would change the `PLACEHOLDER_VALUE` to be the correct secrets for the project. - Removing the line containing the placeholder or replacing `"PLACEHOLDER_VALUE"` with `null` or `""` will clear the configured value from the secret on import. Below are examples of connector and OAuth provider configurations with `PLACEHOLDER_VALUE`. ```json { "configuration": { "authentication": { "apiKey": {}, "basic": {}, "bearerToken": "PLACEHOLDER_VALUE", "method": "bearerToken" }, "baseUrl": "https://example.com", "hmacSecret": "PLACEHOLDER_VALUE", "hmacSignEntireRequest": true, "includeHeadersInContext": true, "insecure": false }, "contentVersion": "", "description": "", "id": "xxxxx", "name": "Example Connector", "status": "enabled", "templateId": "http" } ``` ```json OAuth Provider Configuration { "Spotify": { "authUrl": "https://accounts.spotify.com/authorize", "callbackDomain": "", "clientId": "xxxxx", "clientSecret": "PLACEHOLDER_VALUE", "custom": true, "defaultScopes": [], "description": "", "enabled": true, "grantType": "authorization_code", "issuer": "", "jwksUrl": "", "logo": "asset:img-xxxxx.png", "manageProviderTokens": true, "name": "Spotify", "prompts": [], "redirectUrl": "", "responseMode": "", "revokeUrl": "", "scopeDelimiter": "%20", "scopes": [ "user-read-email" ], "signAlgorithm": "RS256", "tokenUrl": "https://accounts.spotify.com/api/token", "trustProvidedEmails": true, "useNonce": false, "useSelfAccount": true, "userDataAuthHeaderName": "Authorization", "userDataAuthHeaderType": "Bearer", "userDataAuthQueryParamName": "", "userDataClaimsMapping": { "email": "email", "familyName": "", "givenName": "", "loginId": "email", "middleName": "", "name": "display_name", "phoneNumber": "", "picture": "", "username": "", "verifiedEmail": "", "verifiedPhone": "" }, "userDataHeaders": {}, "userDataQueryParams": {}, "userDataUrl": "https://api.spotify.com/v1/me" } } ``` ## Cloning Projects From the [Project Settings](https://app.descope.com/settings/project) page, Descope allows you to clone your projects, creating an exact duplicate of your current project. This is useful, for example, when cloning a development project to set up staging and production environments. The cloning process copies your flows, styles, project settings, authentication configurations, email templates, and more. ![Cloning a project within Descope](/assets/clone-project.webp) ## Exporting Projects Within Descope, you can export a project from the [Project Settings](https://app.descope.com/settings/project) page. This exports your flows, styles, project settings, authentication method configurations, email templates, etc. You can then import the downloaded zip file into another project. **Exporting a project from the console is the same as exporting a snapshot with the CLI.** The only difference is packaging: the console downloads a zip file, while the [CLI](/cli/descope#export-snapshot) writes the same contents to a folder on disk. See the [Project Snapshot](/how-to-deploy-to-production/project-snapshot) guide for the full directory structure and contents of a project export or snapshot. ![Cloning a project within Descope](/assets/export-project.webp) ## Importing Projects Descope allows you to import an [exported project](/how-to-deploy-to-production/managing-environments#exporting-projects) to another existing project. This is useful when you are working between development, staging, and production projects. Once you have tested your configurations within your environments, you can export and import the project into the next level in your deployments. ![Cloning a project within Descope](/assets/import-project.webp) ## Tracking Changes If you would like to track changes between your project configurations, you can export your project and store within your code repository of choice. This allows you to track the configuration changes you have made within your projects over time. Descope also offers a [GitHub template](/managing-environments/manage-envs-in-github) with built in GitHub actions which enable you to quickly configure your CI/CD pipeline within GitHub and a [Gitlab CI/CD template](/managing-environments/manage-envs-in-gitlab) to facilitate a smoother transition of resources from development projects to production environments. We also offer a [Terraform provider](/managing-environments/terraform) and a [Pulumi provider](/managing-environments/pulumi) to help you manage Descope projects and configurations using infrastructure-as-code. # Multi-Region Architecture (/how-to-deploy-to-production/multi-region-architecture) Learn how to handle data residency requirements when deploying applications across multiple regions with Descope # Multi-Region Architecture and Data Residency When deploying applications that serve users across multiple geographic regions, ensuring data residency compliance is critical. This guide explains how to architect your Descope authentication setup to meet regional data residency requirements while maintaining a seamless user experience. ## Understanding Descope's Regional Architecture Descope supports multi-region deployments with data residency in specific geographic locations. However, **each Descope project can only be hosted in one region**. When you create a Descope project, you select its region (US, EU, AP, CA, UK, SG, or SA), and all user data and project configurations are stored and maintained exclusively within that region. Once a region has been selected during project creation, user and tenant data cannot be moved between regions. This is a permanent configuration that ensures data residency compliance. For more details on regional support, see our [Multi-Region Support](/management/project-settings/multi-regional) documentation. ## Recommended Architecture: Separate Auth Domains For applications running in multiple regions (e.g., both US and EU), Descope's **strong recommendation is to use different authentication domains for different regional applications**. This approach ensures: - **Data Residency Compliance**: User data remains in the appropriate region per regulatory requirements (e.g., GDPR for EU users) - **Performance Optimization**: Authentication requests are routed to the nearest regional endpoint - **Clear Separation**: Each region operates independently with its own project and configuration ## Implementation Strategy ### 1. Create Separate Descope Projects Create a separate Descope project for each region where your application operates. Each project will have its own: - Project ID - Regional base URL - User data store - Configuration settings ### 2. Configure Regional Auth Domains Set up separate authentication domains for each region using [Custom Domains](/how-to-deploy-to-production/custom-domain): | Region | Application Domain | Auth Domain | Descope Project | | :----- | :----- | :----- | :----- | | US | `app-us.example.com` | `auth-us.example.com` | US Project | | EU | `app-eu.example.com` | `auth-eu.example.com` | EU Project | | AP | `app-ap.example.com` | `auth-ap.example.com` | AP Project | | CA | `app-ca.example.com` | `auth-ca.example.com` | CA Project | | UK | `app-uk.example.com` | `auth-uk.example.com` | UK Project | | SG | `app-sg.example.com` | `auth-sg.example.com` | SG Project | | SA | `app-sa.example.com` | `auth-sa.example.com` | SA Project | For additional custom deployments in other regions, contact [Descope Support](/support) for guidance and assistance. #### DNS Configuration For each regional auth domain, configure a CNAME record pointing to the appropriate regional Descope endpoint: - **US**: `auth-us.example.com` → `cname.descope.com` - **EU**: `auth-eu.example.com` → `cname.euc1.descope.com` - **AP**: `auth-ap.example.com` → `cname.aps2.descope.com` - **CA**: `auth-ca.example.com` → `cname.cac1.descope.com` - **UK**: `auth-uk.example.com` → `cname.euw2.descope.com` - **SG**: `auth-sg.example.com` → `cname.aps1.descope.com` - **SA**: `auth-sa.example.com` → `cname.sae1.descope.com` ### 3. Implement Regional Routing Logic Your application needs to route users to the correct regional Descope project based on their location or preference. Implement routing logic that: 1. **Determines User Region**: Use geolocation, user preference, or business logic to determine which region a user should use 2. **Selects Project Configuration**: Choose the appropriate Project ID and base URL for that region 3. **Initializes Descope SDK**: Initialize the Descope SDK with the regional project configuration # Private Cloud Deployments (/how-to-deploy-to-production/private-cloud) Configure apps on a dedicated Descope private cloud environment, including base URLs, networking, and feature availability. # Private Cloud Deployments Your Descope CS or SE contact will typically provision these environments for your company. They will then share the hostnames, IPs, and onboarding steps that apply to your account, so you can complete the onboarding. A **private cloud** deployment is a separate Descope environment provisioned for your company. It is not the shared public SaaS hosts (`api.descope.com`, `api.euc1.descope.com`, and so on). Private cloud is commonly used when you need an isolated environment, stricter network controls, or a compliance program such as [FedRAMP](/fedramp). ## How Private Cloud Differs from Public SaaS | | Public regional SaaS | Private cloud | | --- | --- | --- | | **Provisioning** | Create projects on [descope.com](https://app.descope.com) | Provisioned with Descope CS / CSE | | **API host** | Fixed per region (see [Multi-Region Support](/management/project-settings/multi-regional#default-regional-api-hosts)) | Environment-specific (for example `https://api..descope.app`) | | **SDK `baseUrl`** | Usually inferred from [Project ID](/management/project-settings#general) | Must be set explicitly | | **Static IPs** | Published per region | Provided by your CSE | | **Custom domain CNAME** | Regional targets in [Custom Domain](/how-to-deploy-to-production/custom-domain#create-a-dns-record) | Provided by your CSE | | **Some features** | Available on standard plans | May require Support to enable (see below) | ## Base URLs and SDK Configuration Private cloud environments use **environment-specific hostnames**. Dedicated environments are often named `star1`, `star4`, and similar. The SDK **cannot** infer the correct API host from the Project ID alone. Set **`baseUrl`** (and **`baseStaticUrl`** where your stack supports it) to the hostname Customer Success provides, or derive the scheme and host from the OpenID Connect **discovery URL** on the default [Federated application](https://app.descope.com/applications) in the Console. Example for an environment named `star4`: ```javascript ``` The same host applies to Management API calls, OAuth discovery, Flow traffic, and issuer URLs on [inbound apps](/identity-federation/inbound-apps). For background on when `baseUrl` is required, see [Multi-Region Support → Descope Base URLs](/management/project-settings/multi-regional#descope-base-urls) and [Base URL Configuration](/client-sdk/descope-components#base-url-configuration). ## Networking ### Custom domains You can still use a [custom domain](/how-to-deploy-to-production/custom-domain) on private cloud. The **CNAME target** is not the public regional value — your CSE provides the correct target for your environment. ### Static IPs and Firewalls Public regional [static IP lists](/how-to-deploy-to-production/public-static-ips) do not apply to private cloud. Request your environment's **project** and **connector** static IPs from your assigned CSE when configuring firewalls, IdP allowlists, or Zscaler rules. For connector actions against resources inside your network, you may also deploy the [Descope Engine](/connectors/descope-engine) (outbound-only gRPC). That pattern is common in locked-down and FedRAMP deployments. ## Feature Availability Some capabilities are tied to a specific public endpoint or require explicit enablement on dedicated environments. When a feature doc mentions private cloud, it usually means **contact Support or your CSE** before you rely on it in production. | Feature | Private cloud notes | | --- | --- | | **[Descope MCP Server](/mcp/mcp-server)** | Not on public `mcp.descope.com` / `mcp.euc1.descope.com` URLs. Contact [Support](/support) to enable and receive your MCP endpoint. | | **Early-access product features** | Often enabled per environment on request. | | **[Connectors](/connectors)** | Available; static connector IPs and [Engine setup](/connectors/descope-engine) are environment-specific. | Check the feature's own doc for a private-cloud callout, and use this page as the default when you need environment-specific hostnames or enablement. ## FedRAMP and Other Compliance Programs [FedRAMP High](/fedramp) is a dedicated deployment model with its own onboarding, security guide, and connector patterns (including Descope Engine for private-network integrations). Most FedRAMP customers work through Customer Success rather than the standard SaaS checklist. If you are evaluating FedRAMP specifically, start with [Working with Descope on FedRAMP](/fedramp#working-with-descope-on-fedramp). For general dedicated environments that are not FedRAMP, the sections above still apply. # Project Snapshot (/how-to-deploy-to-production/project-snapshot) Guide describing the content and structure of a Descope project snapshot. # Project Snapshot A project snapshot is a portable export of your Descope project configuration. Use snapshots to back up settings, promote changes across environments (for example, development to staging to production), or track configuration in source control. The layout below matches a typical CLI or Console export. Exact files vary by what you have configured — for example, `fgaschema.txt` appears when Fine-Grained Authorization is set up, and `auth/` only lists methods that exist in the project. ## Snapshot Structure When you export a project, Descope writes the configuration to a folder of JSON files (and related text) plus subdirectories. Each top-level path maps to a configuration area in your project. ## Quick Reference | Path | Description | | --- | --- | | `adminportal.json` | Hosted [Admin Portal](/widgets/admin-portal) settings (enabled state, login flow, widgets shown). [Learn More](#admin-portal). | | `applications.json` | [Federated Apps](/identity-federation/applications) (OIDC, SAML, WS-Federation, Custom). [Learn More](#applications). | | `assets/` | Images, HTML/email bodies, and other media referenced elsewhere. `assets.json` is the manifest. [Learn More](#assets). | | `attributes.json` | Custom attribute **schemas** (for example user, tenant, or inbound-app attributes). [Learn More](#attributes). | | `auth/` | One JSON file per authentication method (`magiclink.json`, `oauth.json`, `otp.json`, `sso.json`, and so on). [Learn More](#authentication-methods). | | `connectors/` | Connector index plus one JSON file per connector. Credentials use placeholders. [Learn More](#connectors). | | `fgaschema.txt` | [Fine-Grained Authorization](/authorization/rebac) schema DSL, when FGA is configured. [Learn More](#fine-grained-authorization). | | `flows/` | Flow index and per-flow folders (`metadata.json`, `contents.json`, screen JSON). [Learn More](#flows). | | `outboundapps.json` | [Outbound Apps](/identity-federation/outbound-apps) and optional DCR presets. [Learn More](#outbound-apps). | | `project.json` | Project-level settings (sessions, invites, trusted domains, CIBA email templates, and related options). [Learn More](#project-metadata). | | `resources.json` | [Resources](/resources) (`api` / `mcp`) and `dynamicRegistrationTemplates`. [Learn More](#resources). | | `roles.json` | [Roles and permissions](/authorization/role-based-access-control) (RBAC). [Learn More](#roles). | | `snapshot.json` | Snapshot format version (for example `{ "version": 5 }`). [Learn More](#project-metadata). | | `styles/` | Style index and theme override files for flows and widgets. [Learn More](#styles). | | `widgets/` | Widget definitions, screens, and widget flows. [Learn More](#widgets). | Snapshots capture **configuration**, not live data. Users, tenants, consents, and audit logs are not part of the export. Connector API keys, OAuth provider secrets, and outbound app client secrets are stripped or set to placeholders such as `PLACEHOLDER_VALUE` so they do not land in source control — see [Managing Secrets in Environments](/how-to-deploy-to-production/managing-environments#managing-secrets-in-environments). On CLI import or validate, supply missing secrets with `--secrets-input`. ## Admin Portal `adminportal.json` stores the hosted [Admin Portal](/widgets/admin-portal) configuration: whether the portal is enabled, which login flow and style to use, the portal title, and which [widgets](/widgets) appear (and in what order). Promoting this file keeps the same portal layout across environments. It only selects which widgets the portal surfaces, while the widget implementations themselves live under [`widgets/`](#widgets). ## Applications `applications.json` holds an `applications` array of [Federated Apps](/identity-federation/applications): apps where Descope is the IdP, or hosts login for a Custom app. Each entry has an id, name, enabled state, optional template id, and a protocol block (`oidc`, `saml`, or WS-Fed) with settings like login page URL and claims. The default OIDC app (`descope-default-oidc`) is included when present. Client secrets follow the usual [secrets](/how-to-deploy-to-production/managing-environments#managing-secrets-in-environments) rules, so they are not checked in as plaintext. This file is only for Federated Apps. [Inbound Apps](/identity-federation/inbound-apps) show up elsewhere in the snapshot: consent and CIBA [flows](#flows), [Resources](#resources) and dynamic registration templates, and inbound-app fields in [`attributes.json`](#attributes). ## Attributes `attributes.json` exports custom attribute schemas (the definitions you create in the Console), not the values stored on individual users or tenants. Attributes are grouped by entity. For example, `inboundApp` holds [Inbound App](/identity-federation/inbound-apps) custom attributes, and the same pattern applies for user or tenant attributes when those exist in the project. Each definition includes name, type, display name, options for select types, and edit or view permissions. The actual values on users, tenants, or apps stay out of the snapshot because they are runtime data. ## Authentication Methods The `auth/` directory has one JSON file per authentication method configured in the project. Common files include: | File | Method | | --- | --- | | `magiclink.json` | Magic Link | | `enchantedlink.json` | Enchanted Link | | `embeddedlink.json` | Embedded Link | | `otp.json` | OTP | | `password.json` | Passwords | | `oauth.json` | Social / enterprise OAuth providers | | `sso.json` | Project-level SSO settings | | `totp.json` / `webauthn.json` / `notp.json` | TOTP, passkeys / WebAuthn, NOTP | | `devicecodes.json` | Device authorization | | `recoverycodes.json` / `securityquestions.json` | Recovery codes, security questions | Each file stores that method's project-level settings — templates, selected connectors, redirects, policy toggles, and related Console options. Messaging templates often point at [`assets/`](#assets) with `asset:` references instead of inlining HTML. OAuth provider client secrets use placeholders when exported. ## Connectors `connectors/` includes `connectors.json` (an index of connector IDs) plus one JSON file per connector (`id`, `name`, `type`, `description`, and `configuration`). Sensitive fields in `configuration` (for example SendGrid `apiKey`) are exported as `PLACEHOLDER_VALUE`. On import, the destination keeps existing secrets unless you override them — see [Managing Environments](/how-to-deploy-to-production/managing-environments#managing-secrets-in-environments). ## Fine-Grained Authorization When your project has an FGA schema, the snapshot includes `fgaschema.txt`: the same AuthZ DSL you edit in the Console **Code** view (types, relations, and permissions). Promote this file alongside [`roles.json`](#roles) when environments should share the same [ReBAC](/authorization/rebac) model. If FGA is unused, this file may be absent from the export. ## Outbound Apps `outboundapps.json` has two top-level arrays: | Key | Contents | | --- | --- | | `outboundApps` | [Outbound App](/identity-federation/outbound-apps) configs (OAuth URLs, scopes, client id, DCR settings, logos, and related fields). | | `outboundAppDcrPresets` | Optional dynamic client registration presets shared by outbound apps. | `clientSecret` values are empty or placeholder on export; inject secrets on import when the target project needs them. ## Roles `roles.json` contains a `roles` array. Each role has an id, name, description, and a list of permission names. Importing applies those [RBAC](/authorization/role-based-access-control) definitions so flows, apps, and APIs that reference role names stay consistent across environments. ## Resources `resources.json` captures your project's [Resources](/resources) for environment promotion as infrastructure-as-code. Typical top-level keys: | Key | Contents | | --- | --- | | `resources` | Resource servers. Each has `id`, `name`, `uri`, `type` (`api` or `mcp`), access settings, and `scopes` (permissions, attributes, connections). | | `dynamicRegistrationTemplates` | Templates referenced by MCP resources for dynamic client registration (login page / consent flow, session settings, tags). | MCP server configuration moves between environments this way: each MCP server is a Resource with `"type": "mcp"`. If that resource uses a dynamic registration template (`useTemplate` or `dynamicRegistrationTemplateId`), the template is exported under `dynamicRegistrationTemplates`. API Resources skip that concept entirely. Everything in `resources.json` is applied with the rest of the project when you run [`descope project snapshot import`](/cli/descope#import-snapshot). ## Assets The `assets/` directory holds binary and text files referenced elsewhere in the snapshot — flow or widget images, OAuth provider logos, email HTML, and plain-text bodies. `assets.json` is the manifest: its `mapping` object ties each snapshot path and field to asset filenames. Asset files use content-based names with a type prefix. The hash in each filename is derived from the file contents, so identical assets share the same name across exports. | Prefix | Description | | --- | --- | | `img-` | Images used in flows, widgets, styles, or OAuth provider configuration (for example `.svg` or `.png`). | | `body-` | HTML content, such as email or messaging templates for auth methods like magic link or OTP. | | `emailbody-` | Plain-text email body content (also referenced from `project.json` templates such as CIBA). | Other snapshot files reference assets with an `asset:` prefix followed by the filename — for example `"emailBody": "asset:emailbody-xxxxx.txt"` in `project.json`, or `"logo": "asset:img-xxxxx.svg"` on an OAuth provider. When exporting with the [Descope CLI](/cli/descope#export-snapshot), pass `--no-assets` to skip extracting asset files into the snapshot directory. ## Flows The `flows/` directory holds every flow in the project. `flows.json` is the index: it lists each flow by flow ID, and each ID has a matching subdirectory under `flows/`. ```json title="flows/flows.json" { "flows": [ "sign-in", "sign-up-or-in", "inbound-apps-user-consent", "ciba-authentication" ] } ``` Each flow folder contains the flow logic, metadata, and one JSON file per screen. | File | Description | | --- | --- | | `metadata.json` | Flow display name, description, enabled state, and the list of screen IDs in the flow. | | `contents.json` | Flow logic — the task graph, conditions, actions, and routing between steps. | | `screen-.json` | UI definition for a single screen, including components, layout, and interactions. One file per screen listed in `metadata.json`. | Consent and CIBA flows used by Inbound Apps or MCP (for example `inbound-apps-user-consent`) are ordinary flow folders here. The Resource and template settings that reference those flows live in [`resources.json`](#resources). To work with a single flow outside a full project snapshot, use [`descope flow export`](/cli/descope#flows) or [`descope flow convert`](/cli/descope#flows) to move between snapshot format (a flow folder) and a single JSON file. ## Styles The `styles/` directory holds every style in the project, including custom styles. `styles.json` is the index: it lists each style by style ID (and a `componentsVersion`), and each ID has a matching JSON file under `styles/`. ```json title="styles/styles.json" { "componentsVersion": "3.14.10", "styles": [ "custom-style-1-dark", "custom-style-1-light", "dark", "light", "sso-suite-default-dark", "sso-suite-default-light" ] } ``` Each style JSON file defines only the changes from Descope’s default light or dark styles — overrides rather than full copies — so customizations stay manageable on top of the built-in appearance. ```json title="styles/custom-style-1-dark.json" { "components": { "container": { "id": { "ROOT": { "--descope-container-background-color": "var(--descope-colors-primary-main)" } } } }, "globals": { "colors": { "secondary": { "contrast": "#FFFFFF", "dark": "#355313", "highlight": "#9DDA58", "light": "#7BC32B", "main": "#588B1FFF" } } }, "name": "Custom-Style-1", "type": "flows" } ``` ## Widgets The `widgets/` directory holds every widget in the project. `widgets.json` is the index: it lists each widget by widget ID, and each ID has a matching subdirectory under `widgets/`. ```json title="widgets/widgets.json" { "widgets": [ "access-key-management", "applications-portal", "audit-management", "role-management", "user-access-key-management", "user-management", "user-profile" ] } ``` Each widget folder contains metadata and one JSON file per screen (similar to flows). The `widgets/` directory also contains a `flows/` subdirectory with [widget flows](/widgets/flows). There is a `flows.json` index; each flow ID has a folder under `widgets/flows/` with `contents.json`, `metadata.json`, and per-screen JSON files. The hosted Admin Portal decides which of these widgets to show through [`adminportal.json`](#admin-portal). `widgets.json` lists every widget in the project, whether or not the portal uses it. ## Project Metadata | File | Description | | --- | --- | | `project.json` | [Project-level settings](/management/project-settings): session and refresh token lifetimes, cookie policy, trusted domains, user invite templates and URLs, test-user options, Auth Hosting iframe embedding, CIBA email templates (often via `asset:` pointers), and related Console options. | | `snapshot.json` | Snapshot format version only — for example `{ "version": 5 }`. Used by import/validate to interpret the export. | # Public Static IPs (/how-to-deploy-to-production/public-static-ips) Guide describing how to configure firewall and allowlisting rules with Descope's static IPs for both projects and connectors. # Public Static IPs This guide covers how to configure your environment to work with Descope's static IPs. All outbound requests from Descope services will originate from the following IP addresses. This includes both: - **Project static IPs** - used for all Descope core services. - **Connector static IPs** - used for outbound traffic from static connectors you configure. Static IPs for connectors are a **Pro+ tier feature**. Learn more about upgrading your tier in our [pricing overview](https://www.descope.com/pricing). ## Why Static IPs? All outbound requests from Descope services originate from a fixed set of static IP addresses. This ensures predictable network behavior and makes it straightforward to configure firewalls, access controls, and monitoring tools. Using static IPs allows you to control which traffic reaches your servers, APIs, or databases by allowlisting only Descope's addresses. This approach is also commonly required in regulated environments such as SOC 2, HIPAA, or PCI, where IP allowlisting is part of compliance. In addition to securing your own infrastructure, static IPs are often necessary when federating into other customers' identity providers or when accessing applications deployed inside restricted networks that use solutions like Zscaler. Having a defined set of IPs ensures smooth integration in these environments. ## Descope Static IPs If you're using a [private cloud deployment](/how-to-deploy-to-production/private-cloud) of Descope, please reach out to your assigned CSE for your list of static IPs. Below are the static IPs for **projects** and **connectors**, separated by region. [Audit](/connectors/connector-configuration-guides/audit-and-troubleshooting) and [messaging](/connectors/connector-configuration-guides/messaging) connectors will send requests via the **Project Static IPs**, rather than the Connector static IPs. Please whitelist both sets of IPs if using these types of connectors, in addition to other connectors. ### US (United States) - **Project Static IPs** - `35.170.24.133` - `3.212.215.29` - `52.44.167.251` - **Connector Static IPs** - `98.80.81.66` - `35.170.219.147` - `44.205.77.119` - `3.18.58.201` - `18.223.218.77` - `18.219.10.191` ### EU (European Union) - **Project Static IPs** - `3.72.207.40` - `3.74.59.88` - `3.121.31.67` - **Connector Static IPs** - `18.198.74.251` - `18.198.169.96` - `63.181.51.115` - `51.96.199.85` - `51.34.114.201` - `51.34.71.226` ### AP (Asia Pacific) - **Project Static IPs** - `15.134.52.119` - `15.134.62.254` - `15.134.8.66` - **Connector Static IPs** - `3.105.46.7` - `3.26.162.183` - `52.64.22.244` - `16.51.214.222` - `16.26.146.3` ### CA (Canada) - **Project Static IPs** - `15.156.133.83` - `3.98.82.190` - `99.79.81.170` - **Connector Static IPs** - `16.54.77.72` - `3.97.39.23` - `35.182.118.250` - `56.112.103.152` - `56.112.68.253` ### FedRAMP (Federal Risk and Authorization Management Program) - **Project/Connector Static IPs** - `15.200.14.191` - `15.200.226.219` - `52.61.10.232` ## How to Use Static IPs Add the appropriate Descope IPs for your project's region to your firewall or security group: - **Project IPs**: Always required to allow Descope backend services (such as OAuth/SSO login) to communicate with your systems. - **Connector IPs**: Required if you configure connectors that need outbound communication (e.g., custom APIs or SaaS systems). Example (US project): ```bash # Example AWS Security Group inbound rule Type: HTTPS Source: 35.170.24.133/32, 3.212.215.29/32, 52.44.167.251/32 # Github Template (/managing-environments/manage-envs-in-github) Streamline DevOps with Descope's GitHub CI/CD Template for Seamless Transitions # Github Template This guide walks you through deploying Descope’s [GitHub CI/CD template](https://github.com/descope/project-cicd-template) so you can promote project resources from one Descope project to another (such as from development or sandbox to production) using GitHub Actions. ## Prerequisites Before you begin utilizing the Github CI/CD template, you will need to have a few items outlined below. 1. The Descope project IDs for the project you are exporting from and the project you are importing to. These are on the [Project settings page](https://app.descope.com/settings/project). 2. A management key created on [the Company Settings page](https://app.descope.com/settings/company/managementkeys) of the console. When scoping the management key, ensure it has access to both projects you are exporting/importing from/to. For this guide, we'll be using the projects referenced below. ![Projects used for the CI/CD template explanation within Descope docs](/assets/cicd-projects.webp) ## Deploy the CI/CD Template Navigate to Descope's [Github CI/CD Template](https://github.com/descope/project-cicd-template) within GitHub, and then select `Use this template`. ![Utilize the Descope CI/CD template within GitHub](/assets/cicd-template.webp) After selecting `Use this template`, you will be prompted to create a new repository from the template. ![Create a repository for use with the Descope CI/CD template within GitHub](/assets/cicd-template-repo.webp) ### Configure Secrets and Variables Once the repository has been created, you must add the project IDs and management key to the secrets and variables section of the GitHub repository. This can be done by clicking into the repository's settings, then going to `Secrets and Variables`, then clicking `Actions`. ![Configure Secrets for use with the Descope CI/CD template within GitHub](/assets/cicd-template-secrets.webp) The management key should be stored as a secret by clicking the `New Repository Secret` button. Then creating the secret stored as `MANAGEMENT_KEY`. ![Configure management key secret for use with the Descope CI/CD template within GitHub](/assets/cicd-template-secrets-management-key.webp) Next, switch to the variables tab and add two new variables for `PRODUCTION_PROJECT_ID` and `STAGING_PROJECT_ID`. - `PRODUCTION_PROJECT_ID` - the project ID of the project you are importing to - `STAGING_PROJECT_ID` - the project ID of the project you are exporting from ![Configure project ID variables for use with the Descope CI/CD template within GitHub](/assets/cicd-template-variables.webp) ![Configure project ID variables for use with the Descope CI/CD template within GitHub - 1](/assets/cicd-template-variables-1.webp) ![Configure project ID variables for use with the Descope CI/CD template within GitHub - 2](/assets/cicd-template-variables-2.webp) ### Configure Workflow Permissions The workflows must be allowed to create pull requests and update the repository. - In your repo's `Settings` page, go to `Actions` and select `General` on the side panel. - Scroll to the `Workflow permissions` section. - Make sure the `Read and Write permissions` option is selected. - Ensure the `Allow GitHub Actions to create and approve pull requests` option is checked. ![Configure workflow permissions for use with the Descope CI/CD template within GitHub](/assets/cicd-template-workflow-permissions.webp) ## Usage Upon configuring the repository from the template, there will be no project data stored. The following sections will outline how to create a pull request from staging and progress with publishing to production. ### Create Pull Request from Staging Project To create a pull request from the staging environment, follow these steps: - Go to the Actions page in your repo. - Select the Create Pull Request from Staging Project workflow. - Press the Run workflow button and confirm. ![Create Pull Request from Staging Project with the Descope CI/CD template within GitHub](/assets/cicd-template-pull-request.webp) - After a short while, a new Pull Request will be created. This can be seen under the `Pull Requests` tab in the repository. - You can confirm that the added files contain your staging project's settings and configurations. - Approve and merge the Pull Request to trigger an automatic deployment into your production project. ![Previous of the created Pull Request from Staging Project with the Descope CI/CD template within GitHub](/assets/cicd-template-pull-request-1.webp) You will now see a `ProjectSnapshot` folder in the repo with files representing all the settings and configurations of your project. Note that the path where the files are stored can be customized in the workflow files. ![File structure after committing the pull request Staging Project with the Descope CI/CD template within GitHub](/assets/cicd-template-pull-request-2.webp) ### Deploy to Production Project Upon successfully approving the pull request, the `Deploy to Production Project` will automatically run. You can check the status of this run by navigating to the `Actions` tab within GitHub and then reviewing `All workflows` or selecting the `Deploy to Production Project` action from the left menu. Once this workflow has successfully completed, your Descope flows, styling configurations, etc from your sandbox project will be successfully deployed to your production project. #### Handling Secrets During Production Deployments When running the `Deploy to Production Project` action within the GitHub repo, you may hit a scenario where you've added a new connector (or OAuth provider secrets) within your development environment that has not been created within your production environment. When this occurs, you'll have a failed deployment for the GitHub action. The Descope repository template makes resolving these issues a breeze. The failed deployment will give you exact details about how to fix the problem in the format exampled below. If the connector is already created and configured within the production environment, the current configuration settings and secrets will be retained unless overridden via the connector or OAuth provider configuration file or with the secrets injection outlined below. Please also see the documentation around [secret placeholders](/how-to-deploy-to-production/managing-environments#secret-placeholders) within the connector and OAuth provider configuration files. ![Failed deployment to Production Project action with the Descope CI/CD template within GitHub when the credential details are necessary](/assets/cicd-template-deploy-failed.webp) From the message displayed, we see a connector named `Test`, which does not have a configuration within the production environment. To resolve this, you need to rerun the workflow manually supplying the JSON displayed within the field for `An optional JSON object with additional secrets to inject into the snapshot data`. The JSON can be copied from the message within the failed production deployment. An example can be seen below. ```json { "connector-abc": { "name": "Test", "secrets": { "accessKeyId": "xxxxxxxxxxxxxxx", "secretAccessKey": "yyyyyyyyyyyyyy" } } } ``` Once you have copied the JSON and updated the credentials, you can redeploy by: - Going to `Actions` - Click the `Deploy to Production Project` action - Click `Run workflow` - providing the completed JSON within the `An optional JSON object with additional secrets to inject into the snapshot data` field - Click `Run workflow` per the below example. ![Rectifying a failed production deployment with the Descope CI/CD template within GitHub by supplying connector or OAuth provider credentials](/assets/cicd-template-deploy-failed-fix.webp) After completing these steps to resolve the credential error, your connector credentials and other data will be migrated to your production project. # GitLab Template (/managing-environments/manage-envs-in-gitlab) Streamline DevOps with Descope's GitLab CI/CD Template for Seamless Transitions # GitLab Template This guide shows how to utilize Descope's [GitLab CI/CD template](https://github.com/descope/project-gitlab-cicd-pipeline) to facilitate a smoother transition of resources from development projects to production environments. ## Prerequisites Before you begin utilizing the GitLab CI/CD template, you will need to have a few items outlined below. 1. The Descope project IDs for the project you are exporting from and the project you are importing to. These can be found on the [Project settings page](https://app.descope.com/settings/project) of the console. 2. A management key created on [the Company Settings page](https://app.descope.com/settings/company/managementkeys) of the console. When scoping the management key, ensure it has access to both projects you are exporting/importing from/to. For this guide, we'll be using the projects referenced below. ![Projects used for the CI/CD template explanation within Descope docs](/assets/cicd-projects-list.webp) ## Using the CI/CD Template 1. Create a new repository in GitLab. 2. Navigate to Descope's [GitLab CI/CD Template](https://github.com/descope/project-gitlab-cicd-pipeline) within GitHub. 3. In order to use the template, all you need to worry about is the `.gitlab-ci.yml` file - copy it over to your new GitLab repository. ### Configure Variables and Secrets 1. Once the repository has been created, you need to create a GitLab Project Access Token, as shown in the [GitLab guide](https://docs.gitlab.com/user/project/settings/project_access_tokens/) 2. After creating the Project Access Token, you must add the project IDs and management key to the [CI/CD variables section](https://docs.gitlab.com/ci/variables/) of the GitLab repository. This can be done by clicking into the repository's settings, then going to **CI/CD** and expanding the **Variables** section: - `GITHUB_PUSH_TOKEN` - the Project Access Token created in the previous step. - `MANAGEMENT_KEY` - the Management Key created in the [Company Settings page](https://app.descope.com/settings/company/managementkeys). - `PRODUCTION_PROJECT_ID` - the Project ID of the Descope project you are importing to. - `STAGING_PROJECT_ID` - the Project ID of the Descope project you are exporting from. ![Configure Variables for use with the Descope CI/CD template within GitLab](/assets/gitlab-cicd-variables.webp) ## Usage 1. Click **Build → Pipeline** editor and select **Configure pipeline**. 2. Copy the content of the `.gitlab-ci.yml` file from the template to the text editor and commit the change. ![Descope CI/CD template within GitLab pipline editor](/assets/gitlab-cicd-pipeline-editor.webp) 3. A `validate_and_deploy` job will automatically run without changes. At first, the repository will be empty (Except the `.gitlab-ci.yml` file). We'll want to get the current state of the Descope project into the repository. 4. Go to **Build → Pipelines** in your GitLab project. 5. Select **New pipeline → Select main branch → New pipeline**. ![GitLab pipelines overview](/assets/gitlab-cicd-pipelines-overview.webp) 6. Click the ▶️ play button next to export_and_create_mr. ![GitLab pipeline run](/assets/gitlab-run-cicd-pipeline.webp) 7. After a short while, a new Merge Request will be created. 8. You can confirm that the added files contain your staging project's settings and configurations. 9. Approve and merge the Merge Request to trigger an automatic deployment into your production project. ![GitLab merge request](/assets/gitlab-cicd-merge-request.webp) # Pulumi (/managing-environments/pulumi) Streamline DevOps with Descope's Pulumi Provider # Pulumi Provider In many software development methodologies, there is a need to manage and automate development, testing, and production environments. This need arises from the understanding that all resources deployed in an environment—whether cloud-based or hosted on local servers—eventually reach a stable configuration, often referred to as the "desired state", which primarily includes the configuration of all deployed services. With tools like Pulumi, the manual effort required to maintain this state decreases. Pulumi, along with other infrastructure-as-code solutions, streamlines the process by enabling a code-first approach to automating environments. To read more about Pulumi and this approach, [click here](https://www.pulumi.com/what-is/what-is-infrastructure-as-code/). Descope provides a [pulumi package](https://www.pulumi.com/registry/packages/descope/) that allows managing Descope projects and configuration. ## Prerequisites The pulumi package works with "Pro" or "Enterprise" type license. If you are having trouble with licensing, please contact the Descope customer success team. * A project already created in Descope. * `Management Key`. Create One on the [Company Settings](https://app.descope.com/settings/company). If you intend to create a new project, make sure the key is scoped for use in _all projects_. ### Installation This package is available for several languages/platforms: #### Node.js (JavaScript/TypeScript) To use from JavaScript or TypeScript in Node.js, install using either npm: ``` npm install @descope/pulumi-descope ``` Or yarn: ``` yarn add @descope/pulumi-descope ``` #### Python To use from Python, install using pip: ``` pip install descope_pulumi ``` #### Go To use from Go, use go get to grab the latest version of the library: ``` go get github.com/descope/pulumi-descope/sdk/go/... ``` #### .NET To use from .NET, install using dotnet add package: ``` dotnet add package Descope.Pulumi.Descope ``` ### Configuration The following configuration points are available for the `descope` provider: * descope:projectId (environment: DESCOPE_PROJECT_ID) - Descope Project ID * descope:managementKey (environment: DESCOPE_MANAGEMENT_KEY) - Descope Management Key * descope:baseUrl (environment: DESCOPE_BASE_URL) - Descope Base URL, for `EU` based projects, use `api.euc1.descope.com` ## Reference For detailed reference documentation, please visit the [Pulumi registry](https://www.pulumi.com/registry/packages/descope/api-docs/). ### Examples | Type | Example Repository | |-----------------------|------------------------------------------------------------------------------| | JavaScript/TypeScript | [Click Here](https://github.com/descope/pulumi-descope/tree/main/examples/ts)| | Python | [Click Here](https://github.com/descope/pulumi-descope/tree/main/examples/py)| | Golang | [Click Here](https://github.com/descope/pulumi-descope/tree/main/examples/go)| # Terraform (/managing-environments/terraform) Streamline DevOps with Descope's Terraform Provider # Terraform Provider Terraform is an infrastructure-as-code tool that lets you define your environment configuration in `.tf` files and apply it consistently across development, staging, and production. Instead of configuring environments by hand, you declare a desired state and let Terraform manage it. Read more [here](https://developer.hashicorp.com/terraform/intro). Descope publishes a [Terraform provider](https://registry.terraform.io/providers/descope/descope/latest) for managing projects and their configuration. Terraform is best suited for managing infrastructure and configuration that should be consistent across environments. **Dynamic elements of a project—such as individual users, tenants, SSO connections, and SCIM configurations—are not typically managed by Terraform.** These are unique to each project or environment and are generally handled through the Descope Console, SDKs, or APIs, and not as infrastructure-as-code. **Project-level** SSO settings, SSO Setup Suite options, and the hosted [Admin Portal](/widgets/admin-portal) **are** managed in Terraform — see [SSO Settings and Admin Portal](#sso-settings-and-admin-portal) below. Only resources explicitly declared in your configuration will be affected when you run `terraform apply`. Omitting a block (e.g. `connectors`) means Terraform will not touch it, while adding a block (even if empty) will cause Terraform to manage it and remove anything not declared. ## Prerequisites The Terraform provider requires a paid Descope license (Pro+). Contact [support@descope.com](mailto:support@descope.com) with any licensing questions. * [Terraform CLI](https://developer.hashicorp.com/terraform/install) 1.0 or later installed. * A **Management Key** from [Company Settings](https://app.descope.com/settings/company). To create or manage projects via Terraform, the key needs a company-level role with the appropriate permissions (see [Management Key Roles](/management#management-key-roles)). ## Using the Terraform Provider ### Provider Configuration Declare the Descope provider in your `.tf` file: ```hcl terraform { required_providers { descope = { source = "descope/descope" version = "~> 0.3" } } } ``` Never hardcode your management key in Terraform configuration files—this risks exposing it in version control. Use environment variables or a secrets manager instead. | Variable | Description | |---|---| | `DESCOPE_MANAGEMENT_KEY` | A valid management key for your Descope company | | `DESCOPE_BASE_URL` | Override the Descope API base URL (optional, for testing) | ```shell export DESCOPE_MANAGEMENT_KEY="K2..." ``` With those set, the provider block needs no additional configuration: ```hcl provider "descope" {} ``` Run `terraform init` to download the provider: ```shell terraform init ``` If you need to configure credentials explicitly (e.g. in a module): ```hcl variable "descope_management_key" { type = string sensitive = true } provider "descope" { management_key = var.descope_management_key } ``` ### Creating a Project Add a project resource to your `.tf` file: ```hcl resource "descope_project" "myproject" { name = "project-name" environment = "production" tags = ["foo", "bar"] } ``` Attributes like `tags` support dynamically computed values: ```hcl variable "additional_project_tags" { type = list(string) nullable = false } resource "descope_project" "myproject" { name = "project-name" tags = [ "foo", ...var.additional_project_tags ] } ``` ### Importing an Existing Project If you already have a Descope project and want to bring it under Terraform management, generate its configuration from the live project instead of writing it by hand. This workflow requires Terraform CLI 1.5 or later—`import` blocks and `-generate-config-out` are not available in earlier versions. #### 1. Add an import block ```hcl import { to = descope_project.myproject id = "P..." # your project ID } ``` #### 2. Generate the configuration ```shell terraform plan -generate-config-out="generated.tf" ``` #### 3. Review and clean up the generated file - Replace any secret placeholders (e.g. connector credentials, client secrets) with real values or variable references—secrets are never included in generated configuration. - Remove any blocks you don't want Terraform to manage going forward (e.g. inline `flows`, `styles`, or email templates you'd rather keep editing in the Console). #### 4. Apply to import the project into state Move the contents of `generated.tf` into your main configuration file, keeping the `import` block in place, then run: ```shell terraform plan terraform apply ``` The `import` block tells Terraform to attach the existing project to state rather than create a new one. Once the apply succeeds, delete the `import` block—it has served its purpose, and subsequent `plan`/`apply` runs manage the project as usual. The same `import` + `-generate-config-out` workflow works for the other resources covered in this guide—[Access Keys](#access-keys), [Descope Engine](#descope-engine), [Management Keys](#management-keys), just point the `import` block's `to` and `id` at that resource instead. Access Keys and Engines use `/` as the import `id`. ## Examples Each example below is an attribute inside the `descope_project` resource. ### Project Settings Configure project-level settings: ```hcl project_settings = { refresh_token_expiration = "3 weeks" enable_inactivity = true inactivity_time = "1 hour" } ``` [Full project settings schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--project_settings) ### Invite Settings Configure user invitation behavior: ```hcl invite_settings = { require_invitation = true invite_url = "https://example.com/invite" add_magiclink_token = true expire_invited_users = true invite_expiration = "2 weeks" # Optional: custom connector and templates for invitation emails email_service = { connector = "My Email Connector" templates = [ { name = "Invite Email" subject = "You've been invited" html_body = "

Click here to accept your invitation.

" active = true } ] } } ``` The `expire_invited_users` flag causes invited user accounts to expire if the invitation is not accepted within the `invite_expiration` duration. The `invite_expiration` field accepts human-readable durations such as `"2 weeks"` or `"4 days"`, with a minimum value of `"1 hour"`. Use it alongside `expire_invited_users` and/or `add_magiclink_token`. `email_service` lets you send invitation emails through your own connector and templates instead of Descope's default. `connector` is required and must name an existing connector in your `connectors` block; `templates` is an optional list of email templates, each with a `name`, `subject`, and either `html_body` or `plain_text_body` (with `use_plain_text_body = true`) for the body. [Full invite settings schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--invite_settings) ### Authorization Configure permissions and roles: ```hcl authorization = { permissions = [ { name = "test-permission" description = "this is a test" } ] roles = [ { name = "test-role" description = "this is a test" permissions = ["test-permission"] } ] } ``` [Full authorization schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--authorization) #### FGA Schema Use the `fga` attribute to manage your project's [Fine-Grained Authorization](/authorization/rebac) schema as code. In the Descope console, open the [Authorization > FGA](https://app.descope.com/authorization/fga) page and copy the schema text from the editor. You can paste it inline or load it from a file: ```hcl authorization = { # Inline fga = "model AuthZ\n..." # Or from a file fga = file("${path.module}/fga-schema.txt") } ``` The `fga` value must start with `"model AuthZ"`. Copy the schema text from the Schema tab's editor. [Full FGA schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--authorization--fga) ### Authentication Configure authentication methods: ```hcl authentication = { magic_link = { expiration_time = "1 hour" } password = { lock = true lock_attempts = 3 min_length = 8 disallow_email_match = true disallowed_characters = "'\"" enforce_strength = "very_strong" } sso = { merge_users = true redirect_url = var.descope_redirect_url } } ``` [Full authentication schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--authentication) #### Passkeys Use the `passkeys` block to configure passkey authentication. The `android_fingerprints` field restricts which Android apps may use passkeys by their SHA-256 APK key hash fingerprint (colon-separated hex format). An empty list places no restriction on Android apps. Once you list a fingerprint, only the Android apps you list can use passkeys, so include every signing key your apps use. Web and iOS clients are unaffected. See [Passkeys Settings](/auth-methods/passkeys/settings) for details. ```hcl authentication = { passkeys = { top_level_domain = "example.com" display_name = "Example App" android_fingerprints = [ "AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89" ] } } ``` [Full passkeys schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--authentication--passkeys) ### Attributes Configure custom attributes for users and tenants: ```hcl attributes = { user = [ { name = "test attribute user" type = "string" } ] tenant = [ { name = "test attribute tenant" type = "multiselect" select_options = ["A", "B"] } ] } ``` [Full attributes schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--attributes) ### Connectors Connectors support bearer token auth and role-based auth: ```hcl connectors = { # Bearer Token Authentication Example http = [ { name = "Test HTTP" description = "A Description" base_url = var.http_connector_base_url use_static_ips = false authentication = { bearer_token = var.http_connector_secret } } ] # Role-Based Authentication Example aws_s3 = [ { name = "S3 Audit Connector" description = "A Description" auth_type = "assumeRole" role_arn = "arn:aws:iam::YOUR_ACCOUNT_ID:role/your-connector-role" external_id = "YOUR_EXTERNAL_ID" region = "us-east-1" bucket = "your-audit-logs-bucket" } ] # reCAPTCHA Enterprise Example recaptcha_enterprise = [ { name = "reCAPTCHA Enterprise" description = "Bot protection" project_id = var.recaptcha_project_id site_key = var.recaptcha_site_key api_key = var.recaptcha_api_key action = "login" } ] } ``` For AWS connector role setup requirements, including trust policy configuration, refer to the specific connector documentation ([S3](/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-s3), [SES](/connectors/connector-configuration-guides/messaging/aws-ses), etc.). [Full connectors schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--connectors) ### JWT Templates Use `jwt_templates` to configure custom JWT claim templates. You can include a `description`, control which standard claims are included, and add security features like JTI: ```hcl jwt_templates = { user_templates = [ { name = "app-claims" description = "Adds subscription tier and org context to user JWTs" template = jsonencode({ tier = "@user.customAttributes.subscriptionTier" org_id = "@user.tenants[0].tenantId" }) # Exclude the permissions claim to keep tokens lean exclude_permission_claim = true # Add a unique JWT ID for replay attack prevention add_jti_claim = true # Move the user ID to a new dsub claim, allowing sub to be customized override_subject_claim = true } ] } ``` [Full JWT templates schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--jwt_templates) ### SSO Settings and Admin Portal Use Terraform to manage **project-level** SSO (`authentication.sso`), [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) options (`sso_suite_settings`), invite emails, and the hosted [Admin Portal](/widgets/admin-portal) (`admin_portal`). Provider schema source of truth: [authentication/sso](https://github.com/descope/terraform-provider-descope/blob/main/docs/raw/project/authentication/sso.md) and [adminportal](https://github.com/descope/terraform-provider-descope/blob/main/docs/raw/project/adminportal/adminportal.md) under the [project docs tree](https://github.com/descope/terraform-provider-descope/tree/main/docs/raw/project). | In Terraform (`descope_project`) | Not in Terraform (use Setup Suite / Console / Management API) | | -------------------------------- | ------------------------------------------------------------- | | Project SSO auth method settings (`authentication.sso`) | Per-tenant SAML/OIDC connections (IdP metadata, certs, maps) | | SSO Setup Suite UI options (`sso_suite_settings`) | Per-tenant SCIM tokens and base URLs | | Default post-auth redirect URL | Individual tenants and users | | SSO invite email templates | Day-to-day customer IdP changes | | Admin Portal enablement, style, and widget list | — | Automate per-tenant connections with the [Management SSO SDKs](/management/tenant-management/sso/sdks) or [tenants SSO API](/api/management/tenants/sso), or hand customers the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). #### Project SSO Settings These map to [Project-level SSO settings](/auth-methods/sso/settings) in the Console. ```hcl authentication = { sso = { # Disable direct SSO API/SDK starts (Flows that use SSO still work) disabled = false # Merge SSO logins into existing users with the same email merge_users = true # Default post-auth redirect (tenant / SDK / API can override) redirect_url = "https://app.example.com/auth/callback" # Same SSO domain allowed on more than one tenant allow_duplicate_domains = false # SSO-mapped roles replace the user's existing roles allow_override_roles = true # Prefer group-based role maps when priorities conflict groups_priority = true # Tenant SSO configs must include an SSO domain require_sso_domains = true # Tenant SSO configs must set a groups attribute name require_groups_attribute_name = true # Reject login if assertion email domain ≠ configured SSO domains block_if_email_domain_mismatch = true # Leave email unverified after SSO mark_email_as_unverified = false # Only allow mapping attributes listed below limit_mapping_to_mandatory_attributes = false # Attributes that must be present / mappable from the IdP # (id = Console "Machine Name"; set custom = true for custom attrs) mandatory_user_attributes = [ { id = "email" }, { id = "name" }, { id = "department", custom = true }, ] # SSO Setup Suite — see next subsection sso_suite_settings = { style_id = "customer-facing-style" hide_sso = false hide_scim = false hide_saml = false hide_oidc = false hide_role_mapping = false hide_fga_mapping = false hide_domains = false hide_jit_guide = false force_domain_verification = false show_help_contact = true support_email = "it-support@example.com" } # Optional: custom connector for SSO invite emails email_service = { connector = "My Email Connector" templates = [ { name = "SSO Invite" subject = "You've been invited to sign in" html_body = "

Click here to accept.

" active = true } ] } } } ``` | Attribute | Purpose | | --------- | ------- | | `disabled` | Blocks direct SSO via API/SDK; does **not** disable SSO inside Flows | | `merge_users` | Merge SSO users into existing accounts with the same email ([merging guide](/sso/merging-sso-identities-risk)) | | `redirect_url` | Project default post-auth URL ([settings](/auth-methods/sso/settings#post-authentication-redirect-url)); required for [IdP-initiated](/sso/idp-initiated) unless set per tenant | | `allow_duplicate_domains` | Allow the same SSO domain on multiple tenants | | `allow_override_roles` | Let SSO group → role maps override existing roles ([details](/auth-methods/sso/settings#role-mapping-add-vs-override)) | | `groups_priority` | Enable groups priority when resolving role maps | | `require_sso_domains` | Force an SSO domain on each tenant SSO config ([settings](/auth-methods/sso/settings#requiring-an-sso-domain)) | | `require_groups_attribute_name` | Force a groups attribute name on each tenant SSO config | | `block_if_email_domain_mismatch` | Fail login when email domain ≠ SSO domains | | `mark_email_as_unverified` | Do not mark email verified after SSO | | `mandatory_user_attributes` | Required Descope attributes from SSO (`id`, optional `custom`) | | `limit_mapping_to_mandatory_attributes` | Only allow maps into those mandatory attributes | | `sso_suite_settings` | Hosted Setup Suite UI (below) | | `email_service` | Connector + templates for SSO invite emails | Registry: [authentication.sso nested schema](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--authentication--sso). For `email_service`, the `connector` must be an existing HTTP connector in your `connectors` block. If any template has `active = true`, the connector cannot be `"Descope"`. Each template needs `html_body` or `plain_text_body` with `use_plain_text_body = true`. Template names must be unique and cannot be `"System"`. #### SSO Setup Suite Settings Controls what customer IT admins see in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) (mirrors [SSO Suite settings](/auth-methods/sso/settings#sso-setup-suite-settings) in the Console). | Attribute | Purpose | | --------- | ------- | | `style_id` | Style applied to the suite (must exist in the project) | | `hide_sso` | Hide the whole SSO configuration, leaving a SCIM-only suite (cannot be combined with `hide_scim`) | | `hide_saml` / `hide_oidc` | Hide SAML or OIDC setup (cannot both be `true`) | | `hide_scim` | Hide SCIM in the suite | | `hide_groups_mapping` | Hide both role and FGA mapping UI (cannot be combined with `hide_role_mapping` / `hide_fga_mapping`) | | `hide_role_mapping` | Hide only the role mapping UI | | `hide_fga_mapping` | Hide only the FGA mapping UI | | `hide_domains` | Hide SSO domains UI | | `hide_jit_guide` | Hide the JIT provisioning guide in the hosted UI | | `force_domain_verification` | Require DNS domain verification (incompatible with `hide_domains = true`) | | `show_help_contact` | Show a help/support contact in the suite | | `support_email` | Email shown as that support contact | #### Admin Portal Configures the hosted [Admin Portal](/widgets/admin-portal) — which widgets appear and which style to use. ```hcl admin_portal = { enabled = true style_id = "admin-portal-style" # type + widget_id must match widgets that already exist in the project # (create/export them under Widgets in the Console, or via the widgets block) widgets = [ { type = "users" widget_id = "user-management-widget" }, { type = "roles" widget_id = "role-management-widget" }, ] } ``` | Attribute | Purpose | | --------- | ------- | | `enabled` | Turn the hosted Admin Portal on or off | | `style_id` | Style for the portal | | `widgets` | Ordered list of widgets (`type` + `widget_id`, both required). At least one widget is required when `enabled = true` | `type` is the widget type string from the Console (common examples from the provider: `users`, `roles`). `widget_id` is the ID of the widget instance you want to host. Create those widgets under [Widgets](https://app.descope.com/widgets) / [Admin Widgets](/widgets/admins) (you can also manage widget JSON via the project's `widgets` map — see the [Registry widgets schema](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--widgets)). Registry: [admin_portal nested schema](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--admin_portal). If Descope is the IdP for other apps (outbound SAML/OIDC), configure those under `applications` — see [Applications](#applications) and [Federated Apps](/identity-federation/applications). That is separate from tenant SSO (customers' IdPs signing into your app). ### Flows and Styles If you've designed custom flows in the Descope console, you can export and load them via Terraform: 1. In the Descope console, go to **Authentication Flows** 2. Open the flow you want to manage, click the export button, and save the JSON file (e.g., `flows/sign-up-or-in.json`) 3. Optionally export your flow styles from the same screen and save as `flows/styles.json` 4. Reference the files in your configuration: ```hcl flows = { "sign-up-or-in" = { data = file("${path.module}/flows/sign-up-or-in.json") } } styles = { data = file("${path.module}/flows/styles.json") } ``` [Full flows schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--flows) · [Full styles schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--styles) ### Widgets If you've built a widget (e.g. a user management widget or a role management widget) under [Widgets](https://app.descope.com/widgets) in the Descope console, you can export and manage it as code the same way as flows and styles: 1. In the Descope console, go to **Widgets** and open the widget you want to manage. 2. Export its JSON and save it (e.g., `widgets/user-management.json`). 3. Reference the file in your configuration, keyed by a machine-readable widget ID: ```hcl widgets = { "user-management-widget" = { data = file("${path.module}/widgets/user-management.json") } } ``` [Full widgets schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--widgets) ### Full Terraform Plan Example ```hcl variable "http_connector_base_url" { type = string } variable "http_connector_secret" { type = string sensitive = true } variable "s3_role_arn" { type = string } variable "s3_external_id" { type = string } terraform { required_providers { descope = { source = "descope/descope" version = "~> 0.3" } } } provider "descope" {} resource "descope_project" "my-project" { name = "my-project" environment = "production" tags = ["production", "team-auth"] project_settings = { refresh_token_expiration = "3 weeks" enable_inactivity = true inactivity_time = "2 hours" } invite_settings = { require_invitation = true invite_url = "https://example.com/invite" add_magiclink_token = true expire_invited_users = true invite_expiration = "2 weeks" } authentication = { magic_link = { expiration_time = "1 hour" } password = { lock = true lock_attempts = 3 min_length = 8 disallow_email_match = true disallowed_characters = "'\"" } sso = { merge_users = true redirect_url = "https://example.com/auth/callback" allow_override_roles = true groups_priority = true require_sso_domains = true require_groups_attribute_name = true block_if_email_domain_mismatch = true mark_email_as_unverified = false mandatory_user_attributes = [ { id = "email" }, { id = "name" }, { id = "department", custom = true }, ] sso_suite_settings = { style_id = "my-brand-style" hide_sso = false hide_scim = false hide_saml = false hide_oidc = false hide_role_mapping = false hide_fga_mapping = false hide_domains = false hide_jit_guide = false force_domain_verification = false show_help_contact = true support_email = "it-support@example.com" } } } admin_portal = { enabled = true style_id = "admin-portal-style" widgets = [ { type = "users", widget_id = "user-management-widget" }, { type = "roles", widget_id = "role-management-widget" }, ] } attributes = { user = [ { name = "subscriptionTier" type = "string" } ] tenant = [ { name = "plan" type = "multiselect" select_options = ["free", "pro", "enterprise"] } ] } authorization = { permissions = [ { name = "read:data" description = "Read access to project data" }, { name = "write:data" description = "Write access to project data" } ] roles = [ { name = "viewer" description = "Read-only access" permissions = ["read:data"] }, { name = "editor" description = "Read and write access" permissions = ["read:data", "write:data"] } ] } applications = { oidc_applications = [ { name = "My Web App" description = "Primary OIDC application" force_authentication = false claims = ["sub", "exp", "email"] } ] saml_applications = [ { name = "My SAML App" description = "Enterprise SAML integration" force_authentication = false default_signature_algorithm = "sha256" manual_configuration = { acs_url = "https://example.com/saml/acs" entity_id = "https://example.com" } } ] } jwt_templates = { user_templates = [ { name = "app-claims" description = "Adds subscription tier and org context to user JWTs" template = jsonencode({ tier = "@user.customAttributes.subscriptionTier" org_id = "@user.tenants[0].tenantId" }) exclude_permission_claim = true add_jti_claim = true override_subject_claim = true } ] } connectors = { http = [ { name = "Internal API" description = "Backend service connector" base_url = var.http_connector_base_url use_static_ips = false authentication = { bearer_token = var.http_connector_secret } } ] aws_s3 = [ { name = "S3 Audit Logs" description = "Audit log storage" auth_type = "assumeRole" role_arn = var.s3_role_arn external_id = var.s3_external_id region = "us-east-1" bucket = "my-audit-logs-bucket" } ] } flows = { "sign-up-or-in" = { data = file("${path.module}/flows/sign-up-or-in.json") } } styles = { data = file("${path.module}/flows/styles.json") } } ``` ## Additional Resources Users and tenants are generally not managed via Terraform, but some dynamic resources have dedicated resource types. Defining them as code keeps access control auditable and consistent across environments. ### Management Keys Use `descope_management_key` to manage Descope [Management Keys](/management#management-keys) as code, alongside the rest of your project configuration. The raw key value (`cleartext`) is only available immediately after creation and **cannot be retrieved later**. Store it in a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) right after `terraform apply`. Keys can be scoped to restrict access at the company level, per project, or by project tag: ```hcl # Company-level key (access to all projects) resource "descope_management_key" "pipeline_key" { name = "CI/CD Pipeline Key" description = "Used by the deployment pipeline to manage users" rebac = { company_roles = [""] } } output "pipeline_key_value" { value = descope_management_key.pipeline_key.cleartext sensitive = true } ``` Role names must use the **machine name** format (kebab-case), not the human-readable format from the Descope Console. For example, use `company-asset-mgmt-read-write` instead of "Asset Management - Read & Write". See the [Management Key Roles](/management#management-key-roles) documentation for the complete list of machine names. ```hcl # Project-scoped key resource "descope_management_key" "staging_key" { name = "Staging Key" rebac = { project_roles = [ { project_ids = ["__ProjectID__"] roles = [""] } ] } } ``` [Full management key schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/management_key) ### Access Keys Use `descope_access_key` to manage project-level [Access Keys](/management/m2m-access-keys) as code, for machine-to-machine authentication. The `cleartext` value (the plaintext access key) is only available immediately after creation and **cannot be retrieved later**. Store it in a secrets manager (e.g., AWS Secrets Manager, HashiCorp Vault) right after `terraform apply`. ```hcl resource "descope_access_key" "ci_pipeline" { project_id = descope_project.myproject.id name = "CI/CD Pipeline Key" description = "Used by the deployment pipeline" roles = ["Viewer"] permitted_ips = ["10.0.0.0/8"] expire_time = 1924991999 } output "ci_pipeline_key" { value = descope_access_key.ci_pipeline.cleartext sensitive = true } ``` [Full access key schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/access_key) ### Descopers (Console Users) Use `descope_descoper` to manage [Descopers](/management/company-settings#descopers) as code. Roles can be scoped to the entire company, to specific projects, or to all projects with a given tag. Available roles: `admin`, `developer`, `support`, `auditor`. ```hcl # Company admin resource "descope_descoper" "admin" { email = "admin@example.com" name = "Alice Admin" rbac = { is_company_admin = true } } # Developer scoped to specific projects resource "descope_descoper" "developer" { email = "dev@example.com" name = "Bob Dev" rbac = { project_roles = [ { role = "developer" project_ids = ["P123abc", "P456def"] } ] } } # Support access for all production-tagged projects resource "descope_descoper" "support" { email = "support@example.com" rbac = { tag_roles = [ { role = "support" tags = ["production"] } ] } } ``` [Full descoper schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/descoper) ### Applications Like users and tenants, applications are dynamic resources that vary per environment. The Terraform provider supports two types: **Federated Apps** (First Party Applications, configured inside `descope_project`) and **Inbound Apps** (Third Party Applications with Scopes and Consent, managed as a standalone `descope_inbound_app` resource). #### Federated Apps Use the `applications` block inside `descope_project` to configure OIDC and SAML applications for outbound SSO integrations. ```hcl applications = { oidc_applications = [ { name = "My Web App" description = "Primary OIDC application" force_authentication = false claims = ["sub", "exp", "email"] backchannel_logout_url = "https://example.com/backchannel-logout" } ] saml_applications = [ { name = "My SAML App" description = "Enterprise SAML integration" force_authentication = false default_signature_algorithm = "sha256" manual_configuration = { acs_url = "https://example.com/saml/acs" entity_id = "https://example.com" } } ] } ``` [Full applications schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/project#nestedatt--applications) #### Inbound Apps Use `descope_inbound_app` to manage third-party applications that authenticate users via Descope as an OAuth 2.0 identity provider. OAuth clients, MCP server configurations, and partner integrations all benefit from being version-controlled alongside your project. ```hcl resource "descope_inbound_app" "my_oauth_client" { project_id = descope_project.myproject.id name = "My OAuth Client" description = "OAuth client application" logo_url = "https://example.com/logo.png" login_page_url = "__BaseURL__" approved_callback_urls = [ "https://myapp.com/callback", "http://localhost:3000/callback" ] permissions_scopes = [ { name = "read:profile" description = "Read the user's profile information" values = ["read:data"] }, { name = "write:profile" description = "Modify the user's profile information" optional = true values = ["write:data"] } ] attributes_scopes = [ { name = "email" description = "The user's email address" values = ["email"] } ] } ``` Scopes (`permissions_scopes`, `attributes_scopes`, `connections_scopes`) each take the same shape: | Field | Required | Description | |---|---|---| | `name` | Yes | Unique identifier for the scope | | `description` | Yes | Description shown during the consent flow | | `values` | No | Identifiers of the underlying permissions, attributes, or connections this scope grants | | `optional` | No | When `true`, the user may decline to grant this scope during authorization | [Full inbound app schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/inbound_app) ### Descope Engine Use `descope_engine` to register a [Descope Engine](/connectors/descope-engine) as code. The Engine lets Connectors reach resources inside your private network, but it runs as a container you deploy yourself—the Terraform resource only manages the Engine's registration with Descope (its `id` and `secret`), not the container. ```hcl resource "descope_engine" "private_network" { project_id = descope_project.myproject.id name = "Private Network Engine" } ``` [Full engine schema reference](https://registry.terraform.io/providers/descope/descope/latest/docs/resources/engine) ## Using Terraform Within Your Environment Terraform tracks your Descope project in a state file. Store it somewhere your team can access — remote backends like S3 or Terraform Cloud work well. * Run `terraform plan` to preview changes before applying. * Run `terraform apply` to apply them. # Test Users (/test-users) Learn how to easily implement test user management and authorization for your app with Descope. # Test User Management Descope supports the ability to test end-to-end authentication via test users. Test users can be used to test OTP via email, SMS, or voice, Magic Link via email or SMS, and Enchanted Link via email. Utilizing test users, you can generate OTP codes and Magic/Enchanted link tokens for test users directly using the Descope API and SDK, without sending actual communications to the test account. Once you have the generated code and token, you will verify them with the related OTP, Magic Link, or Enchanted Link verification functions in the SDK or API. Test users are not audited, counted as active users, or monitored within your analytics. ### Creating Test Users #### Descope API or SDK Test users can be generated via the Management SDK's [Create Test User](/test-users/sdks#create-test-user) function or by the management API [Create User](/api/management/users/create-user) function. When creating the test user via the API, you must ensure you pass the `test=true` attribute. #### Dynamic Test User creation Descope allows you to dynamically create test users based on a configured email address regex. This allows you to test your just-in-time user creation during the sign-up-or-in process within your flows or other testing. When the email matches this regex, the user will be created as a test user. You can configure and test this regext within the Test Users section within the [Project Settings](https://app.descope.com/settings/project) area of the Descope console. After the user initiates the authentication process as a dynamic test user, the same SDK and API endpoints for generating OTP, magic links, etc. for test users, are still applicable to the dynamically created test user. It is essential to monitor and clean up your dynamic test users as it could quickly reach the [Test User Limits](/test-users#test-user-limits). ![Descope dynamic test user regex configuration and test](/assets/dynamic-test-user-regex.webp) ### Test User in User Management Once the test users have been created, they can be viewed in the users table in the Descope console. A column "Is Test User" is part of the table where you can see the label against test users. ![Test Users in user console](/assets/test-users-dashboard.webp) Descopers can also use advance filter option to view the test user with the condition as shown below. This will help list out all existing test users in the table. ![Test Users using Filter](/assets/test-users-filter.webp) ### Test User Verifiers When utilizing test users for MFA, it is recommended to generate OTP codes for flows involving email or phone OTP. Additionally, in the [Project Settings](https://app.descope.com/settings/project) under Test User, you can enable Static OTP codes. The regex pattern is tested against the verifier to help narrow the access to static codes. Note that a warning will appear when enabling Static OTP codes. To learn more about setting up Static OTP codes for test users, please refer to the [Static OTP Guide](/test-users/static-otp-guide). This is an insecure method and is only recommended when generated OTP codes are not viable for testing. Therefore, this setting is ignored during project [imports and exports](/how-to-deploy-to-production/managing-environments). ![Static OTP for test user with regex for verifier](/assets/static-otp-test-user.webp) ### Test User Limits Free tier plans are limited to 5 test users per project. Pro-tier plans allow for 100 test users per project. Learn more about upgrading your tier within our [pricing overview](https://www.descope.com/pricing). When the maximum number of test users has been reached, the user will be presented with the below error: ```json { "errorCode": "E111111", "errorDescription": "Failed to create user", "errorMessage": "... [E013002] Failed to create record [error: [E013002] Test users limit exceeded]", "message": "... [E013002] Failed to create record [error: [E013002] Test users limit exceeded]" } ``` ## Test User JWT When utilizing test users, their JWT can be identified by the `"tu": true` immutable claim. ```json { "amr": [ "email" ], "drn": "DS", "exp": 1696964429, "iat": 1696963829, "iss": "xxxxx", "rexp": "2023-11-07T18:50:29Z", "sub": "xxxxxx", "tu": true } ``` # With SDKs (/test-users/sdks) Learn how to easily implement test user for your app with Descope using the Descope backend SDKs. # Test Users with Management SDKs The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). You can use Descope management SDK for common test user management operations. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key="xxxx") except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Create Test User This operation creates a new test user within the project with the details provided. ```javascript // Args: // loginId (str): The login ID of the test user to be created. const loginId = "xxxx" // email (str): Optional user email address of the test user to be created. const email = "email@company.com" // phone (str): Optional user phone number of the test user to be created. const phone = "+12223334455" // displayName (str): Optional user display name of the test user to be created. const displayName = "Joe Person" // roles (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. const roles = ["TestRole1"] // userTenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. const userTenants = [{ tenantId: 'TestTenant', roleNames: ['TestRole'] }] // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = {"Attribute 1": "Value 1", "Attribute 2": "Value 2"} // picture (str): Optional url for user picture const picture = "xxxx" // verifiedEmail (bool): Set to true for the user to be able to login with the email address. const verifiedEmail = true // or false // verifiedPhone (bool): Set to true for the user to be able to login with the phone number. const verifiedPhone = true // or false // additionalLoginIds (optional List[str]): An optional list of additional login IDs to associate with the user const additionalLoginIds = ["MyUserName", "+12223334455"] const resp = await descopeClient.management.user.createTestUser( loginId, email, phone, displayName, roles, userTenants, customAttributes, picture, verifiedEmail, verifiedPhone, null, null, null, additionalLoginIds ); if (!resp.ok) { console.log("Failed to create test user") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created test user") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the test user to be created. # email (str): Optional user email address of the test user to be created. # phone (str): Optional user phone number of the test user to be created. # display_name (str): Optional user display name of the test user to be created. # role_names (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles. # user_tenants (List[AssociatedTenant]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`. # picture (str): Optional url for user picture # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app # verified_email (bool): Set to true for the user to be able to login with the email address. # verifiedPhone (bool): Set to true for the user to be able to login with the phone number. # additional_login_ids (optional List[str]): An optional list of additional login IDs to associate with the user try: resp = descope_client.mgmt.user.create_test_user( login_id="xxxx", email="email@company.com", phone="+12223334455", display_name="Joe Person", role_names=["TestRole"], user_tenants=[AssociatedTenant("TestTenant")], picture="xxxx", custom_attributes={"Attribute 1": "Value 1", "Attribute 2": "Value 2"}, verified_email=True, # or False verified_phone=True, # or False additional_login_ids=["MyUserName", "+12223334455"] ) print("Successfully created user.") print(resp) except AuthException as error: print("Unable to create user.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user to be deactivated. loginID := "xxxx" // userReq (&descope.UserRequest): A list of the user's details. userReq := &descope.UserRequest{} userReq.Email = "email@company.com" userReq.Name = "Joe Person" userReq.Tenants = []*descope.AssociatedTenant{ {TenantID: "tenant-ID1", Roles: []string{"role-name1"}}, {TenantID: "tenant-ID2"}, } VerifiedEmail := true // or false userReq.VerifiedEmail = &VerifiedEmail VerifiedPhone := true // or false userReq.VerifiedPhone = &VerifiedPhone userReq.AdditionalLoginIds = ["MyUserName", "+12223334455"] res, err := descopeClient.Management.User().CreateTestUser(ctx, loginID, userReq) if (err != nil){ fmt.Println("Unable to create test user.", err) } else { fmt.Println("Successfully created test user.") fmt.Println(res) } ``` ```java // User for test can be created, this user will be able to generate code/link without // the need of 3rd party messaging services. // Test user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. UserService us = descopeClient.getManagementServices().getUserService(); try { UserResponseDetails resp = us.createTestUser("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .tenants(Arrays.asList( AssociatedTenant.builder() .tenantId("tenant-ID1") .roleNames(Arrays.asList("role-name1"), AssociatedTenant.builder() .tenantId("tenant-ID2")))); .additionalLoginIDs("MyUserName", "+12223334455")); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.mgmt.user.create_test_user( login_id="xxxx", email="email@company.com", display_name="Joe Person", user_tenants=[ AssociatedTenant("my-tenant-id", ["role-name1"]), ], ) ``` ### Generate OTP For Test User This operation generates an OTP code for authenticating a test user. Note that signin is not complete without the [user verification step](/auth-methods/otp/with-sdks/backend#user-verification), which can be performed using the backend SDKs. ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginId (str): The login ID of the user. const loginId = "xxxx" const resp = await descopeClient.management.user.generateOTPForTestUser(deliveryMethod, loginId); if (!resp.ok) { console.log("Failed to generate test user OTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully generated test user OTP") console.log("OTP Code: " + resp.data.code) } ``` ```python # Args: # login_id (str): The login ID of the user. # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL try: resp = descope_client.mgmt.user.generate_otp_for_test_user(method=DeliveryMethod.EMAIL, login_id="xxxx") code = resp.get("code", "") print("Successfully generated OTP code for test user") print("OTP Code: " + code) except AuthException as error: print("Unable to generate test OTP.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID (str): The login ID of the user. loginID := "xxxx" code, err := descopeClient.Management.User().GenerateOTPForTestUser(ctx, deliveryMethod, loginID) if (err != nil){ fmt.Println("Unable to generate test OTP.", err) } else { fmt.Println("Successfully generated test OTP.") fmt.Println(code) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // OTP code can be generated for test user, for example: try { OTPTestUserResponse res = us.generateOtpForTestUser("email@company.com", DeliveryMethod.EMAIL); // Use res.getCode() for verify and establishing a session } catch (DescopeException de) { // Handle the error } ``` ```ruby # OTP code can be generated for test user, for example: resp = descope_client.generate_otp_for_test_user( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'login-id' ) code = resp['code'] # Now you can verify the code is valid (using descope_client.*.verify for example) ``` ### Generate Magic Link For Test User This operation generates an Magic Link for authenticating a test user. The response contains the link, which contains the token needed to verify the user within the [user verification step](/auth-methods/magic-link/with-sdks/backend#user-verification), which can be performed using the backend SDKs. The token arrives as a query parameter named 't' which can be parse out of the link, see examples below. ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginId (str): The login ID of the user. const loginId = "xxxx" // uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "" // This can be an empty string for testing purposes const resp = await descopeClient.management.user.generateMagicLinkForTestUser(deliveryMethod,loginId, uri); if (!resp.ok) { console.log("Failed to generate test user Magic Link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const token = resp.data.link.split("?t=")[1] console.log("Successfully generated test user Magic Link") console.log("Magic Link Token: " + token) } ``` ```python # Args: # login_id (str): The login ID of the user. # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL # uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' try: resp = descope_client.mgmt.user.generate_magic_link_for_test_user(method=DeliveryMethod.EMAIL, login_id="xxxx", uri="") # uri can be an empty string for testing purposes token = resp.get("link", "").split("?t=")[1] print("Successfully generated Magic Link for test user") print("Magic Link Token: " + token) except AuthException as error: print("Unable to generate test Magic Link.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user. loginID := "xxxx" // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' uri := "" // This can be an empty string for testing purposes link, err := descopeClient.Management.User().GenerateMagicLinkForTestUser(ctx, deliveryMethod, loginID, uri) if (err != nil){ fmt.Println("Unable to generate test Magic Link.", err) } else { token := strings.Split(link, "?t=")[1] fmt.Println("Successfully generated test Magic Link.") fmt.Println("Magic Link Token: " + token) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // Same as OTP, magic link can be generated for test user, for example: try { MagicLinkTestUserResponse res = us.generateMagicLinkForTestUser("email@company.com", "", DeliveryMethod.EMAIL); // Use res.getLink() to get the generated link. To get the actual token, use: // var params = UriUtils.splitQuery("https://example.com" + res.getLink()); // var authInfo = magicLinkService.verify(params.get("t").get(0)); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Same as OTP, magic link can be generated for test user, for example: resp = descope_client.generate_magic_link_for_test_user( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'login-id', ) link = resp['link'] ``` ### Generate Enchanted Link For Test User This operation generates an Enchanted Link for authenticating a test user. The generate Enchanted Link for test user call returns a `pendingRef` and a `link`. Parse the link to capture the token, then utilize the backend SDK to [verify the token](/auth-methods/enchanted-link/with-sdks/backend#user-verification). You will also need to utilize the `pendingRef` to poll for verification status in order to receive the test user's JWT. ```javascript // Args: // loginId (str): The login ID of the user. const loginId = "xxxx" // uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "" // This can be an empty string for testing purposes const resp = await descopeClient.management.user.generateEnchantedLinkForTestUser(loginId, uri); if (!resp.ok) { console.log("Failed to generate test user Enchanted Link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const token = resp.data.link.split("?t=")[1] console.log("Successfully generated test user Enchanted Link") console.log("Enchanted Link Token: " + token) console.log("Enchanted Link Pending Ref: " + resp.get("pendingRef", "")) } ``` ```python # Args: # login_id (str): The login ID of the user. # uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' try: resp = descope_client.mgmt.user.generate_enchanted_link_for_test_user(login_id="xxxx", uri="") # uri can be an empty string for testing purposes token = resp.get("link", "").split("?t=")[1] print("Successfully generated Enchanted Link for test user") print("Enchanted Link Token: " + token) print("Enchanted Link Pending Ref: " + resp.get("pendingRef", "")) except AuthException as error: print("Unable to generate test Enchanted Link.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginId (str): The login ID of the user. loginId := "xxxx" // uri: this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' uri := "" // This can be an empty string for testing purposes link, pendingRef, err := descopeClient.Management.User().GenerateEnchantedLinkForTestUser(ctx, loginID, uri) if (err != nil){ fmt.Println("Unable to generate test Enchanted Link.", err) } else { token := strings.Split(link2, "?t=")[1] fmt.Println("Successfully generated test Enchanted Link.") fmt.Println("Enchanted Link Token: " + token) fmt.Println("Enchanted Link Pending Ref: " + pendingRef) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // Enchanted link can be generated for test user, for example: try { EnchantedLinkTestUserResponse res = us.generateEnchantedLinkForTestUser("email@company.com", ""); // Use res.getLink() to get the generated link. To get the actual token, use: // var params = UriUtils.splitQuery("https://example.com" + res.getLink()); // enchantedLinkService.verify(params.get("t").get(0)); // var authInfo = enchantedLinkService.getSession(res.getPendingRef()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Enchanted link can be generated for test user, for example: resp = descope_client.generate_enchanted_link_for_test_user( 'login-id', '' ) link = resp['link'] ``` ### Generate Embedded Link For Test User This operation generates an Embedded Link for authenticating a test user. The response returns the token to be used in the [user verification step](/auth-methods/embedded-link/with-sdks/backend#embedded-link-verification), which can be performed using the backend SDKs. ```javascript // Args: // loginId (str): The login ID of the user. const loginId = "xxxx" // customClaims (dict): Custom claims to add to JWT, system claims will be filtered out const customClaims = {"custom-key1": "custom-value1", "custom-key2": "custom-value2"} const resp = await descopeClient.management.user.generateEmbeddedLink(loginId, customClaims); if (!resp.ok) { console.log("Failed to generate test user Embedded Link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const token = resp.data.token; console.log("Successfully generated test user Embedded Link") console.log("Embedded Link Token: " + token) } ``` ```python # Args: # login_id (str): The login ID of the user. # custom_claims (dict): Custom claims to add to JWT, system claims will be filtered out try: token = descope_client.mgmt.user.generate_embedded_link(login_id="xxxx", custom_claims={"custom-key1": "custom-value1", "custom-key2": "custom-value2"}) print("Successfully generated Embedded Link for test user") print("Embedded Link Token: " + token) except AuthException as error: print("Unable to generate test Embedded Link.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginId (str): The login ID of the user. loginId := "xxxx" // customClaims (map[string]): Custom claims to add to JWT, system claims will be filtered out customClaims := map[string]any{"custom-key1": "custom-value1", "custom-key2": "custom-value2"} token, err := descopeClient.Management.User().generateEmbeddedLink(ctx, loginID, customClaims) if (err != nil){ fmt.Println("Unable to generate test Embedded Link.", err) } else { fmt.Println("Successfully generated Embedded Link for test user") fmt.Println("Embedded Link Token: " + token) } ``` ```java // Args: // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery String loginId = "email@company.com"; // customClaims: Additional claims to place on the jwt after verification Map customClaims = new HashMap() {{ put("custom-key1", "custom-value1");}} UserService us = descopeClient.getManagementServices().getUserService(); // Embedded link can be generated for test user, for example: try { String token = us.generateEmbeddedLink(loginID, customClaims); } catch (DescopeException de) { // Handle the error } ``` ```ruby token = descope_client.generate_embedded_link(login_id: 'person@company.com', custom_claims: {'key1':'value1'}) ``` ### Delete a Test User This operation allows administrators to delete an existing test user. This action will delete the users forever and they will not be recoverable. ```javascript // Args: // login_id (str): The login_id of the user to be deleted. const loginId = "email@company.com" const resp = await descopeClient.management.user.delete(loginId); if (!resp.ok) { console.log("Failed to delete user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user that's to be deleted. try: resp = descope_client.mgmt.user.delete(login_id="xxxxx") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The loginID of the user to be deleted. var loginID = "email@company.com" err := descopeClient.Management.User().Delete(ctx, loginID) if (err != nil){ fmt.Println("Unable to delete user: ", err) } else { fmt.Println("User Successfully deleted") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); try { us.delete("email@company.com"); } catch (DescopeException de) { // Handle the error } ``` ```ruby # User deletion cannot be undone. Use carefully. descope_client.delete_user('person@company.com') ``` ### Delete All Test Users This operation deletes all test users within the project. This action will delete these users forever and they will not be recoverable. ```javascript // Args: // None const resp = await descopeClient.management.user.deleteAllTestUsers(); if (!resp.ok) { console.log("Failed to delete test users") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted test users") } ``` ```python # Args: # None try: descope_client.mgmt.user.delete_all_test_users() print ("Successfully deleted test users") except AuthException as error: print ("Failed to delete test users") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() err := descopeClient.Management.User().DeleteAllTestUsers(ctx) if (err != nil){ fmt.Println("Unable to delete test users.", err) } else { fmt.Println("Successfully deleted test users.") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // Now test user got created, and this user will be available until you delete it, // you can use any management operation for test user CRUD. // You can also delete all test users. try { us.deleteAllTestUsers(); } catch (DescopeException de) { // Handle the error } ``` ```ruby # You can also delete all test users. descope_client.delete_all_test_users ``` # Static OTP Guide (/test-users/static-otp-guide) Learn how to set up a static OTP test user so Mobile App Store reviewers can sign in to your mobile app without receiving a real OTP. # Static OTP for Mobile App Store Review Apple and Google both require a working demo account when they review a mobile app that sits behind a login screen. Their reviewers can't receive a text message or open a real inbox, so a normal OTP flow will fail their review. With [static OTP for test users](/test-users#test-user-verifiers), you configure one fixed code for a specific test account, while every other user still receives a real, one-time code. This guide sets up a dedicated reviewer account with a static OTP, so you can hand Apple and Google a login ID and code that works. ## Step 1: Create a Reviewer Test User Create a dedicated test user for the reviewer, rather than reusing a real account or a shared team login ID. Test users don't count as active users and aren't audited or included in analytics, so they're a safe fit for this. Use a login ID scoped to this purpose, for example `appstore-reviewer@yourcompany.com` or a phone number set aside only for reviews. Create it one of two ways: - **Management API**: Call [Create User](/api/management/users/create-user) with the `test=true` attribute. - **Management SDK**: Use the [Create Test User](/test-users/sdks#create-test-user) function. Test users count against your project's [test user limits](/test-users#test-user-limits) (5 on the free tier, 100 on Pro-tier plans). ## Step 2: Enable Static OTP for the Reviewer Account In the Descope Console, go to **Project Settings → General → Test Users**. ![Test Users Project Settings](/assets/test-users-project-settings.webp) Turn on **Static OTP code** and configure: - **Static OTP code**: The fixed code Descope returns instead of a randomly generated one when the verifier regex matches. - **Verifier regex**: A pattern matched against the verifier (the email address or phone number the OTP would normally be sent to), not the login ID. Scope it as narrowly as possible, for example an exact match on `appstore-reviewer@yourcompany.com` rather than a broad domain pattern. This is an insecure method by design: anyone who knows the login ID and static code can sign in as that test user. Keep the verifier regex narrow, and apply it only to accounts you created for this purpose. This setting is also ignored during project [imports and exports](/how-to-deploy-to-production/managing-environments), so it won't carry over if you clone or promote environments. ## Step 3: Verify the Sign-In Flow Yourself Before submitting your app for review, walk through the flow as a reviewer would. Open your app's normal sign-in screen, enter the reviewer login ID, and confirm the static code works without Descope sending a real message. No code changes are needed - this works with your existing OTP setup, whether that's [Flows](/flows) or the [mobile SDKs](/auth-methods/otp/with-sdks/mobile). ## Step 4: Submit the Credentials to Apple/Google Provide the reviewer login ID and static OTP code in the review notes with your app submission, and note that the OTP field expects that fixed code. ## Best Practices Keep the verifier regex scoped to the exact reviewer verifier, not a pattern that could also match other test or real accounts. The static code applies to anything that matches it. Revisit this setup whenever Apple or Google re-reviews your app, for example after a major update, since the reviewer account and static code need to keep working. # Developing Locally with Cookies (/unit-testing/local-testing-tokens) Manage SameSite Descope cookie restrictions in localhost testing by creating separate dev and prod projects. # Developing Locally with Cookie-Based Tokens When testing in your local environment, you may encounter a `401 Unauthorized` error when trying to authenticate if you're storing your [session tokens in cookies](/security-best-practices/session-token-storage#session-token-management). This issue arises because cookies are tied to your configured custom domain or `api.descope.com` and are not sent in requests made from a different origin like `localhost`. When your Descope settings are set to manage the session token in cookies, the cookie is set from the backend as `http-only`. As a workaround, you can use our SDK to set the cookie from the client-side instead. This guide provides steps to test cookies in a local development environment. It's generally recommended to create a separate Descope project for local development and testing, so these cookie settings will not impact your production environment. ## Configure Descope Project To test locally, adjust the Cookie Policy settings in your [Descope Project Settings](https://app.descope.com/settings/project) to ensure proper functionality on `localhost`. ### 1. Manage the Refresh Token in either response body or cookies: - If you choose to manage the refresh token in cookies and you have a configured [custom domain](/how-to-deploy-to-production/custom-domain), make sure the cookie policy is set to `None`. The `None` policy explicitly allows cookies to be sent in cross-origin requests, which is necessary when running your application on `localhost` while the cookies are tied to your custom domain. ### 2. Store the session token in the response body: - For the Session Token, select "Manage in response body". Ensure you **Save** these changes. ![Descope settings showing refresh and session token cookie management](/assets/attribution-mappings-both-refresh-session.webp) ### 3. Use the SDK flag: When using either a custom domain or `api.descope.com`, Descope returns the authentication response to the client with a `Set-Cookie` response header. Since `localhost` is not part of that cookie domain, the session token cannot be set this way when developing locally. A workaround is available if you're using one of Descope's frontend SDKs. The SDK reads the server response and sets a cookie on the `localhost` domain to mimic the regular cookie setup with Descope. This is done using the `sessionTokenViaCookie` parameter on each SDK: In the Next.js SDK, `sessionTokenViaCookie` is automatically set to `true`. ```tsx import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( ); }; ``` ```js import descopeSdk from '@descope/web-js-sdk'; const sdk = descopeSdk({ projectId: 'my-project-id', sessionTokenViaCookie: true }); ``` ```js import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { DescopeAuthModule } from '@descope/angular-sdk'; @NgModule({ declarations: [AppComponent], imports: [ BrowserModule, DescopeAuthModule.forRoot({ projectId: '__ProjectID__', sessionTokenViaCookie: true }) ], bootstrap: [AppComponent] }) export class AppModule {} ``` ```js import { createApp } from 'vue'; import App from './App.vue'; import descope from '@descope/vue-sdk'; const app = createApp(App); app.use(descope, { projectId: 'my-project-id', sessionTokenViaCookie: true }); app.mount('#app'); ``` ## Passing Cookies To the Backend To send the authentication cookies from the browser to your backend, configure your HTTP client to include credentials. For example, with `axios` set `withCredentials: true`, or with `fetch` use `credentials: 'include'`. For your backend, you must make sure your CORS settings explicitly allow credentials and specify your app's origin. With each request, you can read the session cookie sent from the client, and validate the session using our session validation functions in our backend SDKs to authenticate the user. # With SDKs (/user-impersonation/impersonation-with-sdks) Learn how to easily implement user impersonation for your app with Descope using the Descope backend SDKs. # User Impersonation with Management SDKs The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). You can use Descope management SDK for user impersonation operations. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" dotnet add package descope ``` ### Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key="xxxx") except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```csharp // appsettings.json { "Descope": { "ProjectId": "your-project-id", "ManagementKey": "your-management-key" } } // Program.cs using Descope; using Microsoft.Extensions.Configuration; // ... In your setup code var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); var descopeProjectId = config["Descope:ProjectId"]; var descopeManagementKey = config["Descope:ManagementKey"]; var descopeConfig = new DescopeConfig(projectId: descopeProjectId); var descopeClient = new DescopeClient(descopeConfig) { ManagementKey = descopeManagementKey, }; ``` ### Impersonate User You can also use our `/impersonation` [API](/api/management/users/impersonate) to impersonate a user. This operation allows administrators to impersonate an existing user. The impersonator user must have the [impersonation permission](/user-impersonation#permissions-and-access-control) in order for this request to work. On success, the response will be a refresh JWT of the impersonated user. ```javascript // Args: // impersonatorId (str): The login_id of the user that's doing the impersonating. // loginId (str): The login_id of the user that's to be impersonated. // validateConsent (boolean): Whether to check if the user to be impersonated has given consent // customClaims (object): Optional, custom claims to be added to the impersonated user's JWT // tenantId (str): Optional, one of the tenants the impersonated user belongs to // refreshDuration (number): Optional, duration in seconds for which the new JWT will be valid const impersonatorId = "admin@company.com" const loginId = "user@company.com" const validateConsent = true const customClaims = {"key1": "value1"} const tenantId = "your-tenant-id" const refreshDuration = 3600 const resp = await descopeClient.management.jwt.impersonate( impersonatorId, loginId, validateConsent, customClaims, tenantId, refreshDuration ); if (!resp.ok) { console.log("Failed to impersonate user") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully impersonated user") console.log(resp.data) } ``` ```python # Args: # impersonator_id (str): The login_id of the user that's doing the impersonating. # login_id (str): The login_id of the user that's to be impersonated. # validate_consent (boolean): Whether to check if the user to be impersonated has given consent # custom_claims (dict): Optional, custom claims to be added to the impersonated user's JWT # tenant_id (str): Optional, one of the tenants the impersonated user belongs to # refresh_duration (int): Optional, duration in seconds for which the new JWT will be valid try: refresh_jwt = descope_client.mgmt.jwt.impersonate( impersonator_id="admin@company.com", login_id="user@company.com", validate_consent=True, custom_claims={"key1": "value1"}, tenant_id="your-tenant-id", refresh_duration=3600 ) print("Successfully impersonated user.") print(refresh_jwt) except AuthException as error: print("Unable to impersonate user.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // impersonatorID (str): The login_id of the user that's doing the impersonating. impersonatorID := "admin@company.com" // loginID (str): The login_id of the user that's to be impersonated. loginID := "user@company.com" // validateConsent (boolean): Whether to check if the user to be impersonated has given consent validateConsent := true // customClaims (map[string]interface{}): Optional, custom claims to be added to the impersonated user's JWT customClaims := map[string]interface{}{"key1": "value1"} // tenantID (str): Optional, one of the tenants the impersonated user belongs to tenantID := "your-tenant-id" // refreshDuration (int32): Optional, a custom refresh duration in seconds for the JWT refreshDuration := int32(3600) refreshJWT, err := descopeClient.Management.JWT().Impersonate(ctx, impersonatorID, loginID, validateConsent, customClaims, tenantID, refreshDuration) if err != nil { fmt.Println("Unable to impersonate user.", err) } else { fmt.Println("Successfully impersonated user.") fmt.Println(refreshJWT) } ``` ```java // Args: // impersonatorId (String): The login ID of the user that's doing the impersonating. // loginId (String): The login ID of the user that's to be impersonated. // validateConsent (boolean): Whether to check if the user to be impersonated has given consent. // customClaims (Map): Optional, custom claims to add to the impersonated user's JWT. // tenantId (String): Optional, one of the tenants the impersonated user belongs to. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { String refreshJwt = jwts.impersonate( "admin@company.com", "user@company.com", true, new HashMap() {{ put("key1", "value1"); }}, "your-tenant-id"); System.out.println("Successfully impersonated user."); System.out.println(refreshJwt); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // ImpersonatorId (string): The login ID of the user that's doing the impersonating. // LoginId (string): The login ID of the user that's to be impersonated. // ValidateConsent (bool?): Whether to check if the user to be impersonated has given consent. // CustomClaims (ImpersonateRequest_customClaims): Optional, custom claims to add to the impersonated user's JWT. // SelectedTenant (string): Optional, one of the tenants the impersonated user belongs to. // RefreshDuration (int?): Optional, duration in seconds for which the new JWT will be valid. var customClaims = new ImpersonateRequest_customClaims(); customClaims.AdditionalData["key1"] = "value1"; var impersonateRequest = new ImpersonateRequest { ImpersonatorId = "admin@company.com", LoginId = "user@company.com", ValidateConsent = true, CustomClaims = customClaims, SelectedTenant = "your-tenant-id", RefreshDuration = 3600, }; var impersonateResponse = await descopeClient.Mgmt.V1.Impersonate.PostAsync(impersonateRequest); Console.WriteLine("Successfully impersonated user."); Console.WriteLine(impersonateResponse?.Jwt); ``` ### Impersonate User with Step-Up You can also use our `/impersonate/stepup` [API](/api/management/users/impersonate-stepup) to impersonate a user with step-up authentication. This operation allows administrators to impersonate an existing user and receive a step-up session JWT for the impersonated user. The impersonator user must have the [impersonation permission](/user-impersonation#permissions-and-access-control) in order for this request to work. On success, the response will be a session JWT of the impersonated user. ```python # Args: # impersonator_id (str): The login_id of the user that's doing the impersonating. # login_id (str): The login_id of the user that's to be impersonated. # validate_consent (boolean): Whether to check if the user to be impersonated has given consent # custom_claims (dict): Optional, custom claims to be added to the impersonated user's JWT # tenant_id (str): Optional, one of the tenants the impersonated user belongs to # refresh_duration (int): Optional, duration in seconds for which the new JWT will be valid # stepup (bool): Whether to generate a step-up token for the impersonated user try: session_jwt = descope_client.mgmt.jwt.impersonate( impersonator_id="admin@company.com", login_id="user@company.com", validate_consent=True, custom_claims={"key1": "value1"}, tenant_id="your-tenant-id", refresh_duration=3600, stepup=True ) print("Successfully impersonated user with step-up.") print(session_jwt) except AuthException as error: print("Unable to impersonate user with step-up.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // impersonatorID (str): The login_id of the user that's doing the impersonating. impersonatorID := "admin@company.com" // loginID (str): The login_id of the user that's to be impersonated. loginID := "user@company.com" // validateConsent (boolean): Whether to check if the user to be impersonated has given consent validateConsent := true // customClaims (map[string]interface{}): Optional, custom claims to be added to the impersonated user's JWT customClaims := map[string]interface{}{"key1": "value1"} // tenantID (str): Optional, one of the tenants the impersonated user belongs to tenantID := "your-tenant-id" // refreshDuration (int32): Optional, a custom refresh duration in seconds for the JWT refreshDuration := int32(3600) sessionJWT, err := descopeClient.Management.JWT().ImpersonateStepup(ctx, impersonatorID, loginID, validateConsent, customClaims, tenantID, refreshDuration) if err != nil { fmt.Println("Unable to impersonate user with step-up.", err) } else { fmt.Println("Successfully impersonated user with step-up.") fmt.Println(sessionJWT) } ``` ```java // Args: // impersonatorId (String): The login ID of the user that's doing the impersonating. // loginId (String): The login ID of the user that's to be impersonated. // validateConsent (boolean): Whether to check if the user to be impersonated has given consent. // customClaims (Map): Optional, custom claims to add to the impersonated user's JWT. // tenantId (String): Optional, one of the tenants the impersonated user belongs to. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { String sessionJwt = jwts.impersonateStepup( "admin@company.com", "user@company.com", true, new HashMap() {{ put("key1", "value1"); }}, "your-tenant-id"); System.out.println("Successfully impersonated user with step-up."); System.out.println(sessionJwt); } catch (DescopeException de) { // Handle the error } ``` ### Stop User Impersonation You can also use our `/stop-impersonation` [API](/api/management/users/stop-impersonation) to impersonate a user. This feature enables users to seamlessly switch back to their original account during an impersonation session. ```javascript // Args: // jwt (string): The impersonation JWT to be stopped (required). // customClaims (object): Optional, custom claims to add to the new JWT // selectedTenant (string): Optional, the tenant ID to set on the DCT claim // refreshDuration (number): Optional, duration in seconds for which the new JWT will be valid const jwt = "xxxxxxxxx" const customClaims = {"role": "admin"} const selectedTenant = "tenant-123" const refreshDuration = 3600 const resp = await descopeClient.management.jwt.stopImpersonation( jwt, customClaims, selectedTenant, refreshDuration ); if (!resp.ok) { console.log("Failed to stop impersonation") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully stopped impersonation. New JWT issued:") console.log(resp.data.jwt) } ``` ```python # Args: # jwt (str): The impersonation JWT you want to stop. # custom_claims (dict): Optional, custom claims to add to the new JWT # tenant_id (str): Optional, tenant ID to set on the DCT claim # refresh_duration (int): Optional, duration in seconds for which the new JWT will be valid try: new_jwt = descope_client.mgmt.jwt.stop_impersonation( jwt="xxxxxxxx", custom_claims={"role": "admin"}, tenant_id="tenant_123", refresh_duration=3600 ) print("Successfully stopped impersonation. New JWT issued.") print("JWT:", new_jwt) except AuthException as error: print("Failed to stop impersonation") print("Status Code:", error.status_code) print("Error:", error.error_message) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // jwt (string): The impersonation JWT that you want to stop. jwt := "xxxxxxx" // customClaims (map[string]interface{}): Optional, custom claims to add to the new JWT (can be nil if not used) customClaims := map[string]interface{}{"role": "admin"} // tenantID (string): Optional, tenant ID for DCT claim context tenantID := "tenant-123" // refreshDuration (int32): Optional, a custom refresh duration in seconds for the JWT refreshDuration := int32(3600) resp, err := descopeClient.Management.JWT().StopImpersonation( ctx, jwt, customClaims, tenantID, refreshDuration ) if err != nil { fmt.Println("Failed to stop impersonation:", err) } else { fmt.Println("Successfully stopped impersonation. New JWT issued:") fmt.Println(resp.JWT) } ``` ```java // Args: // jwt (String): The impersonation JWT to be stopped (required). // customClaims (Map): Optional, custom claims to add to the new JWT. // tenantId (String): Optional, the tenant ID to set on the DCT claim. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { String newJwt = jwts.stopImpersonation( "xxxxxxxxx", new HashMap() {{ put("role", "admin"); }}, "tenant-123"); System.out.println("Successfully stopped impersonation. New JWT issued."); System.out.println(newJwt); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // Jwt (string): The impersonation JWT to be stopped (required). // CustomClaims (StopImpersonationRequest_customClaims): Optional, custom claims to add to the new JWT. // SelectedTenant (string): Optional, the tenant ID to set on the DCT claim. // RefreshDuration (int?): Optional, duration in seconds for which the new JWT will be valid. var customClaims = new StopImpersonationRequest_customClaims(); customClaims.AdditionalData["role"] = "admin"; var stopImpersonationRequest = new StopImpersonationRequest { Jwt = "xxxxxxxxx", CustomClaims = customClaims, SelectedTenant = "tenant-123", RefreshDuration = 3600, }; var stopImpersonationResponse = await descopeClient.Mgmt.V1.Stop.Impersonation.PostAsync(stopImpersonationRequest); Console.WriteLine("Successfully stopped impersonation. New JWT issued:"); Console.WriteLine(stopImpersonationResponse?.Jwt); ``` # User Impersonation (/user-impersonation) This guide will cover the fundamentals of user impersonation and how to impersonate users with Descope. # User Impersonation User impersonation is a Descope feature that enables privileged users to act on behalf of another user. It is particularly useful for: - **Troubleshooting:** Reproduce user issues directly. - **Testing:** Validate user-specific functionality without sharing credentials. - **User Support:** Provide direct assistance in the user's context. This guide explains how impersonation works, how to configure permissions, how to create and embed flows for consent, impersonation, stopping impersonation, step-up authentication, and how to understand and verify impersonation JWTs. ## Permissions and Access Control For more information on roles and permissions within Descope, you can review the [Authorization](/authorization/role-based-access-control) docs. Only users with the `Impersonate` permission can impersonate other users. This permission can be assigned in two ways: | Level | Description | |--------|--------------| | **Project-level** | Allows impersonating any user in the project. | | **Tenant-level** | Restricts impersonation to users within a specific tenant. | To grant impersonation rights: 1. In the [Authorization > RBAC](https://app.descope.com/authorization/rbac) page, create or edit a role. This can also be done programatically via [SDKs or APIs](/authorization/role-based-access-control/with-sdks) as well. 2. Add the `Impersonate` permission to the role. 3. In the [Users](https://app.descope.com/users) tab, assign the role to a user. Only trusted administrators or support staff should have impersonation access. ### Tenant Level Impersonation In Descope, impersonation privileges can be scoped to a tenant. This is useful when you want to restrict impersonation to users within a specific tenant. If a user has a tenant-level role with the `Impersonate` permission, they will only be able to impersonate users within that tenant. 1. In the [Authorization > RBAC](https://app.descope.com/authorization/rbac) page, create or edit a role (e.g., `Tenant Admin`). This can also be done programatically via [SDKs or APIs](/authorization/role-based-access-control/with-sdks) as well. 2. Assign the `Impersonate` permission to that role. 3. In the [Users](https://app.descope.com/users) tab, assign the role to the user for a specific tenant. This restricts impersonation to users within that tenant. ![Tenant-level impersonation example](/assets/impersonation-role-level.webp) ## Impersonation Flow Actions Descope provides **three** Flow actions related to impersonation: | Flow Action | Description | Configuration Options | |--------------|-------------|--------------| | **User / Impersonate** | Starts impersonation of another user. | Optionally validate user consent before impersonation. Optionally add a step-up claim to the issued JWT. | | **Update User / Impersonation Consent** | Collects user consent for impersonation. | Set **consent expiration (in hours)**. | | **User / Stop Impersonation** | Ends impersonation and returns to the original user's session. | No additional configuration available. | If you wish to allow/enforce user granting consent before impersonation, you can do so by adding the `Update User / Impersonation Consent` action to the flow, described in the [Gathering User Consent](#gathering-user-consent) section. Alternatively, consent can be granted programmatically via the [Update User Impersonation Consent API](/api/management/users/update-user-impersonation-consent). To allow the impersonating user to complete a step-up action within the impersonated session, see [Step-Up Authentication During Impersonation](#step-up-authentication-during-impersonation). Otherwise, skip to the [Impersonating a User with Descope Flows](#impersonating-a-user-with-descope-flows) section, to learn more about how to impersonate a user. ## Gathering User Consent Before impersonation, users can grant consent to be impersonated in two ways: 1. **User Impersonation Consent Flow** - User-facing flow where users explicitly grant consent (recommended for interactive scenarios) 2. **Management API** - Programmatically grant consent for automated setups and administrative workflows (see [Grant Consent with the Management API](#grant-consent-with-the-management-api)). ### Create the User Consent Flow In order to be able to grant consent to be impersonated, the user must be authenticated first. If the user is already authenticated, the user will still have to login again to grant consent. This effectively acts as a step-up or MFA action, in this case. ![An example of requiring the user to authenticate again before allowing consent](/assets/impersonation-consent-flow.webp) To allow the user to grant consent, add the `Update User / Impersonation consent` action to the flow by clicking the `+` in the top left of the Descope flow editor. Once added, connect within the flow accordingly, and edit the action to configure the **Consent Expiration In Hours**. ![An example of adding the Update User / Impersonation consent action within Descope flows](/assets/impersonation-consent-action.webp) Once the user consent has been granted, you will be able to view the consent expiration within the [User's page](https://app.descope.com/users) of the Descope console. ![An example of the consent expiration within the users page in the Descope Console](/assets/impersonation-consent-expiration.webp) ### Grant Consent with the Management API You can grant consent directly with the [Update User Impersonation Consent](/api/management/users/update-user-impersonation-consent) API, without the user completing a consent flow. This endpoint requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). ```bash curl -X POST "__BaseURL__/v1/mgmt/user/update/impersonationConsent" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "loginId": "user@company.com", "impersonationConsent": 1700000000 }' ``` - **`loginId`** (string, required) - The login ID of the user granting consent. - **`impersonationConsent`** (int32, required) - The Unix timestamp, in seconds, at which the consent expires. The `impersonationConsent` value is an absolute point in time, not a duration. Consent is considered granted only while that timestamp is in the future, which is checked whenever impersonation runs with consent validation enabled. Sending a timestamp in the past, or `0`, revokes any consent the user previously granted. The response contains the updated user object. ## Impersonating a User with Descope Flows This section covers how to create a flow that allows you to impersonate a user. If you wish to manage impersonation via SDKs or APIs, you can see our [User Impersonation with SDKs](/user-impersonation/impersonation-with-sdks) guide instead. ### Create the User Impersonation Flow In your impersonation flow, you'll need to create a screen with the `Impersonated User` input field within it. This field will take a login ID of the user you wish to impersonate. ![An example of a user impersonation screen within a Descope flow](/assets/impersonation-flow-screen.webp) Afterwards, you'll need to add the `User / Impersonate` action in the flow after the screen. If you wish to bypass user consent to be impersonated, you can uncheck the `Validate user consent` option on this action. However, this is not recommended when running in a production environment for security reasons. If you wish to perform actions that require a step-up claim, you can check the `Add stepup claim to JWT` option. More details on this can be found in the [Step-Up Authentication During Impersonation](#step-up-authentication-during-impersonation) section. ![An example of a impersonate user Descope flow](/assets/impersonation-flow-action.webp) Finally if you wish to allow the impersonating user to stop impersonation, you can add a path in your flow with the `User / Stop Impersonation` action to accomplish this. ### Using the Impersonation Flow In order to begin impersonating a user through a flow, a user who possesses the `Impersonate` permission must be authenticated first. See the [Permissions and Access Control](#permissions-and-access-control) section above for more details on setting up the required permissions. After the impersonator has successfully authenticated and impersonated a user, Descope will return a user token with the `sub` claim set to the impersonated user's ID. If you typically include additional claims in your user token, you'll need to add a [`Custom Claims Action`](/flows/actions/custom-claims) to the flow to set these claims. This is described in further detail [below](#displaying-impersonator-information). You can also validate that the user was successfully impersonated by searching the [Audit Trail](https://app.descope.com/audits) for the string `Impersonate`. If you wish to validate the impersonated session JWT, you can do so by sending the `DS` token to the [Validate Session](/api/session/validate-session) API endpoint. ### Stop Impersonation in Flow The `Stop Impersonation` action enables users to seamlessly switch back to their original account during an impersonation session, eliminating the need for manual logout and re-authentication. This action can be triggered after a `Load User` action in the flow, utilizing the existing impersonated session. ![Stop Impersonate Action in Descope flow](/assets/impersonation-flow-stop-action.webp) After adding this action, once you successfully end the flow, a JWT for the user will be returned without the impersonator's user ID within `act[sub]`. ```json { "drn": "DS", "exp": 1693325322, "iat": 1693324722, "iss": "P2UcQcyxrjSA3mr6y0xZKRYI5oJz", "rexp": "2023-09-26T15:58:42Z", "sub": "U2UcRpsA4DSJ8kBvp6jikPyJ3aJ8" } ``` ### Step-Up Authentication During Impersonation The `User / Impersonate` action includes an **Add stepup claim to JWT** option. When enabled, the issued impersonation token will include the `su: true` claim, signaling to your application that this session has step-up authorization. This is useful when you want to gate sensitive operations — such as accessing billing details or making account changes — on a stepped-up impersonation session. To enable it, open the `User / Impersonate` action in your flow and check **Add stepup claim to JWT**. ![The User / Impersonate action panel showing the Add stepup claim to JWT checkbox](/assets/impersonation-action-stepup.webp) When this option is enabled, the resulting impersonation JWT will include `su: true` alongside the standard `act.sub` claim: ```json { "act": { "sub": "U2UcRpsA4DSJ8kBvp6jikPyJ3aJ8" }, "sub": "U2UcRIInQ56lEhuRtxWx1NsmgxaS", "su": true, "iss": "P2UcQcyxrjSA3mr6y0xZKRYI5oJz", "iat": 1693324722, "exp": 1693325322, "rexp": "2023-09-26T15:58:42Z", "drn": "DS" } ``` ## Understanding Impersonation JWTs When impersonation succeeds, the session JWT reflects both the impersonator and impersonated user. The impersonator's User ID is stored within the `act.sub` claim. ```json { "act": { "sub": "U2UcRpsA4DSJ8kBvp6jikPyJ3aJ8" }, "sub": "U2UcRIInQ56lEhuRtxWx1NsmgxaS", "iss": "P2UcQcyxrjSA3mr6y0xZKRYI5oJz", "iat": 1693324722, "exp": 1693325322, "rexp": "2023-09-26T15:58:42Z", "drn": "DS" } ``` ### Displaying Impersonator Information In order to refresh the values of custom claims in the token, such as if a custom attribute value for the user is changed, you'll need to make a request to the [me](/client-sdk/auth-helpers#core-sdk-functions) endpoint, using either our SDKs or API. When using impersonation, you may want to display information about the impersonator in your application's frontend. To access information about the impersonator, you need to read the session JWT. In a flow, you can add information to the token by using the [`Custom Claims` Action](/flows/actions/custom-claims). Note that when user attribute custom claims (e.g. `user.name`) are set before the impersonation action in the flow, they reflect the impersonator. When custom claims are set after the impersonation action, they reflect the impersonated user. It's worth noting that if you're managing your [session token with cookies](/security-best-practices/session-token-storage#managing-with-cookies), the frontend will not be able to read the JWT claims to retrieve impersonator info. Therefore you'll have to manage returning this information manually from your backend, as described in the following steps: - From the frontend, send a request including the JWT to your backend. - In the backend, validate the JWT using one of the [Backend SDKs](/sessions/validation/backend#validate-session) or the [API](/api/session/validate-session). - Extract the impersonator's user ID from the `act.sub` claim in the validated JWT. - Use the [Management SDK](/management/user-management/sdks#load-existing-user-details) or [API](/api/management/users/load-user) to fetch the impersonator's details using the extracted user ID. - Return the impersonator's details to the frontend for display. ## Auditing All impersonation actions are logged in the [Audit Trail](https://app.descope.com/audits) as `LoginSucceed` with method `Impersonate`, showing both impersonator and impersonated user IDs. # Authentication (/auth-methods) Learn how to easily implement secure authentication flows and methods in your application with Descope. # Authentication Descope provides a comprehensive suite of authentication methods and flows to secure your application, including: - [One Time Password (OTP)](/auth-methods/otp) - [Magic link](/auth-methods/magic-link) - [Enchanted link](/auth-methods/enchanted-link) - [Social Login (OAuth)](/auth-methods/oauth) - [Single Sign-On (SSO)](/auth-methods/sso) - [Passkeys](/auth-methods/passkeys) - [Authenticator Apps (TOTP)](/auth-methods/auth-apps) - [Passwords](/auth-methods/passwords) - [Security Questions](/auth-methods/security-questions) - [nOTP (WhatsApp)](/auth-methods/notp) - [Embedded link](/auth-methods/embedded-link) - [Recovery Codes](/auth-methods/recovery-codes) - [Device Authentication](/auth-methods/device-auth) - [MCP Server Authentication (Agentic Identity)](/agentic-identity-hub/core-components/mcp-servers) ## Choosing the Right Authentication Method Selecting the appropriate authentication method depends on your application's security requirements and user experience goals. With Descope, you are not locked into a single authentication method. You can enable multiple methods at the same time and adjust your approach as your needs evolve. Here are some considerations to help you make the right choice: ### [When to Choose Passwordless Authentication](https://www.descope.com/learn/post/passwordless-authentication) - **Enhanced Security**: Eliminates password-related vulnerabilities and reduces the risk of credential-based attacks. - **Improved User Experience**: Streamlines the login process by removing the need to remember and manage passwords. - **Modern Approach**: Aligns with current security best practices and user expectations. ### [When to Choose Password-based Authentication](https://www.descope.com/learn/post/password-authentication) - **Familiar Experience**: Suitable for applications where users expect conventional authentication methods. - **No Email/Phone Requirement**: Supports username and password login without requiring email or phone, using a unique identifier to associate credentials. ## Understanding Auth Actions: When using Descope for authentication, there are a few key actions to understand: - **Sign Up**: Sign Up is used when creating a new user. It will fail if the user already exists. This action is typically used to onboard new users. - **Sign In**: Sign In is for logging in an existing user. It fails if the user doesn't already exist in your system, making it suitable for authentication flows where the user is known. - **Sign Up or In**: Sign Up or In combines the previous two, automatically logging in the user if they already exist or signing them up if they don't. This is used to simplify your flow logic and handle both processes with one action. - **Update User**: Update User allows you to enhance an existing user's authentication method, like adding a passkey or linking another login option to the same account. This doesn't create a new user but modifies the existing one to support more login methods. ## Using Authentication Methods in Flows These methods are fully compatible with both our client and backend SDKs, as well as our APIs. For more information on these, visit our [Getting Started](https://docs.descope.com/getting-started#would-you-like-to-integrate-descope-without-using-flows) guide. Here's a quick demonstration of adding an authentication method to a Flow: # Audit Trail Streaming (/audit-trails-and-integrations/audit-trail-streaming) This guide will cover the fundamentals and one use case regarding streaming your Descope audit trail to a third-party service. # Audit Trail Streaming Descope allows you to stream audit logs to third-party services like [AWS S3](/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-s3) and [Datadog](/connectors/connector-configuration-guides/audit-and-troubleshooting/datadog) using connectors. This enables enhanced storage scalability, improved data resilience, and access to advanced analytics tools. ## Audit Streaming with Connectors Set up connectors to stream your audit trail to third-party services. To view available options, search for audit on the [Connectors page](https://app.descope.com/connectors) in the Descope console. ### AWS S3 Example #### Configure the Connector Configure the AWS S3 Connector according to the instructions in the [AWS S3 Connector Doc](/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-s3). #### Viewing Audit Objects in Amazon S3 After configuring the connector, Descope Audit trails are stored in S3 as JSON objects. These objects are organized by project ID and date. You can open any object to view its details. ![An example of directory structure when streaming Descope audit logs to Amazon S3](/assets/example-amazon-s3-directory-structure.webp) ![An example of the date formatted directory structure when streaming Descope audit logs to Amazon S3](/assets/example-amazon-s3-directory-structure-2.webp) ## Audit Event remoteAddress Field Streamed audit events carry the originating client IP address in a top-level `remoteAddress` field, so you can read it without parsing request headers or digging into the nested `data` payload: ```json { "type": "LoginSucceed", "userID": "U2ESG1VEKbTdXnKk1AX3", "externalIDs": ["jane.doe@company.com"], "occurred": "2024-09-10T14:22:05Z", "method": "otp", "remoteAddress": "203.0.113.42", "data": {} } ``` Events that Descope cannot associate with a client IP either carry an empty value or omit the field. ### Field Naming by Destination Connectors that prefix log fields apply their prefix to `remoteAddress` as they do to every other field. The default prefix is `descope.`, which you can change or remove in the connector configuration. | Destination | Field name | | --- | --- | | AWS S3, Audit Webhook, Mixpanel | `remoteAddress` | | Datadog, Splunk, Google Cloud Logging, New Relic, Cribl, OpenTelemetry | `descope.remoteAddress` | | Sumo Logic | `descope_remote_address` | `remoteAddress` is never masked, even when **Mask PII Data** is enabled on the connector. See [Masking PII in Audit Logs](/audit-trails-and-integrations/masking-pii-in-audit-logs). ## Managing Streaming Errors ### Error Notifications The audit streaming connector will be automatically paused if the endpoint returns non-2xx HTTP status codes (such as 4xx or 5xx errors) multiple times consecutively. This is a protective measure to prevent continuous failed delivery attempts. When an audit streaming error occurs, Descope displays a toast notification in the console to alert you of the issue. This notification provides quick access to investigate and resolve the problem. ![Notification of a streaming error within Descope](/assets/streaming-error-notification.webp) ### Dismissing Error Notifications If you're not ready to address a streaming error immediately, you can dismiss the toast notification: 1. When the streaming error notification appears in the console, locate the dismiss button (typically an "X" or close icon) 2. Click the dismiss button to close the notification 3. The notification will be removed from view, allowing you to continue working 4. You can still access the [Connectors page](https://app.descope.com/connectors) later to review and resolve any streaming issues This allows you to manage your workflow without being interrupted by error notifications while still maintaining the ability to address connector issues when convenient. # Filtering Audit Events (/audit-trails-and-integrations/filtering-audit-events) Learn how to filter Descope audit events by event type, user, date range, and more using the Descope Console, Backend SDKs, or REST API. # Filtering Audit Events The Descope Audit Trail supports flexible filtering so you can quickly isolate the events that matter. Filter in the [Descope Console](https://app.descope.com/audits), through [Management SDKs](/audit-trails-and-integrations/sdks), or via the [Search Audit](/api/management/audit/search-audit) REST API endpoint. ## Filtering in the Descope Console The Descope Console provides a visual interface for searching and filtering your audit trail without writing any code. Navigate to the [Audit page](https://app.descope.com/audits) in your project to access these controls. ![audit filtering example](/assets/audit-filtering-example.webp) ### Filter by User Search for audit events tied to a specific user by entering their **Login ID** (e.g., email address or phone number) or **User ID** into the search bar. ![audit filtering by userID example](/assets/audit-filter-by-user.webp) ### Filter by Action You can use the action to search for items like `LoginStarted`, `LoginSucceed`, or general `Failed` items. See [Audit Events](/audit-trails-and-integrations/audit-events) for valid values. ![audit filtering by failed example](/assets/audit-filter-failed-action.webp) ### Additional Console Filters The Console also supports filtering by: - **Device**: Filter by device category: `Desktop`, `Mobile`, `Tablet`, `Bot`, or `Unknown`. - **Authentication Method**: Filter by method: `otp`, `totp`, `magiclink`, `oauth`, `saml`, or `password`. - **Geographic Location**: Filter by country code (e.g., `US`, `IL`). - **Remote Address**: Filter by originating IP address. - **Tenant**: Filter events scoped to a specific tenant. - **Level**: Filter to **Company**-level events only. See [Company-level Auditing](/audit-trails-and-integrations#company-level-auditing). - **Free Text**: Full-text search across all audit fields simultaneously. ![audit filtering with multiple fields example](/assets/audit-filter-multiple-example.webp) ## Filtering via the Backend SDK Use the Management SDK when you need programmatic search: custom security dashboards, compliance exports, or alert pipelines. See [Audit with Management SDKs](/audit-trails-and-integrations/sdks) for install steps, the full filter parameter reference, pagination (`SearchAll` in Go), and code samples in every supported language. Descope enforces a rate limit of 10 requests per minute for audit search operations. ## Filtering via the REST API You can also query audit events directly using the [Search Audit](/api/management/audit/search-audit) REST API endpoint. The request body accepts the same filter fields documented on the [SDK page](/audit-trails-and-integrations/sdks#search-filter-parameters). Here is an example request: ```bash curl --request POST \ --url __BaseURL__/v1/mgmt/audit/search \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "actions": ["LoginSucceed", "LoginFailed"], "userIds": ["U2abc123xyz"] }' ``` ## Understanding Event Types and Actions Every audit event recorded by Descope has both a **type** and an **action**. These two fields have a one-to-one relationship: each action maps to exactly one type. For example, the `LoginSucceed` action always corresponds to the `Information` type, while `LoginFailed` maps to `Warning`. Because of this direct mapping, filtering by `action` in the SDK or API is functionally equivalent to selecting event types in the Console filter dropdown. There is no need to convert between the two. Use whichever is more convenient for your workflow. For the full list of event types, actions, and their descriptions, see the [Audit Events](/audit-trails-and-integrations/audit-events) reference. # Overview (/audit-trails-and-integrations) Learn how to search and review project-level and company-level audit events in the Descope audit trail. # Audit The Descope Audit Trail logs security-relevant events across your Descope projects and company. Use it for monitoring, troubleshooting, and compliance. ## How to Work With the Audit Trail | Task | Guide | | --- | --- | | Browse and filter in the console | [Filtering Audit Events](/audit-trails-and-integrations/filtering-audit-events) | | Search or create events from your backend | [Audit with Management SDKs](/audit-trails-and-integrations/sdks) | | See what gets logged | [Audit Events](/audit-trails-and-integrations/audit-events) | | Stream to a third-party service | [Audit Trail Streaming](/audit-trails-and-integrations/audit-trail-streaming) | | Redact sensitive fields | [Masking PII in Audit Logs](/audit-trails-and-integrations/masking-pii-in-audit-logs) | You can also open the [Descope Console](https://app.descope.com/audits) directly or call the [Search Audit](/api/management/audit/search-audit) REST API. For **SCIM provisioning**, Descope emits detailed audit rows (`SCIMEvent`) with request bodies, results, and field-level changes. See [SCIM Audit Events](/audit-trails-and-integrations/audit-events/scim-audit-events). ## Company-level Auditing In addition to project-level events, Descope logs company-level events for actions taken across your Descope company: creating or deleting projects, managing management keys, and changing company settings. These events appear in the same audit table as project events so you have one place to review administrative activity. To view only company-level events, open the [Audit page](https://app.descope.com/audits) in any project within your company and set the **Level** filter to **Company**. The same filter is available in the [Search Audit](/api/management/audit/search-audit) API and [Management SDKs](/audit-trails-and-integrations/sdks). ![Company Level Audit Search](/assets/company_level_audit_search.webp) For the full list of company-level events, see [Company-level Audit Events](/audit-trails-and-integrations/audit-events#company-level-audit-events). For where company-level configuration lives, see [Company Settings](/management/company-settings). ## Audit Details by Type Some audit actions carry a structured **Data** payload with details about what changed. The sections below describe the fields logged for each of these action types. ### User Update Audit Detail Descope logs the new values on `UserModified` actions so you can see what changed on a user. Below is an example payload from the audit trail: ```json JSON { "Change": { "added_multi_tenant_roles": [ "xx" ], "added_roles": [ "xx" ], "custom_attribute_emailConsent": true, "custom_attribute_myAttribute": true, "display_name": "Test Me", "family_name": "Test", "given_name": "Me", "middle_name": "Middle", "phone": "12223334455" }, "correlation_id": "xx", "request_details": { "contentLength": "956", "headers": { "descope": { "cf-bot-score": "99", "cf-connecting-ip": "xx", "cf-ja3-hash": "xx", "cf-ray": "xx-DFW", "cf-verified-bot": "false", "x-request-id": "xx" }, "http": { "origin": "https://app.descope.com", "referer": "https://app.descope.com/", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" } }, "host": "console.descope.com", "method": "POST", "uri": "/console/v1/users/xx", "url": "/console/v1/users/xx" } } ``` ### Role Association Audit Detail Descope logs the `UsersRolesAssociationsModified` action whenever a user's role or tenant-role associations change, so you can see which users and roles were affected — and how — directly from the audit trail. It is generated when: - Roles are assigned to users (`add`) - Roles are removed from users (`remove`) - Tenants and roles are assigned or removed together (`add` / `remove`) - Roles are updated from SAML group mappings (`set`) - A user's roles are bulk-set (`set`) - Role or permission associations are recalculated after a group-mapping change (`recalculate`) The **Data** section contains: | Key | Description | |-----|-------------| | `operation` | How associations changed: `add`, `remove`, `set`, or `recalculate`. | | `users_ids` | IDs of the affected users. | | `users_login_ids` | Login / external identifiers of the affected users, when available. | | `roles_ids` | IDs of the roles added, removed, or set. Omitted for `recalculate` when no explicit roles are supplied. | When a single user is affected, the event's **User ID** and **Login IDs** columns are also populated. Tenant-scoped changes list the affected tenants in the **Tenants** field. Because these events now carry `users_login_ids`, you can filter them in the [Search Audit](/api/management/audit/search-audit) API by `externalIds` (or by `tenants`). ### Session Migration Audit Detail Descope logs an audit event when an external session token is exchanged for a Descope session during [session migration](/migrate/session-migration), on both success and failure: - `ExternalSessionMigrationSuccess` - the exchange succeeded and a Descope session was issued. - `ExternalSessionMigrationFailure` - the exchange failed. Logged as a **Warning**. The failure event's **Data** section contains: | Key | Description | |-----|-------------| | `error_message` | Why the exchange failed. | | `externalAuthProvider` | The external provider the token came from. | | `externalUserSource` | The user attribute Descope matched on, such as `email`. | | `externalUserId` | The user's ID at the external provider. | | `externalUserSourceValue` | The value of the matched attribute. | Failures include cases where no external authentication provider is configured on the project, no Descope user matched the identifier in the token, or Descope could not generate or validate the new session. Session migration does not create users just in time, so the user must already exist in Descope. # Masking PII in Audit Logs (/audit-trails-and-integrations/masking-pii-in-audit-logs) Control which personally identifiable information gets masked before audit and troubleshoot logs are forwarded to external connectors. # Masking PII in Audit Logs When forwarding audit or troubleshoot logs to an external connector (such as AWS S3, Cribl, Splunk, or Datadog), you can enable **Mask PII Data** to automatically obscure personally identifiable information before it leaves Descope. This lets you send logs to third-party platforms without exposing raw user data. Configure this setting per connector when setting up or editing an audit connector in the [Descope Console](https://app.descope.com/connectors). Enable the **Mask PII Data** toggle in the connector configuration panel. ## What Gets Masked Masking applies to two parts of each log record: - **externalIDs** — the login identifiers associated with a user (emails, phone numbers) - **data** — custom key-value fields whose field name matches a known PII pattern Descope identifies PII fields by name (case-insensitive): | If the field name contains… | Masking applied | |---|---| | `email`, `e-mail`, `mail`, `sentTo` | Email masker | | `phone`, `mobile`, `telephone`, `tel` | Phone masker | | `name`, `first_name`, `last_name`, `full_name`, `given_name`, `family_name`, `display_name`, `middle_name` | Name masker | | `external_id`, `externalid` | Email or phone masker, depending on value format | Field names are matched using **substring search** (case-insensitive). This means short patterns like `tel`, `mail`, or `name` will also match unintended fields (e.g., `tel` matches `hotel`, `telemetry`, or `intel`). When adding custom fields to the `data` object, avoid using these substrings in field names unless you want them masked. ## What Does NOT Get Masked The following fields remain in plain text regardless of the setting: | Field | Examples | |---|---| | Internal Descope IDs | User IDs, Project IDs, Tenant IDs, Actor IDs | | Timestamps | `occurred` | | Action types | `LoginSucceed`, `UserCreated` | | Authentication method | OTP, OAuth, Magic Link, etc. | | IP address & geo data | `remoteAddress`, `geo` | | Device information | Browser, OS, device type | | Flow & execution metadata | Flow IDs, Execution IDs, Request IDs | | Any `data` field whose name does not match a PII pattern | Custom app metadata, feature flags, etc. | ## Example Here's an example audit record for a `LoginSucceed` event: **Without masking** ```json { "type": "LoginSucceed", "userID": "U2ESG1VEKbTdXnKk1AX3", "externalIDs": ["jane.doe@company.com", "+12025551234"], "occurred": "2024-09-10T14:22:05Z", "method": "otp", "remoteAddress": "203.0.113.42", "data": { "full_name": "Jane Doe", "plan": "enterprise" } } ``` **With masking enabled** ```json { "type": "LoginSucceed", "userID": "U2ESG1VEKbTdXnKk1AX3", "externalIDs": ["jan*****@com****.com", "+1*********1234"], "occurred": "2024-09-10T14:22:05Z", "method": "otp", "remoteAddress": "203.0.113.42", "data": { "full_name": "J**e D**", "plan": "enterprise" } } ``` ## When To Mask PII Data **Enable Mask PII Data if:** - You must meet compliance requirements such as GDPR or HIPAA when sharing logs with third parties - You are sending logs to an external vendor and want to minimize PII exposure - Your analytics rely on action types, user IDs, or timestamps rather than raw email or phone values **Leave it disabled if:** - You need exact email or phone matching for fraud detection or alerting workflows - You correlate log events by login identifier across multiple systems - You need to identify and contact users directly based on audit log data # With SDKs (/audit-trails-and-integrations/sdks) Search and create Descope audit events programmatically using backend SDKs. # Audit with Management SDKs You can search the Descope audit trail and emit custom audit events from your backend using the Management SDK or the [Search Audit](/api/management/audit/search-audit) and [Create Audit Event](/api/management/audit/create-audit-event) API endpoints. For filtering in the [Descope Console](https://app.descope.com/audits), see [Filtering Audit Events](/audit-trails-and-integrations/filtering-audit-events). Descope enforces a rate limit of 10 requests per minute for audit search operations. ## Search Audits ```javascript // Args: // searchOptions: (AuditSearchOptions): A completed descope structure with the desired audit search options. const searchOptions = { userIDs: ["xxxxxx"], actions: ["LoginSucceed"], excludedActions: null, // List of actions to exclude // from: time.Tim, // Retrieve records newer than given time. Limited to no older than 30 days. // to: time.Time, // Retrieve records older than given time. devices: null, // List of devices to filter by. Current devices supported are "Bot"/"Mobile"/"Desktop"/"Tablet"/"Unknown" methods: null, // List of methods to filter by. Current auth methods are "otp"/"totp"/"magiclink"/"oauth"/"saml"/"password" geos: null, // List of geos to filter by. Geo is currently country code like "US", "IL", etc. remoteAddresses: null, // List of remote addresses to filter by loginIDs: null, // List of login IDs to filter by tenants: null, // List of tenants to filter by noTenants: true, // Should audits without any tenants always be included // text: "John" // Free text search across all fields } const resp = await descopeClient.management.audit.search(searchOptions) if (!resp.ok) { console.log("Failed to search audits.") } else { console.log("Successfully searched audits.") console.log(resp) } ``` ```python # Args: # user_ids (List[str]): Optional list of user IDs to filter by user_ids = ["xxxxxx"] # actions (List[str]): Optional list of actions to filter by actions = ["LoginSucceed"] # excluded_actions (List[str]): Optional list of actions to exclude excluded_actions = None # devices (List[str]): Optional list of devices to filter by. Current devices supported are "Bot"/"Mobile"/"Desktop"/"Tablet"/"Unknown" devices = None # methods (List[str]): Optional list of methods to filter by. Current auth methods are "otp"/"totp"/"magiclink"/"oauth"/"saml"/"password" methods = None # geos (List[str]): Optional list of geos to filter by. Geo is currently country code like "US", "IL", etc. geos = None # remote_addresses (List[str]): Optional list of remote addresses to filter by remote_addresses = None # login_ids (List[str]): Optional list of login IDs to filter by login_ids = None # tenants (List[str]): Optional list of tenants to filter by tenants = None # no_tenants (bool): Should audits without any tenants always be included no_tenants = True # text (str): Free text search across all fields text = None # from_ts (datetime): Retrieve records newer than given time but not older than 30 days from_ts = None # to_ts (datetime): Retrieve records older than given time to_ts = None try: resp = descope_client.mgmt.audit.search(user_ids=user_ids, actions=actions, excluded_actions=excluded_actions, devices=devices, methods=methods, geos=geos, remote_addresses=remote_addresses, login_ids=login_ids, tenants=tenants, text=text, from_ts=from_ts, to_ts=to_ts) print ("Successfully searched audits") print (resp) except AuthException as error: print ("Failed to search audits") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // searchOptions: (AuditSearchOptions): A completed descope structure with the desired audit search options. searchOptions := &descope.AuditSearchOptions{} searchOptions.UserIDs = []string{"xxxxxx"} searchOptions.Actions = []string{"LoginSucceed"} searchOptions.ExcludedActions = nil // List of actions to exclude // searchOptions.From = time.Time // Retrieve records newer than given time. Limited to no older than 30 days. // searchOptions.To = time.Time // Retrieve records older than given time. searchOptions.Devices = nil // List of devices to filter by. Current devices supported are "Bot"/"Mobile"/"Desktop"/"Tablet"/"Unknown" searchOptions.Methods = nil // List of methods to filter by. Current auth methods are "otp"/"totp"/"magiclink"/"oauth"/"saml"/"password" searchOptions.Geos = nil // List of geos to filter by. Geo is currently country code like "US", "IL", etc. searchOptions.RemoteAddresses = nil // List of remote addresses to filter by searchOptions.LoginIDs = nil // List of login IDs to filter by searchOptions.Tenants = nil // List of tenants to filter by searchOptions.NoTenants = true // Should audits without any tenants always be included // searchOptions.Text = "John" // Free text search across all fields // Pagination: use Limit (page size) and Page (zero-based page index) to page // through large result sets instead of receiving a single capped response. searchOptions.Limit = 100 // Number of records to return per page searchOptions.Page = 0 // Zero-based page index to retrieve // SearchAll returns the matching records for the requested page along with the total // number of records that match the search, which you can use to drive pagination. res, total, err := descopeClient.Management.Audit().SearchAll(ctx, searchOptions) if err != nil { fmt.Println("Unable to search audits: ", err) } else { fmt.Printf("Successfully searched audits (showing %d of %d total): \n", len(res), total) for _, auditEvent := range res { fmt.Println(auditEvent) } } return err ``` Use `SearchAll`, which returns the matching records and the total result count so you can paginate. The previous `Search` function is deprecated and internally calls `SearchAll`, discarding the total count. ```java AuditService as = descopeClient.getManagementServices().getAuditService(); // Full text search on the last 10 days try { AuditSearchResponse resp = as.search(AuditSearchRequest.builder() .from(Instant.now().minus(Duration.ofDays(10)))); } catch (DescopeException de) { // Handle the error } // Search successful logins in the last 30 days try { AuditSearchResponse resp = as.search(AuditSearchRequest.builder() .from(Instant.now().minus(Duration.ofDays(30))) .actions(Arrays.asList("LoginSucceed"))); } catch (DescopeException de) { // Handle the error } ``` ```php // Any of the following arguments can be used as a search term for the function, // not all of them need to be included in a search $response = $descopeSDK->management->audit->search( "userIds", // List of user IDs to filter by. "actions", // List of actions to filter by. "excludedActions", // List of actions to exclude. "devices", // List of devices to filter by (e.g., "Bot", "Mobile", "Desktop"). "methods", // List of methods to filter by (e.g., "otp", "totp", "magiclink"). "geos", // List of geographical locations to filter by (country codes). "remoteAddresses", // List of remote addresses to filter by. "loginIds", // List of login IDs to filter by. "tenants", // List of tenants to filter by. "noTenants", // Whether to include audits without tenants. "text", // Free text search across all fields. "fromTs", // Retrieve records newer than this timestamp. "toTs" // Retrieve records older than this timestamp. ); print_r($response); ``` ```csharp // Args: // searchRequest (SearchAuditRequest): Audit search filters. var searchRequest = new SearchAuditRequest { UserIds = new List { "xxxxxx" }, Actions = new List { "LoginSucceed" }, ExcludedActions = null, // List of actions to exclude // From = "...", // ISO-8601 string; records newer than this (max 30 days) // To = "...", // ISO-8601 string; records older than this Devices = null, // "Bot"/"Mobile"/"Desktop"/"Tablet"/"Unknown" Methods = null, // "otp"/"totp"/"magiclink"/"oauth"/"saml"/"password" Geos = null, RemoteAddresses = null, LoginIdsContain = null, // note: LoginIdsContain, not LoginIds Tenants = null, NoTenants = true, // Text = "John", Size = 100, // page size Page = 0, // zero-based page index }; try { var resp = await descopeClient.Mgmt.V1.Audit.Search.PostAsync(searchRequest); // resp.Audits, resp.Total } catch (DescopeException ex) { // Handle the error } ``` ### Search filter parameters All parameters are optional. Combine them to build precise queries. | Parameter | Type | Description | |---|---|---| | `actions` | `string[]` | Filter to specific event actions (maps 1:1 to event types). See [Audit Events](/audit-trails-and-integrations/audit-events) for valid values. | | `excludedActions` | `string[]` | Exclude specific event actions from results. | | `userIDs` | `string[]` | Filter by Descope User ID(s). | | `loginIDs` | `string[]` | Filter by Login ID(s) (e.g., email address, phone number). | | `from` | `timestamp` | Return events newer than this time. Cannot be older than 30 days. | | `to` | `timestamp` | Return events older than this time. | | `devices` | `string[]` | Filter by device type: `"Bot"`, `"Mobile"`, `"Desktop"`, `"Tablet"`, `"Unknown"`. | | `methods` | `string[]` | Filter by authentication method: `"otp"`, `"totp"`, `"magiclink"`, `"oauth"`, `"saml"`, `"password"`. | | `geos` | `string[]` | Filter by country code, e.g. `"US"`, `"IL"`. | | `remoteAddresses` | `string[]` | Filter by originating IP address(es). | | `tenants` | `string[]` | Filter by tenant ID(s). | | `noTenants` | `boolean` | When `true`, always includes events with no associated tenant alongside tenant-scoped results. | | `text` | `string` | Free-text search across all audit fields. | ### Example: failed logins for a user (last 7 days) ```javascript const searchOptions = { userIDs: ["U2abc123xyz"], actions: ["LoginFailed"], from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), } const resp = await descopeClient.management.audit.search(searchOptions) ``` ```python from datetime import datetime, timedelta try: resp = descope_client.mgmt.audit.search( user_ids=["U2abc123xyz"], actions=["LoginFailed"], from_ts=datetime.utcnow() - timedelta(days=7), ) print(resp) except AuthException as error: print("Error: " + str(error.error_message)) ``` ```go ctx := context.Background() from := time.Now().AddDate(0, 0, -7) searchOptions := &descope.AuditSearchOptions{ UserIDs: []string{"U2abc123xyz"}, Actions: []string{"LoginFailed"}, From: from, } res, total, err := descopeClient.Management.Audit().SearchAll(ctx, searchOptions) ``` ```java AuditService as = descopeClient.getManagementServices().getAuditService(); try { AuditSearchResponse resp = as.search(AuditSearchRequest.builder() .userIds(Arrays.asList("U2abc123xyz")) .actions(Arrays.asList("LoginFailed")) .from(Instant.now().minus(Duration.ofDays(7)))); } catch (DescopeException de) { // Handle the error } ``` ```php $response = $descopeSDK->management->audit->search( userIds: ["U2abc123xyz"], actions: ["LoginFailed"], fromTs: (new DateTime())->modify('-7 days'), ); print_r($response); ``` ```csharp var searchRequest = new SearchAuditRequest { UserIds = new List { "U2abc123xyz" }, Actions = new List { "LoginFailed" }, From = DateTimeOffset.UtcNow.AddDays(-7).ToString("o"), }; var resp = await descopeClient.Mgmt.V1.Audit.Search.PostAsync(searchRequest); ``` ## Create Audit Event Beyond the events Descope logs automatically, you can emit custom audit rows from your backend. See [Creating Custom Audit Events](/audit-trails-and-integrations/audit-events#creating-custom-audit-events) on the Audit Events reference. ```javascript // Args: // auditOptions: (AuditCreateOptions): A completed descope structure with the desired audit creation options. const auditOptions = { userId: "xxxxxx", // Optional audit user ID action: "LoginSucceed", // The action that was performed. type: "info", // Choose from three severity levels: info, warn, or error actorId: "xxxxxx", // The user that performed the action tenantId: "xxxxxx", // The tenant that the action was performed in data: { // Optional additional data to include in the audit event key1: "value1", key2: "value2" } } await descopeClient.management.audit.createEvent(auditOptions) ``` ```python # Args: # user_id (str): Optional audit user ID user_id = "xxxxxx" # action (str): Audit action that was performed action = "LoginSucceed" # audit_type (str): Choose from three severity levels: info, warn, or error audit_type = "info" # actor_id (str): The user that performed the action actor_id = "xxxxxx" # tenant_id (str): The tenant that the action was performed in tenant_id = "xxxxxx" # data (dict): Optional additional data to include in the audit event data = { "key1": "value1", "key2": "value2" } try: resp = descope_client.mgmt.audit.create_event(user_id=user_id, action=action, type=audit_type, actor_id=actor_id, tenant_id=tenant_id, data=data) print ("Successfully created audit event") print (resp) except AuthException as error: print ("Failed to create audit event") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // createOptions: (AuditCreateOptions): A completed descope structure with the desired audit create options. createOptions := &descope.AuditCreateOptions{} // createOptions.UserID: Optional audit user ID createOptions.UserID = "xxxxxx" // createOptions.Action: Audit action that was performed createOptions.Action = "LoginSucceed" // createOptions.Type: Choose from three severity levels: info, warn, or error createOptions.Type = "info" // createOptions.ActorID: The user that performed the action createOptions.ActorID = "xxxxxx" // createOptions.TenantID: The tenant that the action was performed in createOptions.TenantID = "xxxxxx" // createOptions.Data: Optional additional data to include in the audit event createOptions.Data = map[string]interface{}{ "key1": "value1", "key2": "value2", } // CreateEvent returns nil if the event was successfully created, or an error if it failed. err := descopeClient.Management.Audit().CreateEvent(ctx, createOptions) if err != nil { fmt.Println("Unable to create audit event: ", err) } else { fmt.Printf("Successfully created audit event") } return err ``` ```java AuditService as = descopeClient.getManagementServices().getAuditService(); try { as.createEvent(AuditCreateRequest.builder() .userId("some-id") // Audit user ID .action("some-action-name") // The action that was performed .type(AuditType.INFO) // Choose from three severity levels: info, warn, or error .actorId("some-actor-id") // The user that performed the action .tenantId("some-tenant-id") // The tenant that the action was performed in .data(Map.of("key1", "value1", "key2", "value2")) // Optional additional data to include in the audit event .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby client.audit_create_event( user_id: "UXXX", # optional, the ID of the user associated with the event actor_id: "UXXX", # required, the actor that performed the action tenant_id: "tenant-id", # required, the tenant that the action was performed in action: "pencil.created", # required, the action that was performed type: "info", # either: info/warn/error # required data: { # optional, additional data to include in the audit event pencil_id: "PXXX", pencil_name: "Pencil Name" } ) ``` ```php $response = $descopeSDK->management->audit->createEvent( "action", // The action that was performed "type", // Choose from three severity levels: info, warn, or error "actorId", // The actor that performed the action "tenantId", // The tenant that the action was performed in "userId", // Optional, the ID of the user associated with the event ["key1" => "value1", "key2" => "value2"] // Optional, additional data to include in the audit event ); print_r($response); ``` ```csharp // Args: // auditRequest (CreateAuditRequest): Custom audit event details. var auditRequest = new CreateAuditRequest { UserId = "xxxxxx", Action = "LoginSucceed", Type = "info", // "info", "warn", or "error" ActorId = "xxxxxx", TenantId = "xxxxxx", Data = new CreateAuditRequest_data { AdditionalData = new Dictionary { { "key1", "value1" }, { "key2", "value2" }, } } }; try { await descopeClient.Mgmt.V1.Audit.Event.PostAsync(auditRequest); } catch (DescopeException ex) { // Handle the error } ``` # Customizing Flow Errors (/handling-flow-errors/customizing-flow-errors) This guide covers how to handle errors within Descope flows. # Handling Flow Errors This guide will cover displaying and controlling behavior around flow errors. Within Descope screens, if an external error is provided during an action, you can transform the error to be a more consumable error for your end user. Outside of screens, you can route based on errors. ## Customizing Flow Errors in Flow Builder Within the Descope flow builder, you can customize errors in various places, such as Descope actions and conditionals. Some screen components can also have customized errors; you can review details for these components within the [Inputs Overview](/flows/screens/inputs). This section will cover handling errors within Descope actions and conditions. ### Actions Below is an example of an action with multiple errors that can be configured and customized. This example shows the `Sign In/Password` action, which has multiple errors that can modify their behavior. When customizing an error behavior within a flow action, the options are: #### Automatic Descope will automatically redirect the user back to the last screen they viewed if an error occurs. On that screen, the error message component will display the error. When using the **Automatic** option, you can customize the error message shown to the user. Any custom message you set will replace the default system message. For example, an user attempts to sign in with an incorrect password. With **Automatic** error handling, Descope redirects them back to the sign-in screen and displays a custom error message like "Invalid email or password. Please try again." This provides immediate feedback without exposing whether the email exists in the system. #### Mitigate Handle the error silently and continue as if the step succeeded. This is useful when you don't want to expose the reason for the error but still want the user to proceed, for example, showing a generic message like: "If this account exists, you'll receive an email." For example, during a password reset flow, if the email doesn't exist in your system, you can use **Mitigate** to show a generic success message like "If an account with this email exists, you'll receive a password reset link." This prevents user enumeration while still providing a positive user experience, as the flow continues to the next step as if the operation succeeded. #### Continue Pass the error from the current step to another step in the flow, where it can be handled explicitly with custom logic. For example, in a sign-up flow, if email verification fails, you can use **Continue** to pass the error to the next step. A conditional step can then check the error and decide what to do next — for example, show a screen asking the user to enter the correct email address, or redirect them to sign in if the account already exists. #### Ignore Continue to the next step in the flow (using success output) regardless of the step's outcome. For example, in a multi-factor authentication flow, you might want to log failed attempts to an audit system, but still allow the user to proceed to the next authentication step. Using **Ignore**, the flow treats the audit logging step as successful even if it fails, ensuring the authentication process isn't interrupted by non-critical logging errors. ![Descope error handling configuration within a Descope flow action](/assets/flow-error-handling-config.webp) Below is an example of the `Sign In/Password` action with two custom error handling configurations selected. Within the flow, you can see that the different errors are handled differently as the flow proceeds. You can use additional actions, screens, or conditions to configure custom actions. An enhanced example of this behavior would be using custom error codes from an HTTP connector in a later condition; you can read about this use case [here](/connectors/connector-configuration-guides/network/generic-http). ![Descope error handling of Descope flow action when connected to other tasks within the flow](/assets/custom-error-password-flow.webp) To learn how to use these error-handling options to hide sensitive information and prevent attackers from enumerating user accounts, see our [preventing user enumeration guide](/security-best-practices/preventing-user-enumeration). ### Conditions Below is an example of a Descope flow condition configured with multiple conditions and different error handling configurations. Flow conditions have a toggle for each condition to `Treat as an error`; when toggled, the condition is marked as an error, and the error behavior can be configured. When customizing an error behavior within a flow condition, the options are: - Automatic: Descope will automatically redirect the error to the error message component in the last screen viewed by the end user. - When Automatic is selected, you can customize the error message that appears when the action returns the error to the previous screen. Custom error messages will override the system error messages. - Continue: This still allows you to configure the error message and handle the following screen displayed to the user or action ran within the flow. ![Descope error handling configuration within a Descope condition within a Descope flow - part 1](/assets/custom-error-flow-condition1.webp) ![Descope error handling configuration within a Descope condition within a Descope flow - part 2](/assets/custom-error-flow-condition2.webp) Once you have configured the error handling within your condition, you can configure the next steps for the condition to take within your flow based on the error handling. In this example, a `403` or `404` will return to the `Sign In` screen and display the configured error, while the else condition will continue and get further information from the user. ![Descope error handling configuration example of using a dynamic value of an HTTP connector error](/assets/custom-http-error-flow.webp) #### Show Error with Custom Handling In some cases, you may want to perform an action when an error occurs (e.g., generating an audit event), but still return the user to the previous screen with a custom error message. While custom error handling allows you to perform an action, it does not support directly returning a message to the previous screen like automatic handling does. To work around this, you can add a condition after your action with a static expression such as `true = true`. You can then treat the condition as an error with automatic handling. This allows you to display a custom error message while still running an action before returning to the previous screen. Steps: 1. Handle the error using custom handling in your first action or condition. 2. Add the action you want to perform. 3. Add a condition block after the action with a static expression that is always true, like `true = true`. 4. Enable **Treat as an error** for the condition. 5. Select automatic handling and enter your custom error message. Here is an example with the `Sign In/Password` action. After the user tries to sign in, if there is an error with their password, this flow triggers custom error handling. First an audit event will be created, and then the `Trigger Error Message` condition will return a custom error message to the Sign In screen. ![Descope flow configuration showing custom error handling path with an audit event and a condition block](/assets/flow-error-handling-workaround.webp) ![Descope always true condition to trigger an error message after custom error handling](/assets/flow-error-handling-true-condition.webp) ### Dynamic Values in Flow Errors Descope's error handling also allows you to utilize dynamic values within customized errors for actions, conditions, etc. Using this functionality would be helpful when you want to display data back to the user during the returned error. Here is an example of using a dynamic value of the returned response code from an HTTP connector. ![Descope error handling configuration example of using a dynamic value of an HTTP connector error](/assets/custom-error-dynamic-value.webp) ![Descope error displayed to user when using a dynamic value of an HTTP connector error](/assets/custom-error-dynamic-value-message.webp) Here is an additional example of using a dynamic value of the provided user data within a `Sign In/Password` action. ![Example of configuring a Descope action to utilize a dynamic value within the error returned to the user](/assets/password-error-dynamic-value.webp) ![Example of error displayed to a user when using a dynamic value within an Descope flow action](/assets/password-dynamic-value-example.webp) ## Customizing Flow Errors via Client SDKs When you want to override or translate Desope's system or customized flow errors, you will utilize Descope's `errorTransformer` within the front-end client SDK. This section outlines how to use the `errorTransformer` within your front-end client. ### Customizing Flow Errors Below are examples of how to transform errors during flow execution. If you want to override your customized flow error that is configured within an action, condition, etc., you will create an item in the map like this: `"Error Message configured within flow builder":"Overridden Error Message within front-end client"` The [localization guide](/management/localization#localizing-errors) provides a more detailed example of this. ```javascript import { Descope } from '@descope/react-sdk' const App = () => { return ( {...} console.log('Logged in!')} onError={(e) => console.log('Could not logged in')} const errorTransformer = useCallback( (error: { text: string; type: string }) => { const translationMap = { SAMLStartFailed: 'Failed to start SAML flow' }; return translationMap[error.type] || error.text; }, [] ); errorTransformer={errorTransformer} /> ) } ``` ```javascript export function translateError(error) { const translationMap = { SAMLStartFailed: 'Failed to start SAML flow', }; return translationMap[error.type] || error.text; } ``` ```javascript function translateError(error) { const translationMap = { SAMLStartFailed: 'Failed to start SAML flow', }; return translationMap[error.type] || error.text; } const descopeWcEle = document.getElementsByTagName('descope-wc')[0]; descopeWcEle.errorTransformer = translateError; ``` ```javascript ``` ```html ``` #### Localizing Flow Errors You can also localize flow errors for different languages. For an example of localizing flow errors in multiple languages, see the [Localization](/management/localization#localizing-errors) guide. ## Customizable Errors Below outlines the Descope system error types you can transform when running Descope flows. | Category | Errors | | --------- | ------ | | OTP |
  • OTPSignUpOrInEmailFailed
  • OTPSignUpOrInPhoneFailed
  • OTPVerifyCodeEmailFailed
  • OTPVerifyCodePhoneFailed
  • OTPUpdateUserEmailFailed
  • OTPUpdateUserPhoneFailed
  • OTPUnauthorizedRequest
  • OTPSignInEmbeddedFailed
  • OTPSignUpOrInEmbeddedFailed
  • OTPSignUpEmbeddedFailed
  • OTPVerifyEmbeddedFailed
| | TOTP |
  • TOTPSignUpFailed
  • TOTPVerifyCodeFailed
  • TOTPUpdateUserFailed
  • TOTPUnauthorizedRequest
| | Magic Link |
  • MagicLinkSignUpOrInFailed
  • MagicLinkSignUpOrInFailed
  • MagicLinkMisconfiguration
  • MagicLinkUpdateUserEmailFailed
  • MagicLinkUpdateUserPhoneFailed
  • MagicLinkUnauthorizedRequest
  • MagicLinkVerifyFailed
| | Enchanted Link |
  • EnchantedLinkSignUpOrInFailed
  • EnchantedLinkMisconfiguration
  • EnchantedLinkUpdateUserEmailFailed
  • EnchantedLinkUnauthorizedRequest
  • EnchantedLinkVerifyFailed
| | Embedded Link |
  • EmbeddedLinkSignUpOrInFailed
  • EmbeddedLinkSignInFailed
  • EmbeddedLinkSignInFailed
  • ActionErrorEmbeddedLinkUpdateUser
  • ActionErrorEmbeddedLinkUnauthorizedRequest
| | Social (OAuth) |
  • OAuthStartFailed
  • OAuthMisconfiguration
  • OAuthExchangeCodeFailed
| | Biometrics (WebAuthn) |
  • WebauthnFailed
  • WebauthnSignUpStartFailed
  • WebauthnSignUpFinishFailed
  • WebauthnSignInStartFailed
  • WebauthnSignInFinishFailed
  • WebauthnSignUpOrInStartFailed
  • WebauthnSignUpOrInFinishFailed
  • WebauthnUpdateUserStartFailed
  • WebauthnUnauthorizedRequest
  • WebauthnUpdateUserFinishFailed
  • ActionErrorWebauthnFailed
| | SAML Login |
  • SAMLStartFailed
  • SAMLMisconfiguration
  • SAMLExchangeCodeFailed
| | Passwords |
  • PasswordSignUpFailed
  • PasswordSignInFailed
  • PasswordExpired
  • PasswordSendResetFailed
  • PasswordUpdateFailed
  • PasswordReplaceFailed
  • PasswordUpdateFailed
| | Tenants & SSO Config |
  • TenantCreation
  • SetSAMLConfigFailed
  • GetSAMLConfigFailed
  • SAMLConfigSelectTenant
  • ConfigProvideTenant
  • GetTenantConfigFailed
  • SetTenantConfigFailed
| | User Invites |
  • InviteUsersSelectTenant
  • InviteUsersPermissions
  • InviteUsers
| | Roles |
  • GetDefaultRoles
  • AssignRoles
  • ActionErrorAddRolesFailed
  • ActionErrorAddRolesFailedDoNotExists
| | Connectors |
  • ConnectorFailed
  • EmailConnectorFailed
  • SMSConnectorFailed
| | User Impersonation |
  • ImpersonationConsent
  • ActionErrorImpersonateFailed
  • ActionErrorImpersonateNoPermission
  • ActionErrorImpersonateConsent
  • ActionErrorMissingImpersonatingUser
| | General |
  • InvalidJWT
  • LoggedInFailed
  • ActionErrorLoadUserFromJWTFailed
  • ActionErrorLoadUserJwtVerify
  • ActionErrorLoadUserNoUser
  • ActionErrorParseCustomClaims
  • ActionErrorUpdateJWT
  • UpdateUserPropertiesFailed
| # How to Debug Flows (/handling-flow-errors/debug-flows) How to debug the Flows you've created. # Debugging Flows If you want to test and debug your flows to better understand the errors, there are a few ways you can do that with Descope. ## Using Hosted Flow Application The first and easiest way, is to use the hosted [Descope Flow application](/identity-federation/auth-hosting) that we offer, to see your flows in action: **Using Hosted Descope App:** If you're using `https://auth.descope.io/`, you can append a `&debug=true` flag to your URL like so: ``` https://auth.descope.io/__ProjectID__?flow=sign-up-or-in&debug=true ``` **Using Self-Hosted Descope App:** If you're hosting the application yourself by cloning the [repo](https://github.com/descope/auth-hosting), in either localhost or somewhere else, the same `&debug=true` flag will be used. The debugger will then appear in the right hand corner like this: ![Hosted Flow with Debugger turned on](/assets/debugger-turn-on.webp) ## Using the Flow Runner Within the Descope console, you can run your flows and see the output of the actions that occur during the flow execution. The flow runner output includes any errors for the flow execution as well. Using the flow runner within the console is very helpful in debugging your flow behavior in an easy-to-read manner. To run the flow within the console, navigate to [flows](https://app.descope.com/flows), select the flow you want to test, and click run towards the top right. Once you have started the flow, you'll see your debug messages populated on the screen, and any errors that occur will also be displayed there. ![Debugging flows with the console flow runner](/assets/debugger-flow-runner.webp) You can also write your own messages to the flow runner. Scriptlets support the standard `console` methods, so `console.log()`, `console.warn()`, `console.error()`, and `console.debug()` calls in your Scriptlet code appear in the runner messages as the flow executes. See [Logging and Debugging](/flows/actions/scriptlets#logging-and-debugging), for more details. ## Using the SDKs Under the [Getting Started Guide](/getting-started) for each of the SDK/Frameworks, you will see there is a debug option built into every Flow component. If you enable that in your application, you'll be able to debug problems in your flow, not relying on our flow hosting application. If you have any other questions about Descope or how to debug your Flows, feel reach to reach out to [us](/support)! # Troubleshooting Flows (/handling-flow-errors/troubleshooting-flows) Learn about Troubleshooting Flows within your project # Troubleshooting Flows As any developer knows, the more tools you have to debug your application, the better. Descope knows this and enables you to debug your flows in several ways. This article will cover the various tools Descope provides for troubleshooting your flows. ## Client SDK Debug Mode Under the [Getting Started Guide](/getting-started) for each of the SDK/Frameworks, you will see there is a debug option built into every Flow component. Below is an example of the debugging modal when enabled within the Descope client SDK. ![Example of debugging modal when enabled within the Descope client SDK](/assets/troubleshooting-flows-debugging-modal.webp) ### Flow Version Mismatch error In some use cases where the conditions are at the beginning of the flow (before any screen), it's more common for users to encounter a flow version mismatch error. This happens when the flow has been rendered for quite some time before interaction. When this occurs, a flag within the client-facing SDKs called `restartOnError` can be set to `true` (default: `false`), which will restart the flow if the components' version has not changed. This will avoid any flow out-of-sync errors in such cases, and enabling this flag can help maintain a smoother user experience. ## Flow Runner You can debug your flow by running it in the Descope flow runner, which writes error messages, console outputs, and connector requests and responses to the Runner messages. To run a flow within the Descope flow runner, navigate to [Flows](https://app.descope.com/flows) and choose your flow, then click the blue `Run` button at the top right. Below is an example of the data written to the runner messages. ![Example of how to use the Descope flow runner messages for flow debugging](/assets/troubleshooting-flows-runner-messages.webp) ## Troubleshooting Logs Descope also allows you to view flow logging within the [Troubleshooting Logs](https://app.descope.com/audits/tlogs) section of the Descope console; this is very helpful for troubleshooting issues reported by your end users as you can see the history of errors that occurred within your flow. These logs will include failures in your Descope flow, including any errors related to Descope actions such as connector actions, sign-in/up/up or in actions, etc. The errors displayed here will show you the error details, including the step name, similar to what you see in the flow runner. When it comes to connectors, the complete error response from the connector is logged to enable you to troubleshoot the behavior of your connector. Similarly to [audit streaming](/audit-trails-and-integrations/audit-trail-streaming), you can stream flow troubleshooting logs via the audit stream connectors by checking the box for `Stream Troubleshooting Events`. Below is an example of the details logged to the troubleshooting logs within Descope. ![An example of the details logged to the troubleshooting logs within Descope](/assets/troubleshooting-flows-troubleshooting-logs.webp) # Hash-Based Routing (/other-troubleshooting/angular-hash-routing) If you're using Angular with hash-based routing you may encounter issues with Magic Link and Oauth not working. This guide explains how to resolve this issue. # Angular Hash-Based Routing Issues When using magic links or OAuth in an Angular application with hash-based routing, you may encounter an issue where the token passed in the URL is not properly verified. As a result, users are redirected back to the login or sign-up page without any progress or error messages. This issue is caused by how Angular’s default hash-based routing handles query parameters. This guide explains how to resolve this issue. ## Custom Location Strategy Since the default hash-based routing disregards query parameters, we must implement a custom hash-based location strategy that includes the query parameters. We first create the custom strategy by extending and overriding the default hash-based strategy: ```javascript // ParameterHashLocationStrategy.ts import { Injectable } from '@angular/core'; import { HashLocationStrategy } from '@angular/common'; @Injectable() export class ParameterHashLocationStrategy extends HashLocationStrategy { override prepareExternalUrl(internal: string): string { return window.location.search + super.prepareExternalUrl(internal); } } ``` Now we need to tell Angular to use this location strategy in `app.module.ts`: ```javascript // app.module.ts import { LocationStrategy } from '@angular/common'; import { ParameterHashLocationStrategy } from './ParameterHashLocationStrategy'; @NgModule({ ... providers: [ ... { provide: LocationStrategy, useClass: ParameterHashLocationStrategy } ], ... }) ... ``` You should now be able to use Magic Link, Oauth, and Enchanted Link for both Flows and SDKs while continuing to use a hash-based location strategy in your Angular application. # Email Sending Delay (/other-troubleshooting/email-sending-delay) Learn how to debug delays in emails sent to users # Troubleshooting Email Sending Delay This guide will help you identify and resolve common issues that can cause delays in emails sent through Descope. Whether you're a developer or an IT administrator, these steps will assist you in troubleshooting and resolving email delivery delays. ## Testing Options Before diving into detailed troubleshooting, ensure that your email configuration settings in Descope and your email server are correct. This includes verifying the sender email address, SMTP server settings, etc. ### Check Email Gateways Does your customer use an email gateway, such as Cisco IronPort Secure Email Gateway? Email gateways can sometimes introduce delays due to security checks, filtering, or rate limiting. Ensure the email gateway settings are optimized for timely email delivery. ### Gathering Logs Descope provides detailed logs that can be accessed through the [Audit tab](https://app.descope.com/audits). Look for the following logs to diagnose email sending issues: ### Analyzing the `.eml` File If you can obtain the `.eml` file of the delayed email, it would be very helpful for a detailed analysis. Here is a guide on how to export an email to a `.eml` file: [Export Email to File](https://tinyurl.com/descope-codetwo-export-email). Once you have the `.eml` file, you can use tools like [EML Analyzer](https://analyzer.sublime.security/) to examine the headers for any indications of delays. Here's an example of how to analyze the timeline of an email: #### Example Analysis 1. **Email Created**: 14:20:15 on AWS 2. **Received from AWS by example.com (Secure Email Gateway)**: 14:20:17 (+2 seconds) 3. **Received from example.com by destination.com (Webmail)**: 14:25:22 (+5 minutes) The email was flagged by the Secure Email Gateway as potential spam, likely causing it to be held for further scanning. The Secure Email Gateway headers are encrypted, so the exact cause of the delay needs to be confirmed with the customer's Secure Email Gateway details. #### Evidence from EML ```plaintext Received: from mx1.example.com ([203.0.113.45]) by webmail.destination.com (15.0.3 build 12) with ESMTP (SSL) id 202401151425221476 for ; Mon, 15 Jan 2024 14:25:22 +0000 Received: from a1-99.smtp-out.amazonses.com ([54.240.1.99]) by mx1.example.com with ESMTP/TLS/ECDHE-RSA-AES256-GCM-SHA384; 15 Jan 2024 14:20:17 +0000 Date: Mon, 15 Jan 2024 14:20:15 +0000 X-Secure-Email-Gateway-Filtered: true ``` #### Key Points to Check - **SPF**: Verify that the Sender Policy Framework (SPF) record is valid. - **DKIM**: Ensure DomainKeys Identified Mail (DKIM) is correctly set up. - **Bounce Reports**: Confirm there are no bounce reports indicating the email was rejected by any server. ## Common Issues and Fixes ### Rate Limiting Ensure that your sending rate does not exceed the limits set by your email service provider or gateway. ### DNS Configuration Verify that your DNS records (SPF, DKIM, DMARC) are correctly configured to avoid email rejection or delay by recipient servers. ### Blacklisting Check if your sending IP or domain is blacklisted. Use online tools to check for blacklisting and follow their guidelines to delist. ### Email Content Ensure that your email content is not triggering spam filters. Avoid using excessive links, images, or spam-like language. ## Conclusion By following these steps, you should be able to diagnose and resolve most issues related to email delivery delays when using Descope. For persistent issues, please contact Descope support for further assistance. # Mitigating High Spam Report Rates (/other-troubleshooting/mitigating-high-spam-reports) Reduce and prevent high email spam complaint rates # Mitigating High Spam Report Rates Email providers and mailbox providers (Gmail, Outlook, Yahoo, etc.) track how often recipients mark messages as spam. If your complaint rate climbs too high, your sending domain or IP reputation can suffer, causing your authentication emails (OTPs, magic links, invites, and notifications) to land in spam folders or get blocked entirely. This guide walks through how to monitor for spam complaints, identify what's causing them, and put controls in place to bring your rate back down. ## Why this matters A high spam complaint rate doesn't just affect the flagged messages — it can degrade deliverability for all mail sent from your domain or through your configured connector, including business-critical emails like password resets and login links. Mailbox providers and email service providers (such as AWS SES, SendGrid, or your SMTP host) may throttle, pause, or suspend sending if complaint rates exceed their thresholds. ## What you can do ### 1. Monitor bounces and complaints Before you can fix a high spam rate, you need visibility into it. - **Use your email provider's native monitoring.** If you've connected a custom [email connector](/connectors/connector-configuration-guides/messaging) (AWS SES, SendGrid, or generic SMTP), use that provider's built-in bounce, complaint, and reputation tooling — for example, [AWS SES suppression lists](https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.html) and [Virtual Deliverability Manager](https://docs.aws.amazon.com/ses/latest/dg/vdm.html), or SendGrid's suppression and activity feeds. These tools let you stop sending to addresses that have already bounced or complained, which is the single biggest lever for reducing your complaint rate. - **Review Descope Audit Logs.** The [Audit and Troubleshoot page](https://app.descope.com/audits) in the Descope Console shows a record of messages sent through your project, which can help you correlate spikes in complaints with specific flows, templates, or time windows. - **Export and inspect flagged messages.** If your provider supports it, pull the `.eml` file for a complained-about message and check its headers, content, and send path — the same approach used in [troubleshooting email sending delays](/other-troubleshooting/email-sending-delay). ### 2. Diagnose what's causing complaints Once you have data, look for patterns. Common root causes include: - **Unverified or scraped addresses.** If your sign-up or contact form collects email addresses without verification, it may be exposed to bots or bad actors submitting addresses that don't belong to them, causing complaints from people who never asked for the email. Descope's OTP and Magic Link settings include an **Allow Unverified Recipient Email Addresses** toggle (off by default) — enabling it increases fraud and spam risk, so only turn it on if you have other verification controls in place. See the [OTP settings](/auth-methods/otp/settings#allow-unverified-recipient-email-addresses-or-phone-numbers) and [Magic Link settings](/auth-methods/magic-link/settings#allow-unverified-recipient-email-addresses-or-phone-numbers) docs. - **Excessive message volume to the same recipient.** Sending too many OTPs, magic links, or retries to one address in a short window is a common trigger for spam reports. Descope lets you configure **Number of Retries and Attempts Timeframe** on both [OTP](/auth-methods/otp/settings#number-of-retries-and-attempts-timeframe-seconds) and Magic Link authentication methods, which caps how many messages a single recipient can receive in a given period. - **Abuse or bot traffic driving sends.** If bots or scripts are triggering sign-up, login, or password-reset flows at volume, this can flood addresses with unwanted email. Use the [Check Rate Limit flow action](/flows/actions/rate-limit-action) (available on Growth and Enterprise plans) to rate-limit by IP, ASN, or device fingerprint (JA4) before an email is sent, which helps prevent both brute-force abuse and email spam. - **Missing or broken sender authentication.** Emails sent without valid SPF, DKIM, and DMARC records are more likely to be flagged as spam or spoofed, and any spam that does slip through is more damaging to your domain's reputation. Verify these DNS records for the sending domain configured on your [email connector](/connectors/connector-configuration-guides/messaging). - **Sending from a domain users don't recognize.** If emails come from a generic or unfamiliar sender address, recipients are more likely to mark them as spam simply because they don't recognize the sender. Configure a custom **Sender Address** and **Sender Name** on your [SMTP](/connectors/connector-configuration-guides/messaging/smtp), AWS SES, or SendGrid connector, using a domain your users associate with your brand. - **Template content that reads as spammy.** Excessive links, images, or urgent/promotional language in the email body can trigger both spam filters and manual complaints. Review and simplify your [custom email templates](/auth-methods/otp/settings#templates). ### 3. Implement changes Based on your findings, make targeted changes, for example: - Add or tighten retry/attempt limits on OTP and Magic Link methods. - Add a Check Rate Limit action earlier in the flow to block automated abuse before an email is ever sent. - Turn off (or add compensating controls around) unverified recipient sending. - Fix SPF/DKIM/DMARC records for your sending domain. - Simplify email templates and ensure any unsubscribe or opt-out mechanism your own emails reference is present and functional. - Enable your email provider's suppression list so bounced or complained-about addresses are automatically excluded from future sends. Roll out one change at a time where possible so you can measure its effect on your complaint rate before moving to the next. ### 4. Confirm the fix After implementing changes, continue monitoring bounce and complaint data (via your connector provider's tools and Descope Audit Logs) to confirm the rate is trending down. If you're operating under a review or sending pause from your email service provider, be prepared to summarize: - What caused the high complaint rate. - What changes you made to your sending configuration or flows. - Why those changes prevent the issue from recurring. ## Keeping your complaint rate low going forward Bringing your rate down once isn't the end of the work — mailbox providers and ESPs continue to track your reputation over time. Keep retry limits, rate limiting, and sender authentication in place, periodically review Audit Logs for unusual sending spikes, and re-evaluate your templates and verification settings whenever you change sign-up or notification flows. If you continue to see elevated complaint rates after implementing these controls, contact Descope support for further assistance. # esModuleInterop Issue (/other-troubleshooting/nodejs-typeerror) If you're using our Node.js SDK, you may come across an issue when compiling relating to esModuleInterop. This guide explains how to resolve this issue. # Node.js TypeError Issue (esModuleInterop) When using the Descope Node.js SDK, you may encounter a `TypeError`, relating to a problem that occurs when our CommonJS module is being imported into an ES6 module codebase. This guide explains how resolve this issue. ## esModuleInterop Issue When encountering this TypeScript error message: `TypeError: node_sdk_1.default is not a function`, you can resolve this by this configuration to your `tsconfig` file: ```json { "compilerOptions": { "esModuleInterop": true, } } ``` *To learn more about `esModuleInterop`, you can read about it [here](https://www.typescriptlang.org/tsconfig#esModuleInterop).* If you have any other questions about Descope or our Node.js SDK, feel reach to reach out to [us](/support)! # Cookies with Safari (/other-troubleshooting/safari-cookies) Describe why sessionTokenViaCookie doesn't work in Safari # Troubleshooting Cookies (with Safari) Setting the session token via cookie does not work in Safari in HTTP. When you login in safari with an app that set ```sessionTokenViaCookie```, the session is not in stored in the cookies storage (as shown in the images below). ![sessionTokenViaCookie](/assets/browser-session-token-via-cookie.webp) ![browser cookies](/assets/safari-browser-cookies.webp) Above, the cookie is nowhere to be found. This is because Safari does not allow to you to set a JavaScript cookie that is ```Secure;``` from an HTTP website. So if you run the following in an HTTP served page in JavaScript: ``` document.cookie = "k1=v1;Secure;"; ``` It will behave like the following: - Most browsers will store cookies from both HTTP and HTTPS. - on Safari, this will store the cookie only when executed in HTTPS page context. To conclude, ```sessionTokenViaCookie``` will not work well in Safari if the website is running on HTTP because the way Safari implements cookies. ## Using the SDKs Under the [Getting Started Guide](/getting-started) for each of the SDK/Frameworks, you will see there is a debug option built into every Flow component. If you enable that in your application, you'll be able to debug problems in your flow, not relying on our flow hosting application. If you have any other questions about Descope or how to debug your Flows, feel reach to reach out to [us](/support)! # Self Signed Certificates (/other-troubleshooting/self-signed-certs) How to diagnose and resolve TLS errors caused by self-signed certificates when using Descope SDKs in development and corporate environments. # Self Signed Certificates When using the Descope SDKs behind corporate proxies, local development tunnels, or internal environments that inject **self-signed certificates**, TLS verification may fail. Typical symptoms include SDK calls failing with messages such as: - `self-signed certificate in certificate chain` - `x509: certificate signed by unknown authority` - `CERTIFICATE_VERIFY_FAILED` - `SSLHandshakeException` This guide explains why this happens and how to resolve it safely across supported SDKs. Disabling TLS verification (e.g., setting `NODE_TLS_REJECT_UNAUTHORIZED=0`, `verify=False`, or `InsecureSkipVerify`) exposes your application to man-in-the-middle attacks. **Do not use these workarounds in production.** Instead, configure your system to trust the certificate properly. ## Common Errors by SDK | SDK | Common Error Message | |-----|---------------------| | **Node.js** | `FetchError: request to https://api.descope.com/v2/keys/YOUR_DESCOPE_PROJECT_ID failed, reason: self-signed certificate in certificate chain` | | **Python** | `requests.exceptions.SSLError: HTTPSConnectionPool(host='api.descope.com', port=443): Max retries exceeded with url: /v2/keys/YOUR_DESCOPE_PROJECT_ID (Caused by SSLError(SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain')))` | | **Java** | `javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target` | | **Go** | `Get "https://api.descope.com/v2/keys/YOUR_DESCOPE_PROJECT_ID": x509: certificate signed by unknown authority` | | **.NET** | `System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception. ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid according to the validation procedure.` | ## Recommended Solution: Trust the Certificate The most secure and portable approach is to make your runtime trust the signing certificate: 1. Obtain the self-signed/root CA certificate used by your proxy or development server (usually a `.crt`/`.pem`). 2. Add it to the OS trust store or configure your language/runtime to trust it (see SDK-specific instructions below). 3. **Restart** your app so the new trust settings are picked up. This approach properly configures your application to trust your corporate CA or self-signed certificates while maintaining security. ```bash # Export your CA and point Node to it export NODE_EXTRA_CA_CERTS=/path/to/corp-root-ca.pem node app.js ``` Alternatively, install the CA into the OS trust store so Node picks it up automatically. ```bash # Trust the CA via environment export REQUESTS_CA_BUNDLE=/path/to/corp-root-ca.pem python app.py ``` Or pass a CA bundle to the client/session used by the Descope SDK: ```python import requests session = requests.Session() session.verify = "/path/to/corp-root-ca.pem" # Pass session to SDK if supported, or set globally where HTTP client is constructed ``` ```bash # 1. Import the CA into a Java truststore keytool -importcert -trustcacerts -file corp-root-ca.crt -alias corp-root -keystore truststore.jks # 2. Point the JVM to the truststore java -Djavax.net.ssl.trustStore=truststore.jks \ -Djavax.net.ssl.trustStorePassword=changeit \ -jar app.jar ``` ```go import ( "crypto/tls" "crypto/x509" "net/http" "os" ) func clientWithCA() *http.Client { caCertPool, _ := x509.SystemCertPool() caCert, _ := os.ReadFile("/path/to/corp-root-ca.pem") caCertPool.AppendCertsFromPEM(caCert) tr := &http.Transport{TLSClientConfig: &tls.Config{RootCAs: caCertPool}} return &http.Client{Transport: tr} } ``` Wire this client into the Descope SDK's HTTP layer if configurable. ```csharp var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => { // Validate using enterprise CA logic or chain.Build with extra roots return /* true only if cert chains to your trusted CA */; }; var http = new HttpClient(handler); // Provide HttpClient to the SDK if supported ``` Alternatively, install the CA to the OS store so default validation succeeds. ## Temporary Development Workarounds (Not for Production) If you must proceed quickly in local development, you can temporarily relax TLS checks. However, we strongly recommend using the secure approach above for production environments. These approaches disable SSL certificate validation and should **never** be used in production environments. They create security vulnerabilities by accepting any certificate. ```bash # Disables validation process-wide export NODE_TLS_REJECT_UNAUTHORIZED='0' # DEV ONLY node app.js ``` This affects **all** HTTPS requests in the process, not just Descope. ```python import requests requests.get("__BaseURL__", verify=False) # DEV ONLY ``` Prefer targeting only the specific dev call, not global settings. Java doesn't have a simple global override like other languages. Consider using the secure approach instead. ```java // Avoid global trust-all managers in production code // If absolutely necessary for a local repro, gate behind a dev flag and document the risk ``` ```go &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} // DEV ONLY ``` ```csharp handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; // DEV ONLY ``` # SSO Troubleshooting (/other-troubleshooting/sso-troubleshooting) Learn how to troubleshoot SSO configuration issues and help your customers resolve SSO-related problems. # SSO Troubleshooting This guide helps you troubleshoot SSO configuration issues that your customers might encounter when configuring SSO for their tenant. Follow these steps to identify and resolve common SSO problems. ## SSO Error Codes These errors can occur during an SSO login, when Descope federates to the tenant's identity provider (IdP). Codes marked **Internal** indicate a server-side issue; if you hit one repeatedly, contact support with the code and a timestamp. ### SSO Login and Federation | Code | Meaning | HTTP status | | ---- | ------- | ----------- | | `E061011` | Tenant requires SSO: the user tried a non-SSO method on a tenant that mandates SSO | 401 | | `E061013` | Internal variant of "tenant requires SSO" | Internal | | `E061016` | The redirect URL doesn't match the SSO application's approved redirect URLs | 403 | | `E061301` | SAML token exchange failed (generic exchange failure) | 401 | | `E062605` | Token exchange with the SSO provider (IdP) failed | 401 | | `E062601` | SAML AuthnRequest creation failed | 401 (or 500) | | `E062602` | SAML provider object creation failed | Internal | | `E062603` | SAML assertion handling failed | Internal | | `E062604` | Failed to parse the SAML private key in the configuration | 400 | | `E062606` | Failed to parse the SAML x509 certificate; verify the certificate | 400 | | `E062014` | JIT sign-up was attempted but JIT provisioning is disabled for SSO | 401 | | `E062028` | Groups is a mandatory attribute, but the IdP sent no group values during an SSO Setup Suite connection test (JIT provisioning must be enabled for this check to run) | 400 | | `E062016` | Failed to update ReBAC/FGA mappings from SSO groups | Internal | | `E113201` | Invalid mappable FGA SAML settings (Setup Suite / mappable schema helpers) | 400 | | `E062020` | The email from the IdP doesn't match the email that initiated login | 401 | | `E062021` | The email domain from the IdP doesn't match the configured SSO domain | 401 | | `E062023` | The user is not associated with the requested SSO application | 401 | | `E061502` | SSO invite: failed to load the tenant | 401 | | `E061503` | SSO invite: failed to send the SSO invite | 401 | | `E061206` | IdP-initiated login missing Post Authentication Redirect URL | 401 | Common fixes: - **`E061016`**: add the redirect URL to the SSO application's approved list in your [SSO settings](/auth-methods/sso/settings#post-authentication-redirect-url). - **`E061206`**: set a Post Authentication Redirect URL (project or tenant). See [IdP-initiated](/sso/idp-initiated#what-you-must-configure). - **`E062604` / `E062606`**: bad or stale SAML key/cert — refresh IdP metadata/cert or fix SP keys. See [Certificate and metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation) and [SAML Signing and Encryption Keys](/management/tenant-management/sso/saml-signing). - **`E062014`**: enable [JIT provisioning](/sso/jit-provisioning), or pre-provision the user (SCIM or invite). - **`E062020`**: expected behavior when `enforceInitiatedEmail` (SDKs) or the flow action's **Verify initiated email matches IdP response** toggle is enabled and the IdP authenticated a different account than the one the user started with. Confirm the user picked the right account at the IdP, or turn off the check if it's not needed. See [Backend SDK → Verify the IdP email matches](/auth-methods/sso/with-sdks/backend#verify-the-idp-email-matches-the-initiated-email). - **`E062021` / `E062023`**: check the email and domain the IdP returns against the tenant's SSO domain and the user's app assignment. See [SAML Security → Assertion validation](/security-best-practices/saml-security#assertion-validation). - **`E062016` / `SchemaDoesNotExist`**: define an [FGA schema](/authorization/rebac/define-schema) before using SSO → FGA group maps. See [SSO mapping → FGA](/sso/sso-mapping#groups-to-fga-relations). - **`E062028`**: only appears when you run the [SSO Setup Suite connection test](/auth-methods/sso/sso-setup-suite#testing) with Groups marked mandatory and JIT provisioning enabled. Confirm the test user belongs to at least one group at the IdP, and that **Groups Attribute Name** matches the IdP's actual claim or attribute name exactly. - **`E113201`**: FGA mappable helpers need valid FGA-mappable SAML settings and the AllowFGAMappings flag. - **Internal codes**: retry once; if it persists, contact support with the code and a timestamp. ### SCIM (Directory Sync) These errors happen during SCIM operations at the user layer, when Descope provisions users to or from a tenant's SCIM-enabled application. They're returned as HTTP `400`. | Code | Meaning | HTTP status | | ---- | ------- | ----------- | | `E025104` | SCIM sync failed | 400 | | `E025105` | Failed to send an outbound SCIM request (delivery to the downstream app failed) | 400 | | `E025106` | SCIM user conversion failed (mapping a user to or from the SCIM schema failed) | 400 | ## Step 1: Check Audit Logs The [Audit and Troubleshoot page](https://app.descope.com/audits) in the Descope Console provides detailed information about SSO-related events. To effectively use audit logs for SSO troubleshooting: 1. Review our [SSO Audit Events documentation](/audit-trails-and-integrations/audit-events#sso-related-fields) to understand available events 2. Filter events using the keyword "SSO" in the search bar 3. Focus on login events that contain SSO-specific fields: - SAML/OIDC response data - Group and role assignments - User provisioning details ## Step 2: Common Issues and Solutions **Network Connectivity**: Ensure Descope can access your IdP endpoints. If using a custom IdP, you may need to whitelist Descope's static public IPs. See our [Public Static IPs Doc](/how-to-deploy-to-production/public-static-ips) for the complete list of IP addresses. ### SAML Configuration Issues 1. **Metadata Mismatch** - Verify the Descope ACS (Assertion Consumer Service) URL and Entity ID are copied correctly to the IdP - Verify the IdP metadata matches exactly 2. **Certificate Issues** - Ensure certificates are valid and not expired - Verify certificate format (Base64-encoded X.509) - Check if IdP requires signing 3. **Callback URL Configuration** - Verify the callback URL is correctly configured in your application settings - Ensure the URL matches the one registered in your SSO configuration - Check for any URL encoding issues or trailing slashes 4. **Attribute Mapping Problems** - Review attribute mapping configuration in SSO Setup Suite - Verify IdP is sending expected attributes - Check attribute names and formats match exactly ### OIDC Configuration Issues 1. **Endpoint Configuration** - Verify all endpoint URLs are correct - Check authorization and token endpoint URLs - Confirm userinfo endpoint is accessible 2. **Client Credentials** - Verify Client ID and Secret are correct - Check if credentials have expired - Confirm redirect URIs are properly configured 3. **Scope Configuration** - Ensure required scopes are configured - Verify IdP is authorized for requested scopes - Check if custom scopes are properly formatted ### User Attribute / Group Mapping Issues [User/Group mapping](/sso/sso-mapping) is one of the most common sources of SSO configuration problems. To check group mapping (outside of audit logs), you need to either use the Management SDK/API or manually check the user's roles to verify they were correctly mapped. Here's how to troubleshoot based on your Identity Provider: 1. **General Group Mapping Issues** - Verify the Group's Attribute Name matches exactly what your IdP sends - Check if groups are being sent in the SAML assertion or OIDC token - Ensure group names in the mapping match exactly (case-sensitive) - Review the [Audit Logs](https://app.descope.com/audits) to see what group data is being received 2. **Microsoft Entra ID (Azure AD) Specific Issues** - User attributes often come in URI format (e.g., `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`) - Common issues with email/login ID mapping: - Make sure you map the SAML Subject ID to either `user.mail` or `user.userprincipalname`. If the User Principal Name (UPN) is not the user's email address, use `user.mail`. - Check if the email format matches your application's requirements - Ensure the user has an email address in their contact details in Entra - For groups: - Enable "Security Groups" in token configuration - Use Object ID instead of Display Name for more reliable mapping - SCIM Provisioning timing: - By default, Entra only syncs users and groups every ~40 minutes (this interval is not configurable) - Disabled or deprovisioned users are often **not** pushed to Descope until Entra runs a cycle that includes them — or until you trigger **Provision on Demand** / a provisioning job manually - To force immediate provisioning (create, update, or disable), use **Provision on Demand** in the Entra admin portal - See [SCIM with Azure](/management/tenant-management/scim/azure-scim) and [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices#provisioning-timing) 3. **Okta Specific Issues** **Understanding Okta's Group System** - Two separate group mechanisms exist in Okta: 1. **Push Groups** - Control SAML/OIDC group assertions - Configure in the "Group Attribute Statements" section - Used for sending group membership in auth tokens - Can use regex matching (e.g., `.*` to match all groups) 2. **Assignments** - Control SCIM provisioning - Found in the "Assignments" tab - Determine which users/groups get provisioned - Requires explicit assignment for provisioning - Use separate groups for SCIM vs SAML/OIDC push **Troubleshooting Steps** - Missing users in Descope: - Check if users are assigned to the application - Users in Push Groups but not assigned won't be provisioned - Group membership problems: - Verify "Group Attribute Statements" is enabled - Ensure groups are included in SAML assertion - Check group name matching is exact (case-sensitive) ### SSO User Merging When troubleshooting SSO user merging issues, see [Merging SSO Identities](/sso/merging-sso-identities-risk). ### SCIM Configuration Issues SCIM troubleshooting depends on your specific setup. If you're using only SSO with JIT Provisioning, without SCIM, this section doesn't apply to you. #### Scenario 1: SSO + SCIM with JIT Provisioning When using both SAML/OIDC SSO and SCIM together with JIT enabled: - **Mapping Configuration**: SCIM and SSO logins read the same group mapping, configured in the tenant's Roles & Groups tab - **Login ID Consistency**: Ensure login IDs are consistent across both protocols: - **SAML**: Login ID is the SAML NameID - **OIDC**: Login ID is the issuer by default (can be changed) - **SCIM**: Login ID is the username - **User Merging**: SCIM follows the [same merging rules as SSO authentication](/sso/merging-sso-identities-risk) and is affected by the [Convert Existing Users to SSO-only setting](/auth-methods/sso/settings#convert-existing-users-to-sso-only) #### Scenario 2: SCIM Only (No SSO) SCIM does not require SSO to be enabled for the tenant. Access key creation, the IdP's connection test, and user and group sync all work even when SSO was never set up. Group mapping is configured in the Roles & Groups tab of the tenant's authentication settings, and attribute mapping in the SSO tab. You can configure both without enabling SSO, either in the console or through the SCIM setup guides in the SSO Setup Suite: - **Group Mapping**: Configure group-to-role mapping in the Roles & Groups tab, and it applies to SCIM even when SSO is disabled. The mapping is kept in step across the tenant's SAML and OIDC settings, so switching the tenant's SSO method later does not lose or change it. - **Attribute Mapping**: Only custom attributes need explicit mapping (standard attributes are covered by SCIM protocol) - **User Merging**: Merging is controlled by the project-level [Convert Existing Users to SSO-only setting](/auth-methods/sso/settings#convert-existing-users-to-sso-only) and applies to SCIM regardless of the tenant's SSO state - **User Marking**: Users created via SCIM while SSO is disabled are not marked as SSO users; they are marked as SCIM-provisioned only - **Email Domain Validation**: This validation only applies when the project-level [Block Login on Email Domain Mismatch setting](/auth-methods/sso/settings#block-login-on-email-domain-mismatch) is enabled. When that setting is on, SCIM email writes are validated against the tenant's SSO Domains list (not the tenant's Email Domain setting) if SSO is enabled or SSO domains are configured for the tenant. If the tenant has neither SSO enabled nor any SSO domains configured, the check is skipped. #### Scenario 3: User Cannot Log In, Was Never Provisioned This is the most common SCIM-related issue: SCIM is configured, JIT is disabled (as recommended), and a specific user (often a new hire) cannot log in. The failure can surface in one of two ways: * **Authentication is rejected by the IdP** before Descope is reached. This is the typical case: the IdP rejects login for any user who is not assigned to the app. * **Authentication succeeds, but Descope returns a "user not found" style error.** This happens when the customer uses different IdP applications for SSO and SCIM, and the user is assigned to the SSO app but not the SCIM app, so SSO works but no SCIM record exists in Descope. **Cause:** In both cases, the IdP never provisioned this user to Descope. Provisioning is controlled by the IdP, not by Descope. A user is only pushed to Descope if they are **assigned to the application on the IdP side** representing Descope (an *Enterprise Application* in Azure, an *app integration* in Okta), either directly or as a member of a group that is assigned to the app. If the user was not in scope when SCIM was first set up and was never assigned afterward, the IdP will not push them, and Descope will have no record of them. **Diagnosis:** 1. Open the [Audit and Troubleshoot page](https://app.descope.com/audits) and filter for `SCIMEvent` events scoped to the tenant. 2. Search for an event corresponding to the user (by email or login ID). 3. If no `SCIMEvent` exists for the user, the IdP has never pushed them. The fix is on the IdP side. **Resolution:** * In Azure: confirm the user is assigned to the Enterprise Application (directly or via an assigned group), then use **Provision on Demand** to push them immediately rather than waiting for the next 40-minute cycle. * In Okta: confirm the user is in the **Assignments** tab of the application. Note that Assignments (which control SCIM provisioning) are separate from Okta's **Push Groups** (which push group objects via SCIM) and from **Group Attribute Statements** / OIDC claims (which control the groups sent in the SAML/OIDC authentication assertion). A user appearing in any of those does not mean they are assigned to the app. * Ask the customer about their ongoing onboarding process: if they assign users one at a time, every new hire will hit this issue. Point them at [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices) for the group-based assignment pattern. Group-to-role mapping in Descope does **not** cause provisioning. A user being in a group that is mapped to a Descope role does not mean the IdP will push that user. Provisioning and role mapping are entirely separate. See [Group Mapping vs. Provisioning](/management/tenant-management/scim#group-mapping-vs-provisioning) for the full distinction. #### General SCIM Troubleshooting For all SCIM scenarios: - Check SCIM logs for provisioning errors - Ensure attribute mappings are consistent across all configurations - Test with a sample user to verify proper provisioning and merging ## Step 3: Using SAML Tracer [SAML Tracer](https://chrome.google.com/webstore/detail/saml-tracer/mpdajninpobndbfcldcmbpnnbhibjmch) is a browser extension that helps debug SAML authentication issues: ![example SAML tracer](/assets/saml_tracer.webp) 1. **Installation and Setup** - Install SAML Tracer in your browser - Enable the extension before testing SSO 2. **Capturing SAML Traffic** - Start SAML Tracer before initiating SSO login - Look for SAML Request and Response messages - Examine the raw SAML assertions 3. **Common Patterns to Look For** - Missing or malformed SAML assertions - Incorrect signature validation - Timestamp validation failures - Missing required attributes # HMAC Authentication Types (/connectors/connector-hmac-usage) How to use the HMAC Authentication Type with HTTP Connectors. # Using HMAC Authentication Type HMAC is a specific type of authentication code involving a cryptographic hash function and a secret key. It may be used to simultaneously verify both the data integrity and the authentication of a message, as with any MAC. Descope allows you to use HMAC to sign the payload of your HTTP Connector. The outcome signature will be sent in the `x-descope-webhook-s256` header. The recipient service should use this secret to validate the payload's integrity and authenticity by verifying the supplied signature. ## Validating the HMAC Signature To validate the HMAC signature, the code could look something like this: ```javascript title="index.js" import { RawBodyRequest } from '@nestjs/common'; import crypto from 'crypto'; import { Request } from 'express'; function validate(raw: RawBodyRequest): boolean { const hmac = crypto.createHmac('sha256', process.env.HMAC_SECRET_KEY); hmac.update(Buffer.from(raw.rawBody)); // Ensure rawBody is a Buffer hmac.end(); const calculated = hmac.read().toString('base64'); const signature = raw.get('x-descope-webhook-s256') ?? 'invalid or nonexisting signature'; // Convert both the calculated signature and the received signature to Buffers const signatureBuffer = Buffer.from(signature, 'base64'); const calculatedBuffer = Buffer.from(calculated, 'base64'); // Ensure both Buffers are of the same length if (signatureBuffer.length !== calculatedBuffer.length) { return false; } return crypto.timingSafeEqual(signatureBuffer, calculatedBuffer); } ``` Or this: ```javascript title="index.js" const express = require('express'); const bodyParser = require('body-parser'); const crypto = require('crypto'); const app = express(); const PORT = 3000; // Middleware to parse JSON payloads app.use(bodyParser.json()); function verifyHmacSignature(payload, secret, sentHmac) { const computedHmac = crypto .createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('base64'); return sentHmac === computedHmac; } app.post('/webhook-endpoint', (req, res) => { const payload = req.body; // This is the parsed body const headers = req.headers; // This contains all headers // Assuming the sent HMAC is transmitted in the header 'x-hmac-signature' const sentHmac = headers['x-descope-webhook-s256']; // Use your secret here const secret = 'YOUR_SECRET'; if (!verifyHmacSignature(payload, secret, sentHmac)) { res.status(403).send('Invalid HMAC'); } // Serve request }); app.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); }); ``` ## Mocking an HMAC Signature If you wish to test the HMAC signature validation, you can use the following code to generate a valid signature for a given payload and secret, then include it in the headers. Note that Descope creates the HMAC signature from a JSON string, not the raw post body. ```javascript title="test.js" import fetch from 'node-fetch'; import crypto from 'crypto'; it('HMAC Signature', async () => { fetch.mockResolvedValue({ ok: true, json: () => Promise.resolve({}), }); const hmacSecret = 'YOUR_SECRET'; const payload = { p1: 'v1', }; await handler({ command: 'post', configuration: { baseUrl: 'https://example.com', hmacSecret, }, args: { endpoint: '/api', payload, }, }); // ensure that the signature is correct const expectedSignature = crypto .createHmac('sha256', hmacSecret) .update(JSON.stringify(payload)) .digest('base64'); expect(fetch).toHaveBeenCalledWith( 'https:/example.com/api', expect.objectContaining({ method: 'POST', body: JSON.stringify({ p1: 'v1', }), headers: expect.objectContaining({ 'x-descope-webhook-s256': expectedSignature, }), }), ); }); ``` # JSON Array Handling (/connectors/connector-json-usage) How to properly handle JSON arrays when using HTTP connectors. # JSON Array Handling Connectors may return their payload as an array of items. In the example below, we showcase configuring the connector action and utilizing the returned array data to update a user's properties, such as their phone number. This flow takes the user's email and then queries an API to verify if the email exists as a user in a 3rd party system through the HTTP Connector, and if so, update their user information to include their phone number. ## Configure Connector Action The first step is to configure your connector action. You will later need to use the `Context Key` of the connector's data; in this example, the data is stored under `connectors.httpResult`. ![HTTP Connector Configuration Retrieving Custom Data](/assets/http-connector-json-array.webp) ## Get Data from Array Next, we can access the data from the returned array from other Descope actions or conditionals, before we try to access it though, here is an example of it's contents. ```json { "connectors": { "httpResult": { "body": { "data": [ { "user@email.com": { "email": "user@email.com", "email_verified": true, "name": { "first": "John", "last": "Doe" }, "phone": { "number": "+12223334455", "verified": true } } } ] } } } } ``` Now we will access the array; in this example, we will use the `Update User/ Properties` action to get data from the array and update the user's phone number and verification status. Based on the above returned data under the `connectors.httpResult` context key, we will need to parse out the phone details per the below paths using [`jq`](https://jqlang.org/). - `jq:.connectors.httpResult.body.data[0][].phone.verified` - `jq:.connectors.httpResult.body.data[0][].phone.number` ![Update User Based on HTTP Connector JSON Array Data](/assets/update-user-json-array.webp) ## Connecting the Flow Once you have configured the connector action and the condition or other action to parse the data via `jq`, you can now connect the flow. Here is the flow in this example once completed. ![Example Flow Overview Showing User Property Update From Returned JSON Array Data](/assets/flow-json-array.webp) ## Post Flow Execution Below you can see the user's details are updated after the flow execution. ![User Properties Without Phone Number Prior To Update From Returned JSON Array Data](/assets/json-before.webp) ![User Properties Without Phone Number After Update From Returned JSON Array Data](/assets/json-after.webp) # Connectors with Localhost (/connectors/connector-localhost-usage) Learn how to test connectors against APIs running on localhost using tunneling services. # Testing a Connector with Localhost You can test Connectors against APIs running on localhost by using a tunneling service to expose your local API to the internet. This is useful for preliminary testing before deploying to a production environment. The HTTP Connector being tested in this manner is a particular example of using tunnels to test locally deployed services. This method can be used to test any connector, or API, that requires a public URL. ### Prerequisites 1. A REST API running on your localhost 2. A tunneling service to expose your localhost API to the internet. Popular options include: - [Cloudflare Tunnel](https://developers.cloudflare.com/pages/how-to/preview-with-cloudflare-tunnel/) - [ngrok](https://ngrok.com/docs/api/resources/tunnels/) - localhost.run ### Step-by-step Guide: #### Deploy your API using a chosen service Choose any of the above-mentioned services to expose your local API to the internet. Once exposed, note down the public URL as it will be used in the next step. #### Configuring the HTTP Connector Navigate to the Descope's HTTP Connector configuration page and fill in the required parameters: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Base URL**: Input the consistent section, or the root, of your API's URL. This should start with either `http://` or `https://`. Use the public URL from Step 1. - **Authentication Type**: Descope supports various methods to authenticate with your service. Choose the method that suits your API: - **Bearer Token**: Used for access keys such as JWTs. - **API Key**: This usually involves a key-value pair. - **Basic Authentication**: The traditional username and password method. - **None**: Select this if your API doesn't require any authentication. - **Headers (Optional)**: Some APIs need specific headers, usually key-value pairs, to provide more details about the impending action. - **HMAC Secret (Optional)**: HMAC is a symmetric key method for message signing. The provided secret will be used to sign the payload. The outcome signature will be sent in the `x-descope-webhook-s256` header. The recipient service should use this secret to validate the payload's integrity and authenticity by verifying the supplied signature. - **Trust Any Certificate**: By default, this option is turned off. If enabled, the client will overlook any certificate errors. While convenient for testing, it's crucial to remember that this is an insecure choice for production. ![configure generic http connector](/assets/generic-http-connector-config.webp) #### Running the Flow Once you've configured the HTTP Connector and added it in the Flow Editor, you can run your flow locally. During this process, the conector should be integrated seamlessly. #### Final Steps After you've completed your tests, consider deploying your API to a more stable service. Once done, revisit the HTTP Connector configuration page to update it with the new parameters from your permanent API deployment. # Connectors in Flows (/connectors/connectors-in-flows) Learn how to utilize Descope Connectors to enrich your flows by interacting with 3rd party services # Connectors in Flows Connectors enable you to integrate with third-party services to fetch data or push information during flow execution. Configure connectors in the [Connectors page](https://app.descope.com/connectors) of the Descope Console, then add them as steps within your flows. For further information about Connectors including dedicated configuration guides, check out our [Connectors Doc](/connectors). ## Testing Connectors If the connector supports testing, use the test functionality on the connector configuration page to verify connectivity. For some connector types, you may need to provide a test URL or endpoint. Test results display in the Test Results panel, showing the response or any errors that occurred. ## Implementing Connectors in Flows ### Adding a Connector 1. Click the **+** icon in the flow editor and select **Connector** 2. Select the Connector or Connector type (e.g., GET request for Generic HTTP) ![Adding Descope third party action within flows](/assets/add-3rd-party-action.webp) ![Adding Descope third party configuration within flows](/assets/select-3rd-party-action.webp) ### Configuring a Connector This section covers the possible settings for Connectors in [Flows](/flows). #### Connector Settings | Setting | Description | | ------- | ----------- | | Step Name | Name for the connector step in your flow | | Connector | Select from configured connectors in your project | | Base URL | Auto-populated from connector configuration (read-only) | | Endpoint | Endpoint path (can include query parameters) | | Payload | JSON Payload for the request | | Timeout | HTTP request timeout in seconds | | Run asynchronously | Select this option if the flow doesn't need to wait for the step's result to continue (like sending analytics data). Response will not be available in context. | | Context Key | Response data is mapped to this key for use in later steps | | Error Handling | Define custom error handling | ### Attaching Connectors to a Flow Once you have configured the Connector, attach it within your flow. In this example, the action is connected after successful authentication of the user. ![Descope connector action shown within flows](/assets/add-the-connector-within-flow.webp) ### Accessing Connector Results Once a connector step executes, its response data is available in the flow context under the **Context Key** you specified during configuration. Access the data using the pattern `connectors.`. For nested JSON properties, use dot notation (`connectors..`). You can use connector results in: - **Conditions**: Implement conditional logic based on connector response data - **Custom Claims**: Add connector data to the user's JWT using the [Custom Claims](/flows/actions/custom-claims) action - **Update User Properties**: Update user attributes with data from the connector response - **Other Actions**: Reference connector data in any action that supports dynamic values ## Troubleshooting Connectors You can use the [Flow Runner](/handling-flow-errors/troubleshooting-flows#flow-runner) to debug connectors. When running a flow, Descope logs the request sent to the connector and the response received in the runner messages. # Descope Engine (/connectors/descope-engine) Deploy the Descope Engine to run connector actions inside your private network. # Descope Engine This is currently only available to [FedRAMP](/fedramp) customers. The **Connectors → Engines** page in the Descope Console must be enabled by Descope for your company. If you do not see the Engines page, contact the Descope CS team before proceeding. The Descope Engine is a lightweight, deployable Docker image that allows Descope [Connectors](/connectors) to reach resources that are not accessible from the public internet — for example, an internal HTTP API behind your firewall or a private database. In order to use Descope Connectors against private resources, you must deploy and run this Engine within **your own environment**. Descope does not run the Engine for you in Descope's cloud. ## How It Works 1. You create an Engine in the Descope Console and copy its **Engine ID** and **Engine Key**. 2. You run the Engine container in your network with those credentials. It opens a single **outbound** TLS connection to Descope (`descope.fedstart.com:443`) — no inbound ports. 3. When a [flow](/flows) invokes a connector assigned to that Engine, Descope sends the command over the open connection. The Engine executes it locally (for example, calling an internal API) and returns the result. ### Supported Connectors on the Engine These are a list of the connectors that are supported with the Descope Engine. | Connector | Status | Notes | | --------- | ------ | ----- | | **HTTP** (Generic HTTP) | Supported | Primary use case — internal APIs behind your firewall. | | **AWS S3** | Supported | Private or GovCloud buckets from your network. | | **AWS SES** | Not validated | Contact Descope before use. | | **WhatsApp** | Not validated | Contact Descope before use. | | **SMS** (Twilio, Mitto, etc.) | **Not supported** in Gov / FedRAMP | Commercial SMS providers are generally not Gov-compliant. | | **Email** (SendGrid, SMTP, etc.) | **Not supported** in Gov / FedRAMP | Commercial email providers are generally not Gov-compliant. | If your organization identifies a **Gov-compliant** SMS or email provider that must be reachable from the Engine, share provider details and endpoints with Descope for evaluation. ## Quick Start 1. **Create the Engine** in the Descope Console and copy your **Engine ID** and **Engine Key**. 2. **Set up and deploy** the container — pull the image, configure your `.env` with those credentials, and run it. 3. **Test** the connection and assign connectors. ### Step 1: Create the Engine in Descope Create an Engine in the Descope Console and copy its credentials: 1. Open the [Descope Console](https://app.descope.com). Go to **Connectors → Engines**. ![Engines Page](/assets/engines-page.webp) 2. **Create** a new engine, or select an existing one if previously created. ![Create Engine](/assets/create-engine.webp) 3. Once created, you will need to copy the **ID** and **Engine key** from the Engine settings. | Console (Connectors → Engines → Edit) | `.env` variable | | ------------------------------------- | --------------- | | **ID** | `ENGINE_ID` | | **Engine key** | `ENGINE_SECRET` | You will use these values when you configure and deploy the container in the next step. ### Step 2: Set Up and Deploy the Engine #### Configure and run Before deploying, confirm your network allows the outbound connections described in [Networking & Security](#networking--security). Create an `.env` file with your credentials from [Step 1](#step-1-create-the-engine-in-descope): ```bash # Required — from Connectors → Engines → Edit ENGINE_ID=your-engine-id-here ENGINE_SECRET=your-engine-key-here # Descope Engine gRPC endpoint (FedRAMP / Gov) SERVER_ADDRESS=descope.fedstart.com:443 USE_SSL=true ``` To modify the container image and change environment settings, you can review the [Configuration reference](#configuration-reference) section. Pull the Engine image from [Docker Hub](https://hub.docker.com/r/descope/engine) (`descope/engine`) or GitHub Container Registry (`ghcr.io/descope/engine`): ```bash docker run -it --env-file .env descope/engine ``` For high availability, run the same image on multiple hosts or pods using the **same** `ENGINE_ID` and `ENGINE_SECRET`. See the [Reconnect and high availability](#reconnect-and-high-availability) docs for more information. ### Step 3: Associating the Connector with the Engine The list of available connectors that work with the Engine are listed in the [Supported Connectors on the Engine](#supported-connectors-on-the-engine) section. You can associate a connector with an Engine by selecting the Engine in the connector configuration page. 1. Go to **Connectors** and open the connector configuration. 2. In the **Engine** dropdown, select the Engine you created. 3. Save the connector. ![Associate Connector with Engine](/assets/associate-connector-with-engine.webp) Once you've successfully associated a connector with the Engine, you can look for `gRPC stream established with server` and `Listening for commands` in your Docker container logs: ![Engine connected logs](/assets/engine-connected-logs.webp) #### Run an end-to-end test 1. Add the connector to a [flow](/flows) (or trigger the connector action you configured). 2. Run the flow and confirm the connector completes successfully. 3. Check Engine container logs for the execute command and response. If the flow fails, see [Troubleshooting](#troubleshooting). ## Configuration Reference All configuration is provided through environment variables you can set in your docker image. ### Required | Variable | Description | | -------- | ----------- | | `ENGINE_ID` | Engine ID from the Descope Console. Replicas sharing it form one HA group. Your Descope project is derived from this ID. | | `ENGINE_SECRET` | Engine Secret from the Console. Sent in the `Hello` message and verified by Descope on connect. | | `ENGINE_IMAGE_VERSION` | Pre-set in the official image — only set if you build your own image. | | `ENGINE_CONTENT_VERSION` | Connector template version identifier. Pre-set in the official image. | ### Connection & TLS | Variable | Default | Description | | -------- | ------- | ----------- | | `SERVER_ADDRESS` | `localhost:50051` | Descope Engine gRPC endpoint (`host:port`). Use `descope.fedstart.com:443` for FedRAMP / Gov. | | `USE_SSL` | `true` | Use TLS for the connection. Keep `true` in production. | | `VERIFY_SERVER_CERTIFICATE` | `true` | Verify the server TLS certificate. Set `false` only for self-signed certs in development. | ### Timing & keepalive (optional) | Variable | Default | Description | | -------- | ------- | ----------- | | `HEARTBEAT_INTERVAL` | `30000` | Application-level heartbeat interval (ms). | | `GRPC_KEEPALIVE_TIME_MS` | `30000` | HTTP/2 PING interval when idle (ms). Keeps the connection alive through proxies. | | `GRPC_KEEPALIVE_TIMEOUT_MS` | `20000` | How long to wait for a PING acknowledgement before dropping the connection (ms). | | `GRPC_KEEPALIVE_PERMIT_WITHOUT_CALLS` | `true` | Send PINGs even when there are no active calls. | ### Reconnection (optional) | Variable | Default | Description | | -------- | ------- | ----------- | | `MAX_RECONNECT_ATTEMPTS` | `10` | Maximum reconnection attempts before the process exits. | | `BASE_RECONNECT_DELAY` | `1000` | Initial reconnection delay (ms). | | `MAX_RECONNECT_DELAY` | `30000` | Maximum reconnection delay with jitter (ms). | ### Logging (optional) | Variable | Default | Description | | -------- | ------- | ----------- | | `LOG_LEVEL` | `info` | Log verbosity (`fatal`, `error`, `warn`, `info`, `debug`, `trace`). In production (`NODE_ENV=production`) logs are JSON; otherwise pretty-printed. | ## Networking & Security Review this section with your network and security teams **before** you deploy the Engine in production. ### How traffic flows The Engine runs in **your** environment and opens a **single outbound** gRPC connection to Descope over TLS. Descope never initiates inbound connections to your network. You do not need a public IP, load balancer, or inbound firewall rules for Descope to reach the Engine. When a flow runs a connector assigned to that Engine, Descope sends the command over the existing connection. The Engine then calls resources **inside your network** (for example, an internal HTTP API) and returns the result to Descope over the same connection. ### Firewall and egress You will need to make sure that your network allows the FedRAMP static IPs, to be able to reach the Descope Engine. The static IPs are documented [here](/how-to-deploy-to-production/public-static-ips#fedramp-federal-risk-and-authorization-management-program). In addition, you will need to make sure that your network allows the outbound traffic to the Descope Engine: | Direction | Destination | Port | Required for | | --------- | ----------- | ---- | ------------ | | **Outbound** | `descope.fedstart.com` | `443` (TLS) | Engine registration, heartbeats, and connector command dispatch | | **Outbound** | Internal hosts your connectors use | Varies (e.g. `443`, `5432`) | Connector actions (APIs, databases, private S3 endpoints, etc.) | If outbound access to `descope.fedstart.com:443` is blocked, the Console shows **Disconnected** and connector actions cannot run. See [Troubleshooting](#troubleshooting). ### Runtime footprint The Engine is **stateless**: it does not store customer data or require a database. Plan for container logs only; there is nothing to back up on the Engine itself. ## Reconnect and High Availability If the gRPC stream drops, the Engine retries with exponential backoff, sends `Hello` again, and resumes listening. Run **more than one** container with the **same** `ENGINE_ID` and `ENGINE_SECRET` for HA. Descope dispatches each command to any connected container. | Setup | What happens if one container goes down | | ----- | --------------------------------------- | | **Single container** | Connector actions wait or fail until reconnect or replacement. | | **Multiple containers, same `ENGINE_ID`** | Other containers keep executing commands. | ## Troubleshooting These are some of the most common issues and fixes for the Descope Engine. | Symptom | Likely cause / fix | | ------- | ------------------ | | **Engines** page missing | Engine not enabled on company license — contact Descope CS. | | Invalid engine secret / auth failure | Wrong `ENGINE_ID` or `ENGINE_SECRET`, or secret rotated. Re-copy from **Connectors → Engines → Edit**. | | Connection refused / cannot reach server | Wrong `SERVER_ADDRESS`, or egress firewall blocking TLS to `descope.fedstart.com`. | | Connection drops when idle | Proxy idle timeout — lower `GRPC_KEEPALIVE_TIME_MS`. | | Engine shows **Disconnected** | Container not running or credentials misconfigured. | | Commands not executed | No connected Engine with the correct `ENGINE_ID`, or connector not assigned to the Engine. | | Need a shell | The production image is minimal (FIPS Node runtime). Override the entrypoint only when your base tooling supports inspection. | # Overview of Connectors (/connectors) Overview for all the connectors that we support. # Connectors With Connectors, you can enrich your Descope flows by allowing you to integrate with external services to fetch information or push data during the execution of the flow. Connectors are configured within the [Connectors Page](https://app.descope.com/connectors) of the Descope Console. Most connectors are used as steps within [Flows](/flows), except for [Audit & Troubleshooting Connectors](/connectors/connector-configuration-guides/audit-and-troubleshooting) which are used for streaming data and are not added as flow steps. Connectors run **synchronously** by default: the flow waits for the connector to complete and receive a response before advancing to the next step. The response is then available in [flow context](/connectors/connectors-in-flows) for use in conditions, scriptlets, or later steps. You can also configure a connector step to run [asynchronously](/connectors/connectors-in-flows#connector-settings)—for example when the flow does not need the result (e.g. sending analytics or firing a webhook). When run async, the flow continues without waiting and the connector response is not written to flow context. To run connector actions against APIs or services inside your firewall (no inbound ports), deploy the [Descope Engine](/connectors/descope-engine) in your network and assign connectors to it in the Console. Here is a comprehensive list of all of the **Connectors** we currently support, as well as guides on how to configure each of them. If you wish to request a new connector, please submit a [Feature Request](/support/descope-fr-portal). # KrakenD (/other-integrations/krakend) Learn how to best use Descope with the KrakenD API Gateway # Use Descope with KrakenD [KrakenD](https://www.krakend.io/) is an open-source API Gateway that allows you to create a fast, secure, and scalable API gateway. Securing your APIs and underlying microservices is crucial in modern development. Descope enables you to add advanced authentication capabilities to your KrakenD endpoints efficiently. KrakenD integrates with Descope via the [JWT validation](https://www.krakend.io/docs/authorization/jwt-validation/) component. ## The validation workflow Whether you are trying to protect your API from end-users or machine-to-machine access, the workflow is the same: - End Users use their applications to log in to Descope who provides an access token for the session. - Machine-to-machine communication also uses a token from Descope after providing a client_id and a client_secret. - With the token generated by Descope, the client passes it to KrakenD in each request inside an HTTP header or cookie - KrakenD authorizes or not the usage of the specific endpoint according to the rules you have configured. As KrakenD can validate the Descope signature by itself, it does not need to call the Descope server to validate the token every time. Instead, KrakenD queries Descope every 15 minutes (configurable) to ensure the key has not rotated. To learn more about our JWK rotation, see our [doc](/additional-security-features-in-descope/jwk-rotation) on it. ## Protecting endpoints with Descope tokens We will create a simple KrakenD configuration with a single endpoint `/descope-protected`, ensuring only users with valid tokens can access it. Create a `krakend.json` file and add the following configuration: ```json { "version": 3, "timeout": "3s", "endpoints":[ { "endpoint": "/descope-protected", "extra_config": { "auth/validator": { "alg": "RS256", "jwk_url": "__BaseURL__/v2/keys/__ProjectID__" } }, "backend": [ { "host":["http://localhost:8080"], "url_pattern": "/__health" } ] }] } ``` Replace `api.descope.com` with your [custom domain](/how-to-deploy-to-production/custom-domain) or your respective localized base url. That's all you need for the basic configuration! You can expand the structure now to include checking specific roles, claims, etc. ### Testing the configuration From the folder where we create our `krakend.json` file, start the gateway with: ```bash docker run --rm -v "$PWD:/etc/krakend" -p "8080:8080" ``` Verify the gateway is running by checking the unprotected /__health endpoint: ```bash curl -iG http://localhost:8080/__health {"status":"ok"} ``` Now let's try to access the `/descope-protected` endpoint **without a token**: ```bash curl -iG http://localhost:8080/descope-protected HTTP/1.1 401 Unauthorized ``` Since no token is provided, KrakenD correctly denies access. If you check the KrakenD logs, you will also find a line `Error #01: Token not found`. Let's get a valid M2M token now. Log in to your Descope Console and [create an access key](/management/m2m-access-keys). Using your Descope Project Id and Access Key, you can then run the followin `cURL` command to exchange the access key for a JWT: ```bash curl -X POST "__BaseURL__/v1/auth/accesskey/exchange" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer __ProjectID__:" ``` The response of the call above will provide you with a `sessionJwt`. Now you are ready to request protected resources from the gateway. Replace the sample URL with your gateway URL and `` with the `sessionJwt` retrieved in the previous step: ```bash curl --request GET \ --url http://localhost:8080/descope-protected \ --header 'authorization: Bearer ' {"status":"ok"} ``` That's it! The `{"status": "ok"}` is the response you have from the `/descope-protected` endpoint after being validated as a legitimate user. ## Advanced configurations With that we have completed the basic setup that validates users using Descope access tokens. Some possibilities are: - Create [Roles in Descope](/authorization) and add them as a condition to accessing an endpoint in KrakenD. - Customize JWTs with [Descope JWT templates](/management/token/jwt-templates) for Users or Access Keys - Propagate user claims to your backend services. For more details, see [KrakenD's JWT Validation documentation](https://www.krakend.io/docs/authorization/jwt-validation/). ## Conclusion Integrating Descope with KrakenD enhances security while maintaining flexibility. You can designate protected and public endpoints, ensuring controlled access to your APIs. # Auth Helpers (/client-sdk/auth-helpers) Learn how the Auth class in Descope SDK handles user authentication operations within your web client application. # Client SDK Auth Helpers The Auth class is a crucial component of the Descope SDK, handling key user authentication operations. This class is designed to execute essential functions such as fetching user details, fetching and refreshing sessions, and logging out users. Before using the Auth Helper Functions, make sure to [import and initialize the relevant SDK.](/client-sdk/initialize-sdk) ## Manage Session If project settings are configured to manage session token in cookies, Descope services will automatically set the session token in the DS cookie as a Secure and HttpOnly cookie. In this case, the session token will not be stored in the browser and will not be accessible to the client-side code using these session management functions. The `useSession` functions retrieve information about the current user session. They return the following details: #### Objects - `sessionToken`: The JWT for the current session. - `claims`: The claims carried on the current session token, refreshed on every session refresh. This object holds the [custom claims](/management/token) you configured, plus the two expiration claims: - `exp`: The standard JWT expiration timestamp for the current session token. - `rexp`: The refresh token expiration timestamp. Use these on the client to track session validity and to trigger user-facing timeout warnings. For the data types and payload structure, see [Claims returned by the Client SDK](/management/token#claims-returned-by-the-client-sdk), for a timeout-warning component, see [Reading the expiration time](/sessions/management/web#reading-the-expiration-time). #### Booleans - `isAuthenticated`: Boolean for the authentication state of the current user session. - `isSessionLoading`: Boolean that indicates whether the SDK is still resolving the initial session state. It becomes `false` after the initial refresh attempt completes, whether the session refresh succeeds or fails. ### Additional helper functions - `getSessionToken`: The JWT for the current session. - `getRefreshToken`: The refresh JWT for the current session. The `getRefreshMethod` function will not work if refresh tokens are stored in `httpOnly` cookies, as they cannot be accessed from the frontend. - `isSessionTokenExpired`: Boolean for checking if a given session token is expired. - `isRefreshTokenExpired`: Boolean for checking if a refresh token is expired. - `getJwtRoles`: Get current roles from an existing session token. Provide tenant id for specific tenant roles. - `getJwtPermissions`: Get current permissions from an existing session token. Provide tenant id for specific tenant permissions. - `getCurrentTenant`: Get current tenant id from an existing session token (from the dct claim). ```javascript import { useSession, getSessionToken, getRefreshToken, isSessionTokenExpired, isRefreshTokenExpired, getJwtRoles, getJwtPermissions, getCurrentTenant } from '@descope/react-sdk'; import { useCallback } from 'react'; const App = () => { const { isAuthenticated, isSessionLoading, sessionToken, claims } = useSession(); if (isSessionLoading) { return

Loading...

; } const refreshToken = getRefreshToken(); const sessionTokenFromFunction = getSessionToken(); const isRefreshTokenExpired = isRefreshTokenExpired(refreshToken); const isSessionTokenExpired = isSessionTokenExpired(sessionTokenFromFunction); const jwtRoles = getJwtRoles(token = sessionToken, tenant = ''); const jwtPermissions = getJwtPermissions(token = sessionToken, tenant = ''); const currentTenant = getCurrentTenant(token = sessionToken); if isAuthenticated { return ( <>

You are authenticated.

); } }; ``` ```javascript 'use client'; import { useSession, getSessionToken, getRefreshToken, isSessionTokenExpired, isRefreshTokenExpired, getJwtRoles, getJwtPermissions, getCurrentTenant } from '@descope/nextjs-sdk/client'; import { useCallback } from 'react'; const App = () => { const { isAuthenticated, isSessionLoading, sessionToken, claims } = useSession(); const refreshToken = getRefreshToken(); const sessionTokenFromFunction = getSessionToken(); const isRefreshTokenExpired = isRefreshTokenExpired(refreshToken); const isSessionTokenExpired = isSessionTokenExpired(sessionTokenFromFunction); const jwtRoles = getJwtRoles(token = sessionToken, tenant = ''); const jwtPermissions = getJwtPermissions(token = sessionToken, tenant = ''); const currentTenant = getCurrentTenant(token = sessionToken); if isAuthenticated { return ( <>

You are authenticated.

); } }; ``` ```javascript ``` ```javascript import { Component, OnInit } from '@angular/core'; import { DescopeAuthService } from '@descope/angular-sdk'; ... export class AppComponent implements OnInit { isAuthenticated: boolean = false; sessionToken: string | null = null; refreshToken: string | null = null; isRefreshTokenExpired: boolean = false; isSessionTokenExpired: boolean = false; jwtRoles: string[] = []; jwtPermissions: string[] = []; currentTenant: string | null = null; claims: Record | undefined; constructor(private authService: DescopeAuthService) {} ngOnInit() { this.authService.session$.subscribe((session) => { this.isAuthenticated = session.isAuthenticated; this.sessionToken = this.authService.getSessionToken(); this.refreshToken = this.authService.getRefreshToken(); this.isRefreshTokenExpired = this.authService.isRefreshTokenExpired(this.refreshToken); this.isSessionTokenExpired = this.authService.isSessionTokenExpired(this.sessionToken); this.jwtRoles = this.authService.getJwtRoles(token = this.sessionToken, tenant = ''); this.jwtPermissions = this.authService.getJwtPermissions(token = this.sessionToken, tenant = ''); this.currentTenant = this.authService.getCurrentTenant(token = this.sessionToken); this.claims = session.claims; }); } } ``` ## Core SDK Functions The following functions are part of the Core SDK (inherited by all client SDKs) and can be accessed through the `useDescope()` hook. These functions provide additional functionality for session management, tenant operations, and user data retrieval. ### Available Functions - `refresh(options?)`: Refreshes a session token using a valid refresh token. Accepts an optional `options` object: - `skipIfNoSession`: If set to `true`, skips the network request when no session exists — useful for avoiding redundant refresh calls on initial page load for unauthenticated users. - `selectTenant`: Selects a tenant for the current session. You can use this function to switch the `dct` claim in the JWT token. - `myTenants`: Returns tenant information for the current user - `history`: Returns the current user's authentication history - `logout`: Logs out the current session - `logoutAll`: Logs out all sessions for the current user - `me`: Returns the current user details - `isJwtExpired`: Checks if the given JWT is still valid (does NOT check signature) - `getTenants`: Returns the list of tenants in the given JWT (does NOT check signature) ### Usage Examples ```javascript import { useDescope } from '@descope/react-sdk'; const App = () => { const sdk = useDescope(); const handleRefresh = async () => { try { // Pass { skipIfNoSession: true } to skip the refresh if no session exists const result = await sdk.refresh({ skipIfNoSession: true }); console.log('Session refreshed:', result); } catch (error) { console.error('Refresh failed:', error); } }; const handleSelectTenant = async (tenantId) => { try { await sdk.selectTenant(tenantId); console.log('Tenant selected'); } catch (error) { console.error('Tenant selection failed:', error); } }; const getMyTenants = async () => { try { const tenants = await sdk.myTenants(true); // Get selected tenant only console.log('My tenants:', tenants); } catch (error) { console.error('Failed to get tenants:', error); } }; const getUserHistory = async () => { try { const history = await sdk.history(); console.log('User history:', history); } catch (error) { console.error('Failed to get history:', error); } }; const checkJwtExpiry = (token) => { const isExpired = sdk.isJwtExpired(token); console.log('JWT expired:', isExpired); }; const getTenantList = (token) => { const tenants = sdk.getTenants(token); console.log('Tenants in JWT:', tenants); }; return (
); }; ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; const App = () => { const sdk = useDescope(); const handleRefresh = async () => { try { // Pass { skipIfNoSession: true } to skip the refresh if no session exists const result = await sdk.refresh({ skipIfNoSession: true }); console.log('Session refreshed:', result); } catch (error) { console.error('Refresh failed:', error); } }; const getMyTenants = async () => { try { const tenants = await sdk.myTenants(true); console.log('My tenants:', tenants); } catch (error) { console.error('Failed to get tenants:', error); } }; return (
); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: "__ProjectID__" }); // All functions are directly available on the SDK instance const handleRefresh = async () => { try { // Pass { skipIfNoSession: true } to skip the refresh if no session exists const result = await sdk.refresh({ skipIfNoSession: true }); console.log('Session refreshed:', result); } catch (error) { console.error('Refresh failed:', error); } }; const handleSelectTenant = async (tenantId) => { try { await sdk.selectTenant(tenantId); console.log('Tenant selected'); } catch (error) { console.error('Tenant selection failed:', error); } }; const getMyTenants = async () => { try { const tenants = await sdk.myTenants(true); console.log('My tenants:', tenants); } catch (error) { console.error('Failed to get tenants:', error); } }; const getUserHistory = async () => { try { const history = await sdk.history(); console.log('User history:', history); } catch (error) { console.error('Failed to get history:', error); } }; const checkJwtExpiry = (token) => { const isExpired = sdk.isJwtExpired(token); console.log('JWT expired:', isExpired); }; const getTenantList = (token) => { const tenants = sdk.getTenants(token); console.log('Tenants in JWT:', tenants); }; ``` ```javascript ``` ```typescript import { Component } from '@angular/core'; import { DescopeAuthService } from '@descope/angular-sdk'; @Component({ selector: 'app-core-functions', template: ` ` }) export class CoreFunctionsComponent { constructor(private authService: DescopeAuthService) {} async handleRefresh() { try { // Pass { skipIfNoSession: true } to skip the refresh if no session exists const result = await this.authService.descopeSdk.refresh({ skipIfNoSession: true }); console.log('Session refreshed:', result); } catch (error) { console.error('Refresh failed:', error); } } async handleSelectTenant(tenantId: string) { try { await this.authService.descopeSdk.selectTenant(tenantId); console.log('Tenant selected'); } catch (error) { console.error('Tenant selection failed:', error); } } async getMyTenants() { try { const tenants = await this.authService.descopeSdk.myTenants(true); console.log('My tenants:', tenants); } catch (error) { console.error('Failed to get tenants:', error); } } async getUserHistory() { try { const history = await this.authService.descopeSdk.history(); console.log('User history:', history); } catch (error) { console.error('Failed to get history:', error); } } } ``` ## Handling Authentication State Changes Descope provides event listeners to track session and user state changes dynamically. These functions can be accessed through `useDescope()`. ### Event Functions - `onSessionTokenChange(newSession, oldSession)`: Triggers when the session token changes. - `onIsAuthenticatedChange(isAuthenticated)`: Fires when authentication status changes. - `onUserChange(newUser, oldUser)`: Triggers when user data updates. - `onClaimsChange(newClaims, oldClaims)`: Triggers when the [session claims](#manage-session) change, which happens on every session refresh. Use this to react to a new `exp` or `rexp` value, or to a custom claim that changed after a refresh. ```javascript import { useEffect, useState } from 'react'; import { useDescope } from '@descope/react-sdk'; const App = () => { const [user, setUser] = useState(); const [session, setSession] = useState(); const [isAuthenticated, setIsAuthenticated] = useState(false); const [claims, setClaims] = useState>(); const sdk = useDescope(); useEffect(() => { const unsubscribeTokenChange = sdk.onSessionTokenChange(setSession); const unsubscribeAuthChange = sdk.onIsAuthenticatedChange(setIsAuthenticated); const unsubscribeUserChange = sdk.onUserChange(setUser); const unsubscribeClaimsChange = sdk.onClaimsChange(setClaims); return () => { unsubscribeTokenChange(); unsubscribeAuthChange(); unsubscribeUserChange(); unsubscribeClaimsChange(); }; }, [sdk]); return

Listening for authentication changes...

; }; ``` ```javascript 'use client'; import { useEffect, useState } from 'react'; import { useDescope } from '@descope/nextjs-sdk/client'; const App = () => { const [user, setUser] = useState(); const [session, setSession] = useState(); const [isAuthenticated, setIsAuthenticated] = useState(false); const [claims, setClaims] = useState>(); const sdk = useDescope(); useEffect(() => { const unsubscribeTokenChange = sdk.onSessionTokenChange(setSession); const unsubscribeAuthChange = sdk.onIsAuthenticatedChange(setIsAuthenticated); const unsubscribeUserChange = sdk.onUserChange(setUser); const unsubscribeClaimsChange = sdk.onClaimsChange(setClaims); return () => { unsubscribeTokenChange(); unsubscribeAuthChange(); unsubscribeUserChange(); unsubscribeClaimsChange(); }; }, [sdk]); return

Listening for authentication changes...

; }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: "__ProjectID__" }); sdk.onSessionTokenChange((newSession, oldSession) => { console.log("Session updated:", newSession); }); sdk.onIsAuthenticatedChange((isAuthenticated) => { console.log("Authentication changed:", isAuthenticated); }); sdk.onUserChange((newUser, oldUser) => { console.log("User details updated:", newUser); }); sdk.onClaimsChange((newClaims, oldClaims) => { console.log("Session claims updated:", newClaims); }); ``` ```javascript ``` ```typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { DescopeAuthService } from '@descope/angular-sdk'; @Component({ selector: 'app-root', template: '

Listening for authentication changes...

' }) export class AppComponent implements OnInit, OnDestroy { private unsubscribeTokenChange: any; private unsubscribeAuthChange: any; private unsubscribeUserChange: any; private unsubscribeClaimsChange: any; constructor(private authService: DescopeAuthService) {} ngOnInit() { this.unsubscribeTokenChange = this.authService.sdk.onSessionTokenChange( (newSession, oldSession) => { console.log('Session updated:', newSession); } ); this.unsubscribeAuthChange = this.authService.sdk.onIsAuthenticatedChange( (isAuthenticated) => { console.log('Authentication changed:', isAuthenticated); } ); this.unsubscribeUserChange = this.authService.sdk.onUserChange( (newUser, oldUser) => { console.log('User details updated:', newUser); } ); this.unsubscribeClaimsChange = this.authService.sdk.onClaimsChange( (newClaims, oldClaims) => { console.log('Session claims updated:', newClaims); } ); } ngOnDestroy() { this.unsubscribeTokenChange(); this.unsubscribeAuthChange(); this.unsubscribeUserChange(); this.unsubscribeClaimsChange(); } } ``` ## Manage User The `useUser` and `me` functions return information about the currently authenticated user. These methods are used when you need to fetch or display user-related data in your application. These functions returns the following details: #### Objects - `user`: object that contains the following user attributes: - `email`: Email address associated to the user. - `name`: Name associated to the user. - `givenName`: Given name associated to the user. - `middleName`: Middle name associated to the user. - `familyName`: Family name associated to the user. - `phone`: Phone number associated to the user. - `loginIds`: An array of loginIds associated to the user. - `userId`: The user's unique Descope generated userId. - `verifiedEmail`: Boolean whether the email address for the user has been verified. - `verifiedPhone`: Boolean whether the phone number for the user has been verified. - `picture`: The base64 encoded image if the user has an image associated to them. - `roleNames`: An array of roles associated to the user. - `userTenants`: An array of tenants associated with the user. Each tenant object contains: - `tenantId`: The tenant's unique ID. - `tenantName`: The tenant's name. - `roleNames`: An optional array of roles assigned to the user for that tenant. - `permissions`: An optional array of permissions assigned to the user for that tenant. - `createTime`: The time that the user was created. - `totp`: Boolean whether the user has TOTP login associated with it. - `saml`: Boolean whether the user has SAML login associated with it. - `scim`: Boolean whether the user was created using SCIM. - `oauth`: Boolean whether the user has OAuth login associated with it. #### Booleans - `isUserLoading`: Boolean that indicates whether the SDK is still retrieving the current user. It becomes `false` after the request completes, whether the request succeeds or fails. `useDescope`, `useSession`, and `useUser` should be used inside `AuthProvider` context, and will throw an exception if this requirement is not met ```javascript import { useUser } from '@descope/react-sdk'; import { useCallback } from 'react'; const App = () => { const { user, isUserLoading } = useUser(); if (isUserLoading) { return

Loading user...

; } if (!user) { return

No user information is available.

; } return

Hello {user.name}

; }; ``` ```javascript 'use client'; import { useUser } from '@descope/nextjs-sdk/client'; import { useCallback } from 'react'; const App = () => { const { user } = useUser(); return ( <>

Hello {user.name}

); }; ``` ```javascript import createSdk from '../src/index'; { ... } const sdk = createSdk({ projectId: "__ProjectID__"}); await sdk.me(); ``` ```javascript ``` ```javascript import { Component, OnInit } from '@angular/core'; import { DescopeAuthService } from '@descope/angular-sdk'; ... export class AppComponent implements OnInit { ... constructor(private authService: DescopeAuthService) {} ngOnInit() { ... this.authService.user$.subscribe((descopeUser) => { if (descopeUser.user) { this.userName = descopeUser.user.name ?? ''; } }); } } ``` ## Refresh Session In the case that the browser has a valid refresh token on storage/cookie, the user should get a valid session token (i.e. user should be logged-in). For this reason, it is common to call the refresh function after sdk initialization. Refresh returns a session token, so if `autoRefresh` is set to true, the sdk will automatically continue to refresh the token. useDescope is a React hook that retrieves the Descope SDK for further operations related to authentication. This includes the refresh operation, as shown in the React and Next.js examples below. `useSession` triggers a single request to the Descope backend to attempt to refresh the session. If you don't useSession on your app, the session will not be refreshed automatically. If your app does not require useSession, you can trigger the refresh manually by calling refresh from the useDescope hook: ```javascript import { useSession, useUser, useDescope } from '@descope/react-sdk'; import { useCallback } from 'react'; const App = () => { const { isAuthenticated, isSessionLoading } = useSession(); const { refresh } = useDescope(); useEffect(() => { refresh(); }, [refresh]); ``` ```javascript import { useSession, useUser, useDescope } from '@descope/nextjs-sdk/client'; import { useCallback } from 'react'; const App = () => { const { isAuthenticated, isSessionLoading } = useSession(); const { refresh } = useDescope(); useEffect(() => { refresh(); }, [refresh]); ``` ```javascript import createSdk from '../src/index'; let descopeSdk = createSdk({projectId: "__ProjectID__"}); await descopeSdk.refresh(); ``` ```javascript ``` ```javascript import { DescopeAuthModule, DescopeAuthService } from '@descope/angular-sdk'; ... export function initializeApp(authService: DescopeAuthService) { return () => zip([authService.refreshSession(), authService.refreshUser()]); } ``` ## Refresh User Data in JWT The [`useUser` and `me` functions](#manage-user) not only return user information but also refresh the user details stored in the JWT token. Calling `refresh()` updates the session token returned by `useSession()`, but does not re-fetch the user object returned by `useUser()`. When user attributes are updated on the backend (such as through SCIM, admin operations, or other user management processes), call `me()` to retrieve the latest user details. When your application needs both the latest session-token claims and the latest user object, call `refresh()` followed by `me()`. ### Example Use Cases ```javascript import { useUser } from '@descope/react-sdk'; const UserProfile = () => { const { user, isUserLoading } = useUser(); // This will show updated user data after SCIM provisioning // or admin changes, even without re-login return (

Welcome, {user.name}

Email: {user.email}

Department: {user.customAttributes?.department}

); }; ``` ```javascript 'use client'; import { useUser } from '@descope/nextjs-sdk/client'; const UserProfile = () => { const { user, isUserLoading } = useUser(); // This will show updated user data after SCIM provisioning // or admin changes, even without re-login return (

Welcome, {user.name}

Email: {user.email}

Department: {user.customAttributes?.department}

); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: "__ProjectID__" }); // Call this after user attributes are updated elsewhere const refreshUserData = async () => { try { const updatedUser = await sdk.me(); console.log('User data refreshed:', updatedUser); // JWT now contains the latest user attributes } catch (error) { console.error('Failed to refresh user data:', error); } }; ``` ```javascript ``` ## Logout Logs out the currently authenticated user. This method invalidates the user's current JWT tokens and ends their session. This function is typically used when the user chooses to log out of your application. useDescope is a React hook that retrieves the Descope SDK for further operations related to authentication. This includes the logout operation, as shown in the React, Next.js, and Vue examples below. ```javascript import { useDescope } from '@descope/react-sdk'; import { useCallback } from 'react'; const App = () => { const { logout } = useDescope(); const handleLogout = useCallback(() => { logout(); }, [logout]); return ( <> ); }; ``` ```javascript import createSdk from '../src/index'; let descopeSdk = createSdk({projectId: "__ProjectID__"}); await descopeSdk.logout(); ``` ```javascript import descope from '@descope/vue-sdk'; ``` ```javascript import { useDescope } from '@descope/nextjs-sdk/client'; import { useCallback } from 'react'; const App = () => { const { logout } = useDescope(); const handleLogout = useCallback(() => { logout(); }, [logout]); return ( <> ); }; ``` ```javascript import { DescopeAuthService } from '@descope/angular-sdk'; ... constructor(private authService: DescopeAuthService) {} ... ngOnInit() { ... logout() { this.authService.descopeSdk.logout(); } } ``` ## Logout All This will sign the user out of all the devices they are currently signed-in with. Successfully executing this endpoint will invalidate all user's refresh tokens. Response will include all user tokens and fields empty, so client will remove cookies as well. useDescope is a React hook that retrieves the Descope SDK for further operations related to authentication. This includes the logoutAll operation, as shown in the React, Next.js, and Vue examples below. ```javascript import { useDescope } from '@descope/react-sdk'; import { useCallback } from 'react'; const App = () => { const { logoutAll } = useDescope(); const handleLogout = useCallback(() => { logoutAll(); }, [logoutAll]); return ( <> ); }; ``` ```javascript import { useDescope } from '@descope/nextjs-sdk/client'; import { useCallback } from 'react'; const App = () => { const { logoutAll } = useDescope(); const handleLogout = useCallback(() => { logoutAll(); }, [logoutAll]); return ( <> ); }; ``` ```javascript import createSdk from '../src/index'; let descopeSdk = createSdk({projectId: "__ProjectID__"}); await descopeSdk.logoutAll(); ``` ```javascript import descope from '@descope/vue-sdk'; ``` ```javascript import { DescopeAuthService } from '@descope/angular-sdk'; ... constructor(private authService: DescopeAuthService) {} ... ngOnInit() { ... logoutAll() { this.authService.descopeSdk.logoutAll(); } } ``` # Client SDK Reference (/client-sdk) Use Descope Client SDKs to add authentication to your app with secure session management. Supports JavaScript, React, Web Components, and more. # Client SDK Reference This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. ## Descope Client SDKs This is a list of the current web client SDKs we support. } title="Web-JS SDK" description="Vanilla JavaScript SDK for adding authentication to web applications." href="https://github.com/descope/descope-js/tree/main/packages/sdks/web-js-sdk" /> } title="React SDK" description="React SDK with hooks and components for building authentication flows." href="https://github.com/descope/descope-js/tree/main/packages/sdks/react-sdk" /> } title="Vue SDK" description="Vue.js SDK with composables and components for authentication." href="https://github.com/descope/descope-js/tree/main/packages/sdks/vue-sdk" /> } title="Angular SDK" description="Angular SDK with services and modules for handling authentication." href="https://github.com/descope/descope-js/tree/main/packages/sdks/angular-sdk" /> } title="Next.js SDK" description="Next.js SDK with middleware and helpers for protecting routes and APIs." href="https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk" /> ## Authentication Methods Since you are not using Descope Flows, your application must implement the authentication flows and error handling in this approach. The authentication guides below contain step-by-step implementation of different authentication methods using Descope Client SDKs. Each guide will direct you through the steps you need to follow irrespective of your chosen language and framework. The guides also contain sample code for all the languages and frameworks supported by Descope. | Authentication Method | Description | Guide | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | One Time Password (OTP) | A single-use code that grants a user access to your application. Descope supports OTP sent via SMS and email today. | | | Magic Link | A single-use link sent to a user's email address or phone via sms that grants them access to your application. | | | Enchanted Link | An enhanced version of magic link that enables a user to login by clicking a link on a different device. | | | Social Login (OAuth) | Enable users to access your application by using identities they have created on other applications (Google, Twitter, LinkedIn, etc.). | | | Authenticator Apps | Enable users to access your application by using time-based numeric codes generated by apps like Google Authenticator and Authy. | | | Biometrics (WebAuthn) | Enable users to access your application by using biometrics built into their devices (fingerprint scanning, facial recognition, security keys).| | | Single Sign On (SSO/SAML) | Enable users to access your B2B application using single sign-on with identity providers like Google, Microsoft, and Okta. | | | Passwords | Enable users to access your application using passwords. | | | No One Time Password (nOTP) | Enable users to access to your application by sending a verification code via WhatsApp, allowing users to log in without SMS or email. | | ## Client SDK Sample Apps You can reference the sample applications below to familiarize yourself with the different methods and quickly start your client-side implementation. | Language | GitHub Location | | ------------------------- | -------------------------------- | | HTML/Web-Component/Web-JS | | | React | | | React | | | Angular/Webjs | | | Flask/React | | | NextAuth | | | Nextjs | | ## Automatic Retries The Client SDK automatically retries API calls that fail with certain transient error codes covering service unavailability and Cloudflare transient errors. | HTTP Error Code | Description | | --- | --- | | `503` | Service Unavailable | | `521` | Cloudflare — Web Server Is Down | | `522` | Cloudflare — Connection Timed Out | | `524` | Cloudflare — A Timeout Occurred | | `530` | Cloudflare — Site Frozen / Origin DNS Error | The SDK will attempt up to **3 retries**. The first retry fires after **100ms**; each subsequent retry fires after **5 seconds**. # Initialize SDK (/client-sdk/initialize-sdk) Learn how to initialize Descope Client sdks # Initialize SDK # Backend SDK (/backend-sdk) Use Descope Backend SDKs to add authentication to your app with your own developed backend APIs. # Backend SDK Reference This guide is meant for developers who are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. Descope's backend SDKs provide two main functionalities: 1. **Authentication**: Implementing sign-up, sign-in, and other authentication flows 2. **Session Management**: Validating and managing user sessions This page focuses on authentication use-cases. For session management and validation, see [Sessions](/sessions). ## Descope Backend SDKs This is a list of the current backend SDKs we support. } title="Node SDK" description="Node.js SDK for building authentication APIs and session management." href="https://github.com/descope/node-sdk" /> } title="Python SDK" description="Python SDK for building authentication APIs and session management." href="https://github.com/descope/python-sdk" /> } title="Go SDK" description="Go SDK for building authentication APIs and session management." href="https://github.com/descope/go-sdk" /> } title="Java SDK" description="Java SDK for building authentication APIs and session management." href="https://github.com/descope/descope-java" /> } title="Ruby SDK" description="Ruby SDK for building authentication APIs and session management." href="https://github.com/descope/descope-ruby-sdk" /> } title="PHP SDK" description="PHP SDK for building authentication APIs and session management." href="https://github.com/descope/descope-php" /> } title=".NET SDK" description=".NET SDK for building authentication APIs and session management." href="https://github.com/descope/descope-dotnet" /> For diagnosing failed API calls, session validation errors, and configuration problems, see [Logging](/backend-sdk/logging). ## Authentication Methods Since you are not using Descope Flows, your application must implement the authentication flows and error handling in this approach. The authentication guides below contain step-by-step implementation of different authentication methods using Descope Backend SDKs. Each guide will direct you through the steps you need to follow irrespective of your chosen language and framework. The guides also contain sample code for all the languages and frameworks supported by Descope. | Authentication Method | Description | Guide | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | One Time Password (OTP) | A single-use code that grants a user access to your application. Descope supports OTP sent via SMS and email today. | | | Magic Link | A single-use link sent to a user's email address or phone via sms that grants them access to your application. | | | Enchanted Link | An enhanced version of magic link that enables a user to login by clicking a link on a different device. | | | Social Login (OAuth) | Enable users to access your application by using identities they have created on other applications (Google, Twitter, LinkedIn, etc.). | | | Authenticator Apps | Enable users to access your application by using time-based numeric codes generated by apps like Google Authenticator and Authy. | | | Biometrics (WebAuthn) | Enable users to access your application by using passkeys built into their devices (fingerprint scanning, facial recognition, security keys). | | | Single Sign On (SSO/SAML) | Enable users to access your B2B application using single sign-on with identity providers like Google, Microsoft, and Okta. | | | No One Time Password (nOTP) | Enable users to access to your application by sending a verification code via WhatsApp, allowing users to log in without SMS or email. | ## Backend SDK Sample Apps You can reference the sample applications below to familiarize yourself with the different authentication functions and quickly start your implementation. | Language | GitHub Location | | ------------------------- | -------------------------------- | | Flask-React | Flask | | Flask | Flask | | Django | Django | | Nodejs | Nodejs | | Python | Python | # Logging (/backend-sdk/logging) Enable and configure logging in the Descope Go, Ruby, Node.js, and PHP backend SDKs. # Logging The Descope backend SDKs can emit log messages to help you diagnose failed API calls, session validation errors, and configuration problems. Support differs by language, and the SDKs do not share a common shape. Some take a log level, some take a logger object you supply. | SDK | What you set | Log levels | | ----------- | -------------------------------------------------- | ----------------------------------------------------------------------- | | **Node.js** | A `logger` object | None, so you filter inside the logger you supply | | **Go** | `LogLevel`, and optionally a `Logger` of your own | `logger.LogNone`, `logger.LogInfoLevel`, and `logger.LogDebugLevel` | | **Ruby** | `log_level`, and optionally a `logger` of your own | `debug`, `info`, `warn`, `error`, `fatal`, and `unknown` | | **PHP** | `debug` to `true` or `false` | None | ## Enabling a Logger The Node.js SDK takes a `logger` object rather than a log level. Pass it alongside `projectId`: ```javascript import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: '__ProjectID__', logger: console, }); ``` During development you can pass `console` directly. To route messages into your own logging service, supply an object with `debug`, `log`, `warn`, and `error` methods: ```javascript const logger = { debug: (message, ...args) => myLogger.debug(message, ...args), log: (message, ...args) => myLogger.info(message, ...args), warn: (message, ...args) => myLogger.warn(message, ...args), error: (message, ...args) => myLogger.error(message, ...args), }; const descopeClient = DescopeClient({ projectId: '__ProjectID__', logger, }); ``` All four methods are required, which is why passing `console` works. The SDK calls `error` for failures, `log` for informational messages, and `warn` for non-fatal problems. Messages are emitted for session validation failures, refresh token validation failures, access key exchange failures, public key parsing errors, and license handshake problems. The Go SDK takes a log level, and optionally a logger of your own. Set them on `client.Config`: ```go import ( "github.com/descope/go-sdk/descope/client" "github.com/descope/go-sdk/descope/logger" ) descopeClient, err := client.NewWithConfig(&client.Config{ ProjectID: "__ProjectID__", LogLevel: logger.LogDebugLevel, }) ``` ### Supported Log Levels - `logger.LogNone` logs nothing. This is the default. - `logger.LogInfoLevel` logs errors and informational messages. - `logger.LogDebugLevel` logs errors, informational messages, and debug detail. ### Supplying Your Own Logger If you leave `Logger` unset, the SDK writes to Go's `log.Default()`. To send messages elsewhere, provide any type with a `Print(v ...any)` method: ```go type myLogger struct{} func (myLogger) Print(v ...any) { // forward to your logging library } descopeClient, err := client.NewWithConfig(&client.Config{ ProjectID: "__ProjectID__", LogLevel: logger.LogDebugLevel, Logger: myLogger{}, }) ``` The Ruby SDK takes a log level. Unlike every other Descope SDK, **it logs by default at `info`**: ```ruby require 'descope' client = Descope::Client.new( project_id: '__ProjectID__', log_level: 'debug' ) ``` Log messages go to `STDOUT`, prefixed with the timestamp, severity, and your Project ID: ```text [2026-01-14 09:22:41 UTC] INFO PRID: __ProjectID__ Descope::Client: Initializing Descope API ``` ### Supported Log Levels `log_level` accepts the names of Ruby's standard `Logger` levels: `debug`, `info`, `warn`, `error`, `fatal`, and `unknown`. An unrecognized value raises a `NameError`, so keep to that list. The level is resolved in this order: 1. The `log_level` option passed to `Descope::Client.new` 2. The `DESCOPE_LOG_LEVEL` environment variable 3. `info`, if neither is set To minimize log output, override the default by setting the level explicitly: ```ruby client = Descope::Client.new( project_id: '__ProjectID__', log_level: 'error' ) ``` ### Supplying Your Own Logger Passing a `logger` takes over completely. When you supply one, `log_level` is ignored and the level is whatever your logger is configured with: ```ruby require 'logger' client = Descope::Client.new( project_id: '__ProjectID__', logger: Logger.new($stdout, level: Logger::WARN) ) ``` The PHP SDK takes a boolean. Set `debug` in the config array you pass to `DescopeSDK`: ```php require 'vendor/autoload.php'; use Descope\SDK\DescopeSDK; $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], 'debug' => true, ]); ``` There are no levels. When `debug` is on, a failed `POST`, `GET`, or `DELETE` request to the Descope API is written to PHP's error log through `error_log()`: ```text [04-Aug-2026 18:47:06 UTC] Descope SDK [POST] Error: HTTP Status Code: 400, Response: {"errorCode":"E061102","errorDescription":"One time code is invalid"} ``` Messages are prefixed with `Descope SDK` and tagged with the HTTP method, so you can filter for them. Because they go through `error_log()`, they land wherever your PHP installation sends error output: the path set by `error_log` in your `php.ini`, your web server's error log. ### Enabling It Without Changing Code You can also turn debug logging on with the `DESCOPE_DEBUG` environment variable, most reliably from a `.env` file: ```ini title=".env" DESCOPE_DEBUG=true ``` The value must be the exact lowercase string `true`. `1`, `TRUE`, and `on` are ignored. The SDK reads this from PHP's `$_ENV` superglobal, so a variable exported only into the process environment is not picked up unless your `php.ini` `variables_order` includes `E`, which it does not by default. Setting `debug` in the config array always takes precedence over the environment variable. ## Additional Resources - [Error Handling in SDKs](/sdk-error-handling) - [Common Errors](/common-errors) - [Logging in the Mobile SDKs](/mobile-sdk/logging) - [Generate Debug Info for Support](/support/generate-debug-info) # Auth Helpers (/mobile-sdk/auth-helpers) Learn how the Auth class in Descope SDK handles user authentication operations within your mobile application. # Mobile SDK Auth Helpers The Auth class is a crucial component of the Descope SDK, handling key user authentication operations. This class is designed to execute essential functions such as fetching user details, fetching and refreshing sessions, and logging out users. ## Mobile SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK Parameters: - `baseUrl`: Custom domain that must be configured to manage token response in cookies. This makes sure every request to our service is through your custom domain, preventing accidental domain blockages. ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## Manage User Retrieve information about the current authenticated user. These methods are used when you need to fetch or display user-related data in your application. It requires the `refreshJwt` as an argument which is the current refresh token of the user. For Swift, Kotlin, and Flutter, you will use the me method, and for React Native you will use the useDescope hook. This function returns the following details: - `email`: Email address associated to the user. - `name`: Name associated to the user. - `givenName`: Given name associated to the user. - `middleName`: Middle name associated to the user. - `familyName`: Family name associated to the user. - `phone`: Phone number associated to the user. - `loginIds`: An array of loginIds associated to the user. - `userId`: The user's unique Descope generated userId. - `verifiedEmail`: Boolean whether the email address for the user has been verified. - `verifiedPhone`: Boolean whether the phone number for the user has been verified. - `picture`: The base64 encoded image if the user has an image associated to them. - `createTime` **(React Native)** / `createdAt` **(Kotlin, Flutter, Swift)**: The time that the user was created. Additional details returned by some SDKs: - `totp` **(React Native)**: Boolean whether the user has TOTP login associated with it. - `saml` **(React Native)**: Boolean whether the user has SAML login associated with it. - `oauth` **(React Native)**: Boolean whether the user has OAuth login associated with it. - `userTenants` **(React Native)**: An array of tenant names and IDs associated to the user. - `roleNames` **(React Native, Flutter)**: An array of roles associated to the user. - `status` **(React Native, Flutter)**: The user's status, one of 'enabled', 'disabled' or 'invited'. - `hasPassword` **(Flutter)** / `password` **(React Native)**: Boolean whether the user has a password set or not. - `ssoAppIds` **(Flutter)**: A list of SSO App IDs the user is associated with. - `oauthProviders` **(Flutter)**: A list of OAuth providers the user has used. ```swift guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } try await Descope.auth.me(refreshJwt: refreshJwt) ``` ```kotlin Descope.sessionManager.session?.refreshJwt?.run { Descope.auth.me(this) } ``` ```dart final refreshJwt = Descope.sessionManager.session?.refreshJwt; Descope.auth.me(refreshJwt!); ``` ```javascript import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { updateUser } = useSession() const userResponse = await descope.me(session.refreshJwt) ``` ## Refresh Session Refreshes the session of the currently authenticated user. This method is useful when the current user session is about to expire or has expired. By calling this method, you can ensure that the user remains authenticated. This function takes the current `refreshJwt` as an argument. ```swift guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } try await Descope.auth.refreshSession(refreshJwt: refreshJwt) ``` ```kotlin Descope.sessionManager.session?.refreshJwt?.run { Descope.auth.refreshSession(this) } ``` ```dart final refreshJwt = Descope.sessionManager.session?.refreshJwt; Descope.auth.refreshSession(refreshJwt); ``` ```javascript import { useDescope, useSession, useContext } from '@descope/react-native-sdk' const { sdk, session } = useContext() await sdk.refresh(session.refreshJwt) ``` ## Logout Logs out the currently authenticated user. This method invalidates the user's current JWT tokens and ends their session. This function is typically used when the user chooses to log out of your application. The function takes the current `refreshJwt` as an argument. ```swift guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } try await Descope.auth.logout(refreshJwt: refreshJwt) Descope.sessionManager.clearSession() ``` ```kotlin Descope.sessionManager.session?.refreshJwt?.run { Descope.auth.logout(this) Descope.sessionManager.clearSession() } ``` ```dart final refreshJwt = Descope.sessionManager.session?.refreshJwt; if (refreshJwt != null) { await Descope.auth.logout(refreshJwt); Descope.sessionManager.clearSession(); } ``` ```javascript import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { session } = useSession() await descope.logout(session.refreshJwt) ``` # Mobile SDK Reference (/mobile-sdk) Use Descope Mobile SDKs to add authentication to your app with secure session management. Supports Swift. # Mobile SDK Reference This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. ## Descope Mobile SDKs This is a list of all of the Mobile SDKs we currently support. } title="Swift SDK" description="Native iOS SDK for adding authentication to Swift applications." href="https://github.com/descope/descope-swift" /> } title="Flutter SDK" description="Cross-platform SDK for building authentication in Flutter apps." href="https://github.com/descope/flutter-sdk" /> } title="React Native SDK" description="React Native SDK for adding authentication to cross-platform mobile apps." href="https://github.com/descope/descope-react-native" /> } title="Kotlin SDK" description="Native Android SDK for adding authentication to Kotlin applications." href="https://github.com/descope/descope-kotlin" /> Frameworks without a native Descope Mobile SDK can integrate with Descope using standard OIDC endpoints. For example, check out [this sample application](https://github.com/descope-sample-apps/dotnet-maui-sample-app) to see how Descope can be integrated into .NET MAUI applications using standard OIDC with the Authorization Code flow and PKCE. ## Authentication Methods Descope enables you to add authentication to your mobile application. The authentication guides below contain step-by-step implementation of different authentication methods using Descope Mobile SDKs. Each guide will direct you through the steps you need to follow irrespective of your chosen language and framework. The guides also contain sample code for all the languages and frameworks supported by Descope. | Authentication Method | Description | Guide | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | | One Time Password (OTP) | A single-use code that grants a user access to your application. Descope supports OTP sent via SMS and email today. | | | Magic Link | A single-use link sent to a user's email address or phone via sms that grants them access to your application. | | | Enchanted Link | An enhanced version of magic link that enables a user to login by clicking a link on a different device. | | | Social Login (OAuth) | Enable users to access your application by using identities they have created on other applications (Google, Twitter, LinkedIn, etc.). | | | Authenticator Apps | Enable users to access your application by using time-based numeric codes generated by apps like Google Authenticator and Authy. | | | Biometrics (WebAuthn) | Enable users to access your application by using passkeys built into their devices (fingerprint scanning, facial recognition, security keys). | | | Single Sign On (SSO/SAML) | Enable users to access your B2B application using single sign-on with identity providers like Google, Microsoft, and Okta. | | ## Mobile SDK Sample Apps You can reference the sample applications below to familiarize yourself with the different methods and quickly start your client-side implementation. | Language | GitHub Location | | ------------------------- | -------------------------------- | | Swift | | | Flutter (flows) | | | Flutter (without flows) | | ## Debugging and Testing The Mobile SDKs include features to help you diagnose problems while integrating, and to keep the SDK out of the way of your test suite. | Topic | Description | Guide | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | | Logging | Turn on SDK logging to diagnose authentication, session, and flow issues, or route Descope log messages into your own logging service. | | | Network Client | Replace how the SDK performs HTTP requests, so tests make no real network calls or Descope requests reuse your application's existing HTTP stack. | | # Logging (/mobile-sdk/logging) Enable and configure logging in the Descope Swift, Kotlin, Flutter, and React Native SDKs. # Logging The Descope mobile SDKs can emit log messages to help you diagnose authentication, session, and flow issues during development. Logging is **disabled by default**. No output is produced until you explicitly set a logger on the SDK configuration. ## Built-in Loggers The Swift, Kotlin, and Flutter SDKs each ship three built-in loggers. They differ in how much they print, and in whether they print **unsafe runtime values** — full network request and response payloads, tokens, secrets, and personal information. | Logger | Prints | Unsafe values | | -------------- | ---------------------- | ------------------------------------------------- | | `basicLogger` | Errors and info | Never | | `debugLogger` | Errors, info and debug | Only when the app is running in a debug context | | `unsafeLogger` | Errors, info and debug | Always | `debugLogger` is the right choice in most cases. You can add it while diagnosing an issue, and it will not leak sensitive data if you forget to remove it before shipping a build to the App Store or Play Store. The debug context that `debugLogger` detects differs between platforms. On Swift it means a debugger is attached to the process, so the app was launched from Xcode. On Kotlin it means the application is marked debuggable, whether or not Android Studio is attached. On Flutter it means the app was compiled in debug mode, so unsafe values are never printed in profile or release builds. React Native does not use these built-in loggers - see the React Native tab under [Enabling a Logger](#enabling-a-logger) below. Even with unsafe values off, error logs stay actionable - the Descope error code and API route are part of the message itself. For example, a failed OTP verification on Android prints a log line like this: ```text [DescopeAndroid] Network call to auth/otp/verify/email failed with E061102 server error ``` Each SDK prefixes its log lines with its own SDK name: `[DescopeKit]` on Swift, `[DescopeAndroid]` on Kotlin, and `[DescopeFlutter]` on Flutter. ## Log Levels Every log message carries one of three severities: | Level | Used for | | ------- | ------------------------------------------------------------------------------------ | | `error` | Failures that stopped an operation, such as a network error or a timed-out enchanted link | | `info` | Normal lifecycle milestones, such as a network call starting or finishing | | `debug` | Fine-grained detail useful when tracing a problem, such as individual polling attempts | A logger prints messages at its configured level **and everything more severe**. A logger set to `info` prints error and info messages but drops debug ones, which is exactly what `basicLogger` does. How the level is represented differs by platform: `DescopeLogger.Level` is an enum. ```swift DescopeLogger.Level.error DescopeLogger.Level.info DescopeLogger.Level.debug ``` `DescopeLogger.Level` is an enum. ```kotlin DescopeLogger.Level.Error DescopeLogger.Level.Info DescopeLogger.Level.Debug ``` Flutter has no enum. The levels are integer constants on `DescopeLogger`, ordered from most to least severe. ```dart DescopeLogger.error // 0 DescopeLogger.info // 1 DescopeLogger.debug // 2 ``` This matters when you set the `level` property yourself, since it takes an `int` rather than an enum value. React Native has no level type. Your logger object exposes a separate method per severity, and the SDK calls the one that matches the message. See the React Native tab under [Enabling a Logger](#enabling-a-logger). ## Enabling a Logger ```swift Descope.setup(projectId: "__ProjectID__") { config in config.logger = .debugLogger } ``` ```kotlin Descope.setup(this, projectId = "__ProjectID__") { logger = DescopeLogger.debugLogger } ``` ```dart Descope.setup('__ProjectID__', (config) { config.logger = DescopeLogger.debugLogger; }); ``` On Flutter, log messages from the underlying native iOS and Android SDKs are forwarded to your logger automatically once one is set, so native flow execution appears alongside your Dart logs. Your logger's `unsafe` setting is passed through to the native layer, so `unsafeLogger` also captures native payloads. The React Native SDK takes a logger object rather than one of the built-in loggers above. Pass it to `AuthProvider`: ```jsx const logger = { log: (message) => console.log(message), debug: (message) => console.debug(message), warn: (message) => console.warn(message), error: (message) => console.error(message), } ``` During development you can pass `console` directly, or point the methods at a monitoring service instead. The bridge forwards log messages from the native iOS and Android SDKs to this logger, so native flow execution shows up alongside your JavaScript logs. That helps when a flow loads but never completes. Native `info` messages arrive as log. The bridge always runs with unsafe logging disabled, so it never prints native payloads or tokens. React Native has no equivalent of `unsafeLogger`. ## What Gets Logged Once a logger is set, the SDK reports on the operations below. Not every area is covered on every platform, because Flutter and React Native run parts of the authentication natively and those messages arrive through the native log bridge rather than from the Dart or JavaScript layer. | Area | What you'll see | | ------------------------ | ------------------------------------------------------------------------------------------ | | Network requests | Every call starting and finishing, plus server, HTTP and network failures with their codes | | Session refresh | Refreshes triggered by an expiring token, skipped refreshes, and periodic refresh outcomes | | Flow execution | Flow start, ready, success and failure, resume URLs, and native OAuth or web authentication | | Enchanted link polling | Polling start, each wait, success, and timeout | | Persisted session lookup | Whether a stored session was found at startup | ## Custom Loggers The built-in loggers print to the console, which is fine while you are working locally. If you want to route Descope log messages into your own logging framework or a third party monitoring service, provide your own logger instead. On Swift, Kotlin, and Flutter you do this by subclassing `DescopeLogger` and overriding its `output` method. The base class handles level filtering and unsafe-value filtering for you, then hands the surviving messages to `output`. You also choose the level and unsafe behavior yourself by passing them to the initializer, rather than inheriting the fixed combination that a built-in logger uses. ```swift Descope.setup(projectId: "__ProjectID__") { config in config.logger = RemoteDescopeLogger() } // elsewhere class RemoteDescopeLogger: DescopeLogger { init() { super.init(level: .info, unsafe: false) } override func output(level: Level, message: String, unsafe values: [Any]) { RemoteLogger.sendLog("Descope: \(message)") } } ``` ```kotlin Descope.setup(this, projectId = "__ProjectID__") { logger = RemoteDescopeLogger() } // elsewhere class RemoteDescopeLogger : DescopeLogger(level = Level.Info, unsafe = false) { override fun output(level: Level, message: String, values: List) { RemoteLogger.sendLog("Descope: $message") } } ``` ```dart Descope.setup('__ProjectID__', (config) { config.logger = RemoteDescopeLogger(); }); // elsewhere class RemoteDescopeLogger extends DescopeLogger { RemoteDescopeLogger() : super(level: DescopeLogger.info, unsafe: false); @override void output({required int level, required String message, required List values}) { RemoteLogger.sendLog('Descope: $message'); } } ``` To override how the SDK performs HTTP requests rather than how it logs them, see [Network Client](/mobile-sdk/network-client). # Native Flows (/mobile-sdk/native-vs-browser-flows) Learn how native flows work with Descope on mobile. # Native Flows While many other Auth Providers rely on opening external web pages for authentication, Descope's **Native Flows** elevate the user experience by embedding authentication flows directly into your mobile app, creating a seamless experience that feels fully native. Native flows require a flow to be hosted within a web application (either by using our [Auth Hosting app](/identity-federation/auth-hosting) or a self-hosted page), but integrate that content into your app via a webview. This preserves the flexibility and scalability of hosted authentication, while delivering a polished, in-app experience that feels fully native. ## How Native Flows Work **Native flows** provide an integrated authentication experience: 1. **Running Hosted Flow**: The authentication flow is served remotely for secure and stable hosting without extra infrastructure on your part. 2. **Embedded via Webview**: Instead of redirecting users to an external browser, the hosted authentication page is displayed inside a webview, making it feel like a natural part of your app. 3. **Enhanced Control & Customization**: You can preload flows, add custom animations, and control the entire transition within the native app environment, leveraging the flexibility of hosted pages without sacrificing control. 4. **Seamless UX**: Users remain inside the app, maintaining consistent branding and style. The authentication step feels integrated rather than an external detour. ### Different Native Flows Below are general approaches you might choose with Descope's mobile SDK. Each still uses a hosted authentication page displayed in a webview, ensuring a cohesive experience: ![native flow experiences](/assets/native-flow-experiences.webp) - **Simple Flow**: Pushes a `DescopeFlowViewController` onto your navigation stack, immediately presenting the hosted flow in a full screen native webview. - **Modal Flow**: Preloads a `DescopeFlowViewController`, so when the user initiates sign-in, the flow appears in a modal instantly. This creates a smooth, uninterrupted experience. This also does not fill the entire screen like the simple flow does, and mimicks the browser modal a bit more, without the annoying browser related buttons. - **Inline Flow**: Integrates a `DescopeFlowView` directly into your app's view hierarchy. This approach allows for fully custom animations and transitions, making the flow feel like an organic part of your UI. All these approaches keep the user within your app's environment rather than switching to an external browser, ensuring consistent branding, better control, and a smoother user experience. ## Touch Interactions Because a native flow renders inside a webview, it would otherwise carry over touch behaviors that belong to a web browser rather than a native app. To keep the flow feeling native, the mobile SDKs automatically suppress these interactions as soon as the flow is ready. Some examples of these touch behaviors include: - **Text selection is disabled:** Users can't drag-select or highlight text within the flow. - **The long press callout is suppressed on iOS:** Long pressing a link or an image no longer opens the system menu with *Copy*, *Look Up*, and *Share* actions. - **Links and images can't be dragged:** Links inside text components, along with any images on the screen, are marked as non-draggable so they can't be dragged out of the flow. Tapping a link is unaffected and continues to work as expected. ### Disabling with SDKs The rules differ per platform, since the properties that control these behaviors are platform-specific: ```css #content-root * { -webkit-touch-callout: none; -webkit-user-select: none; } ``` The rules are scoped to the flow's content root, so they affect the flow itself and nothing else on the page. ```css /* applied to the hosted page */ * { user-select: none; } /* applied within the flow's content root */ #content-root * { user-select: none; } ``` On Android the SDK also adds a page-level rule, so text selection is disabled across the entire hosted page rather than only inside the flow. Keep this in mind if you self-host your flow page and render your own content around the flow. When using Native Flows, text selection is typically disabled. If any screen in your flow displays a value a user might need to copy, such as an OTP or string shown in a read-only input, you should enable the [Allow copying value](/flows/screens/inputs#allow-copying-value) setting on that input component. This renders a copy icon next to the field, giving users a way to copy the value without relying on text selection. ## Authentication Methods with Native Flows Native Flows work seamlessly with various authentication methods. Native OAuth, Passkeys, and Magic Link all integrate smoothly with Native Flows, though they require additional configuration to set up properly. nOTP is not currently supported on [Native Flows](/mobile-sdk/native-vs-browser-flows) on mobile. To use nOTP on mobile, you must use browser-based [flows](/flows). - **[OAuth](/auth-methods/oauth/with-sdks/mobile)**: Social login providers like Google, Facebook, and Apple can be integrated with Native Flows. The OAuth handshake is handled securely while maintaining the native app experience. - **[Passkeys](/auth-methods/passkeys/with-sdks/mobile)**: Biometric authentication and passkeys work seamlessly with Native Flows, providing a secure and user-friendly authentication experience. On Android, an app that hosts a flow in its own WebView may also need to register its signing fingerprint under [Passkeys Settings](/auth-methods/passkeys/settings). - **[Magic Link](/auth-methods/magic-link/with-sdks/mobile)**: Magic link authentication integrates with Native Flows, allowing users to authenticate via email or SMS links while staying within your app. For methods that inherently rely on browser interactions, Descope's Native Flow handles these gracefully. Under the hood, it may open a secure, controlled browser session that fits naturally into the native flow, preserving a unified look and feel. ## Managing Sessions and Authentication State Beyond the flow itself, proper session management ensures that users remain seamlessly authenticated without repeatedly entering credentials. ### Cookie-Based Token Response with a Custom Domain If your project is configured to manage tokens in cookies — [session tokens](/security-best-practices/session-token-storage#managing-with-cookies) and/or [refresh tokens](/security-best-practices/refresh-token-storage#handling-refresh-tokens-in-cookies) — with a [custom domain](/how-to-deploy-to-production/custom-domain), native flows receive those JWTs as cookies set inside the webview instead of in the authentication response body. Because cookies are scoped to a specific domain and path, the SDK cannot assume the tokens live on the exact domain serving the flow. To resolve this, the mobile SDK extracts the cookie location directly from the authentication response to locate the tokens: - `cookieDomain`: The domain the authentication cookies are scoped to. - `cookiePath`: The path the authentication cookies are scoped to. The SDK resolves cookies by checking the specified domain and path first, then falling back to the flow's URL if no location is configured. Because of this fallback, the flow's host page is not required to match the exact domain your cookies are scoped to. For example, a flow served from [Auth Hosting](/identity-federation/auth-hosting) can successfully hand off tokens scoped to `auth.example.com`. If you set a custom **Refresh Cookie Name** or **Session Cookie Name** in your flow's [End action](/flows/actions/end-action), native flows honor it automatically - no additional SDK configuration is required. To use this feature, ensure the following are configured: 1. Enable **Token Response Methods** for cookies on the [Session Management](https://app.descope.com/settings/project/session) page, following the steps in the [custom domain guide](/how-to-deploy-to-production/custom-domain#manage-tokens-in-cookies). 2. During SDK initialization, set the `baseUrl` to route requests through your custom domain, this ensures refresh cookies are sent correctly during session refreshes. See [Mobile SDK Auth Helpers](/mobile-sdk/auth-helpers#import-and-initialize-sdk) for details. ### iOS with ASWebAuthenticationSession On iOS, **ASWebAuthenticationSession** can enhance session management in scenarios that require browser-based capabilities (such as OAuth). It provides a secure environment to handle authentication sessions while still allowing the app to control aspects like timeouts, tokens, and redirects. When combined with Native Flows, it ensures that even browser-dependent authentication methods (like certain OAuth providers) integrate smoothly without forcing a full browser context switch. This means your iOS app can benefit from secure, system-level session handling while still presenting a native and cohesive UX. ### Android with DescopeSessionManager On Android, Descope offers the `DescopeSessionManager` class to manage authenticated user sessions. It takes care of loading and saving session data, securely storing it with `EncryptedSharedPreferences`, and refreshing sessions before they expire. By initializing Descope with your `applicationContext` and managing sessions with `DescopeSessionManager`, you can ensure that users stay signed in across app launches and have their tokens refreshed automatically. By leveraging `DescopeSessionManager`, you get: - **Auto-Refresh**: Sessions refresh automatically before expiration. - **Secure Storage**: Sessions and tokens are stored in encrypted preferences. - **Persistent Auth State**: The user remains signed in across app restarts, and you can easily revoke or clear sessions when they sign out. This seamless integration of session management helps you deliver a stable, user-friendly authentication experience without manual token handling. ## Conclusion Descope's Native Flows provide the best of both worlds: reliability and maintainability of hosted authentication pages, combined with the control and seamless feel of a fully native UI. With easy access to session management tools on both iOS and Android, you can deliver a top-tier authentication experience without compromising on security, scalability, or branding. By integrating Native Flows, session management tools like `DescopeSessionManager`, and leveraging platform-specific frameworks such as **ASWebAuthenticationSession**, developers can create authentication journeys that users trust, appreciate, and remember. To learn how to integrate Native Flows in your [Swift](/getting-started/swift) or [Kotlin](/getting-started/kotlin) applications, visit the respective quickstart guides. # Network Client (/mobile-sdk/network-client) Override how the Descope Swift, Kotlin, Flutter, and React Native SDKs perform HTTP requests, for unit testing and custom transport. # Network Client By default each Descope mobile SDK performs its own HTTP requests using the platform's standard networking stack. The Swift, Kotlin, and Flutter SDKs let you replace that stack with your own implementation by setting a **network client** on the SDK configuration. ## When to Use It - **Unit testing.** Supply a client that returns canned responses or throws, so tests that exercise your Descope integration make no real network calls. - **Reusing your app's HTTP stack.** Route Descope requests through the same session, connection pool, or client instance the rest of your app already uses. - **Instrumentation.** Observe request timing or attach your own tracing around the calls the SDK makes. ## Implementing a Network Client Each platform defines the client differently, so the method you implement and the values you return are not the same across SDKs. `DescopeNetworkClient` is a protocol with a single method whose signature intentionally matches `URLSession.data(for:)`. ```swift public protocol DescopeNetworkClient: Sendable { func call(request: URLRequest) async throws -> (Data, URLResponse) } ``` Set it when you configure the SDK: ```swift Descope.setup(projectId: "__ProjectID__") { config in config.networkClient = AppNetworkClient(appSession) } ``` To reuse an existing `URLSession` from elsewhere in your app: ```swift class AppNetworkClient: DescopeNetworkClient { let session: URLSession init(_ session: URLSession) { self.session = session } func call(request: URLRequest) async throws -> (Data, URLResponse) { return try await session.data(for: request) } } ``` To make sure no network calls happen at all during a test: ```swift final class FailingNetworkClient: DescopeNetworkClient { let error: DescopeError = .networkError func call(request: URLRequest) async throws -> (Data, URLResponse) { throw error } } ``` `DescopeNetworkClient` is an interface with a suspending method. Unlike Swift and Flutter, it receives the request as separate arguments and returns a `Response` object rather than a platform HTTP type. ```kotlin interface DescopeNetworkClient { suspend fun sendRequest( url: URL, method: String, body: Map?, headers: Map, ): Response class Response( val code: Int, val body: String, val headers: Map>, ) } ``` The `method` argument is an uppercase HTTP method such as `"GET"` or `"POST"`. Set the client when you configure the SDK: ```kotlin Descope.setup(this, projectId = "__ProjectID__") { networkClient = AppNetworkClient() } ``` A client that fails every request, so a test makes no real network calls: ```kotlin class FailingNetworkClient : DescopeNetworkClient { override suspend fun sendRequest( url: URL, method: String, body: Map?, headers: Map, ): DescopeNetworkClient.Response { throw IOException("network disabled in tests") } } ``` `DescopeNetworkClient` is an abstract class built around the `http` package types. ```dart abstract class DescopeNetworkClient { Future sendRequest(http.Request request); } ``` Set it when you configure the SDK: ```dart Descope.setup('__ProjectID__', (config) { config.networkClient = AppNetworkClient(); }); ``` A client that fails every request, so a test makes no real network calls: ```dart class FailingNetworkClient extends DescopeNetworkClient { @override Future sendRequest(http.Request request) async { throw Exception('network disabled in tests'); } } ``` The React Native SDK has no `DescopeNetworkClient`. Pass a `fetch` implementation to `AuthProvider` instead, and it is used for the requests the SDK makes from JavaScript: ```jsx ``` `customFetch` must have the same signature as the global `fetch`. # JWK Rotation (/additional-security-features-in-descope/jwk-rotation) A guide on how JWK rotation works in Descope and how you control it within the Console # JWK Rotation With Descope, all the public keys accessible via a public JWKs endpoint, and your private keys are controlled in the Descope Console under [Project Settings](https://app.descope.com/settings/project). JWK Key rotation occurs regularly, once a day by default, ensuring a smooth transition to the next key without customer disruption. This ensures ongoing security with minimal impact on active sessions. On-demand JWK key rotation is also available, often used in case of a security incident or other custom security procedure, allowing management of JWKs at the project level by Company and Project Admins. After 12 JWK rotations, users with active sessions will have to re-login as JWKs that are more than 12 rotations away from the current one will be invalidated. ![Descope jwk rotation](/assets/signing-keys.webp) When you click on Rotate Key, you will be able to either `Rotate` (will not affect user sessions) or `Rotate and Revoke` (will force all users to re-login). ![Descope jwk rotate or rotate and revoke](/assets/rotate-key-confirmation.webp) With JWK rotation, you can rest assured that the private keys used to sign Descope JWTs are abstracted away at a project level, and that they are securely stored and used. # Refresh Token Rotation (/additional-security-features-in-descope/refresh-token-rotation) A guide and overview of how refresh token rotation works with Descope and how it secures your sessions. # Refresh Token Rotation If you're curious to learn more about refresh token rotation, besides what is talked about here, check out our [blog](https://www.descope.com/blog/post/refresh-token-rotation) on this topic. Refresh token rotation is a security mechanism used to protect the integrity and security of refresh tokens in an authentication flow. In a typical authentication scenario, a refresh token is issued to a client after a user successfully authenticates. This refresh token can be used to obtain new access tokens once the original access token expires. However, static refresh tokens present a potential security risk if they are compromised. To mitigate this risk, refresh token rotation is employed. ## What is Refresh Token Rotation? Refresh token rotation is a process where a new refresh token is issued every time the client uses a refresh token to obtain a new access token. The previous refresh token is invalidated immediately after it's used. This ensures that only one valid refresh token exists for a session at any given time. If an attacker intercepts a refresh token, they won't be able to use it after the legitimate client uses it, thus reducing the window of opportunity for abuse. ## Security Benefits of Refresh Token Rotation ### Reduced Attack Window By rotating refresh tokens, the window of opportunity for an attacker to use a stolen refresh token is significantly reduced. Even if a refresh token is intercepted, it will only be valid until the client uses it to obtain a new access token. ### Detection of Malicious Activity If a refresh token is used more than once, it may indicate that the token has been compromised. Descope can detect this scenario, enabling the system to take appropriate actions such as revoking all tokens associated with the user session, notifying the user, or requiring re-authentication. ### Enhanced Session Management Refresh token rotation ensures that long-lived sessions remain secure. Even if a session lasts for an extended period, the frequent rotation of refresh tokens prevents the same token from being reused, minimizing the risk of token abuse. ## How We Implement Refresh Token Rotation In Descope, refresh token rotation is automatically handled as part of the [OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6819#section-5.2.2.3) flow. When a refresh token is used to obtain a new access token, Descope will issue a new refresh token along with the new access token. The new refresh token will inherit the expiry time of the original token. The old refresh token is immediately invalidated, ensuring that it can no longer be used. The steps involved in refresh token rotation with Descope are as follows: 1. **User Authentication**: The user authenticates with Descope, and a refresh token along with an access token is issued to the client. 2. **Access Token Expiration**: The access token eventually expires, and the client uses the refresh token to request a new access token. 3. **Refresh Token Rotation**: Descope validates the refresh token, issues a new access token, and rotates the refresh token by issuing a new one while invalidating the old token. 4. **Secure Token Storage**: The client stores the new refresh token securely, replacing the old one. In addition to this, we have automatic reuse detection built in to automatically invalidate refresh tokens if there is an attempt to refresh the session token involving an older refresh token. ## Conclusion Refresh token rotation is a crucial security measure in maintaining the integrity of your authentication system. By regularly rotating refresh tokens, Descope enhances the security of user sessions, reducing the risks associated with token theft and unauthorized access. Implementing this feature within your applications is straightforward with Descope, providing a robust defense against potential security threats. # Request Security Headers (/additional-security-features-in-descope/request-security-headers) A guide on how to view and use the security related headers in Descope's audit trail and flows # Security Related Fields in the Audit Trail Descope automatically logs two powerful security-related fields for every authentication event: - **J4A Fingerprinting**: Detects bots and suspicious activity by analyzing request patterns and characteristics. - **ASN (Autonomous System Number)**: Identifies the network and organization behind the request's IP address. These fields are available in the audit trail and can be leveraged in Descope Flows to automate risk-based security decisions. ## What is J4A Fingerprinting? J4A is an advanced fingerprinting system that analyzes: - Request patterns - HTTP headers - Timing and behavioral signals to generate a unique fingerprint for each request. This helps you: - Detect automated attacks (bots, credential stuffing) - Identify suspicious or anomalous activity - Assign a risk score to each authentication attempt **How to use it:** - Review the J4A score in the audit trail for risk assessment - Add flow conditions to block, challenge, or step-up authentication for risky requests ## What is ASN? **ASN (Autonomous System Number)** is a unique identifier for networks on the internet. Each ASN represents a group of IP addresses managed by a single organization (e.g., an ISP, cloud provider, or enterprise). - ASN is a globally unique 16-digit number - Reveals the network origin of a request - Useful for identifying requests from known malicious networks or anonymizing services **How to use it:** - Allow, deny, or require additional verification for requests from specific ASNs - Apply geo/network-based access controls ## Example: Security Headers in the Audit Trail On the [Audit page](https://app.descope.com/audits), you can see these fields in every event. For example, a `LoginSucceed` event includes `cf-ja4` (J4A fingerprint) and `x-asn` (ASN): ```json { "browser": "Chrome", "device": "Desktop", ... "request_details": { "headers": { "descope": { "cf-ja4": "...", // <--- J4A "x-asn": "...", // <--- ASN ... }, ... }, ... } } ``` ## Using J4A & ASN in Descope Flows You can use these fields in your Descope Flows to: - **Block or challenge risky requests**: Use the J4A score to detect and block bots or suspicious activity. - **Apply network-based logic**: Use ASN to allow, deny, or step-up authentication for requests from certain networks or countries. - **Customize user journeys**: Dynamically adapt authentication flows based on risk profile or network origin. ### Example: Require MFA for Risky ASN Suppose you want to require MFA for users coming from ASNs known for phishing or spam. You can do this in your flow conditions: 1. **Add the ASN condition:** ![ASN flow condition](/assets/asn-risky-condition.webp) 2. **Trigger MFA based on ASN:** ![ASN condition in flow MFA](/assets/asn-risky-in-flow.webp) ## Best Practices - **Monitor**: Regularly review J4A and ASN data in your audit logs for new threats. - **Automate**: Use flow conditions to respond to risky requests in real time. - **Adapt**: Update your flow logic as new attack patterns or malicious ASNs emerge. For more details, see the [Descope Audit Trail doc](https://app.descope.com/audits). # Single Active Session (/additional-security-features-in-descope/single-active-session) Learn how to implement single active sessions across devices with Descope SDK. Enhance security, prevent conflicts, and ensure seamless user experiences. # Single Active Session Utilizing Descope's SDK login options, you can force users to have only a single active session across devices. This ensures that all previous sessions are logged out automatically when a user logs in on a new device. This feature is essential for businesses prioritizing data consistency, security, and user experience. Common use cases include: - Streaming Services: Maintain watch history, play positions, and prevent account misuse. - Ride-sharing apps: Ensure seamless order tracking and prevent duplicate bookings. - Finance Apps: Guarantee secure and frictionless account access across devices. This guide will walk you through implementing this feature using both backend and mobile SDKs. ## Implementing A Single Session Follow the instructions below to implement the single valid session across devices. ### Using Backend SDK Utilizing the `loginOptions` object in the SDKs, you can pass a variable that will revoke all previous sessions. ```javascript const loginId = "email@company.com" const uri = "http://auth.company.com/api/verify_magiclink" const deliveryMethod = "email" // loginOptions (LoginOptions): this is where setting "RevokeOtherSessions" takes place. const loginOptions = { "RevokeOtherSessions": true // This ensures previous sessions are revoked } const resp = await descopeClient.magicLink.signIn[deliveryMethod](loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```python login_id = "email@company.com" delivery_method = DeliveryMethod.EMAIL uri = "http://auth.company.com/api/verify_magiclink" # login_options (LoginOptions): this is where setting "revoke_other_sessions" takes place. login_options = { "revoke_other_sessions": true // This ensures previous sessions are revoked } try: resp = descope_client.magiclink.sign_in(method=delivery_method, login_id=login_id, uri=uri, login_options=login_options) print ("Successfully initialized signin flow") except AuthException as error: print ("Failed to initialize signin flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go ctx := context.Background() deliveryMethod := descope.MethodEmail loginID := "email@company.com" URI := "http://auth.company.com/api/verify_magiclink" // login_options (LoginOptions): this is where setting "RevokeOtherSessions" takes place. loginOptions := &descope.LoginOptions{ RevokeOtherSessions: true // This ensures previous sessions are revoked } err := descopeClient.Auth.MagicLink().SignIn(ctx, deliveryMethod, loginID, URI, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize signin flow: ", err) } else { fmt.Println("Successfully initialized signin flow") } ``` ```java String loginId = "desmond@descope.com" User user = User.builder() .name("Desmond Copeland") .phone("212-555-1234") .email(loginId) .build(); LoginOptions loginOptions = new LoginOptions(); loginOptions.setRevokeOtherSessions(true); MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { String uri = "http://myapp.com/verify-magic-link"; String maskedAddress = mls.signIn(DeliveryMethod.EMAIL, loginId, uri, user, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ### Using Mobile SDK Utilizing the `loginOptions` object in the SDKs, you can pass a variable that will revoke all previous sessions. ```swift let deliveryMethod = DeliveryMethod.email let loginId = "email@company.com" let uri = "http://auth.company.com/api/verify_magiclink" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ // signInOptions (SignInOptions): this is where setting "revokeOtherSessions" takes place. .revokeOtherSessions: true ] do { try await Descope.magicLink.signIn(with: deliveryMethod, loginId: loginId, uri: uri, options: signInOptions) print("Successfully initiated Magic Link Sign In") } catch { print("Failed to initiate Magic Link Sign In") print(error) } ``` ```kotlin try { Descope.magicLink.signIn( method = DeliveryMethod.Email, loginId = "email@company.com", options = listOf( SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // options (SignInOptions): this is where setting "revokeOtherSessions" takes place. const options = SignInOptions({revokeOtherSessions: true}); Descope.magicLink.signIn( method: deliveryMethod, loginId: loginId, options: options, uri: uri); ``` ## Session Type Support When implementing the **single active session** feature, you can manage session revocation using a custom session "type", identified by the `dtt` claim. A session "type" is simply a custom string you assign to a session—such as `"Mobile"` or `"Web"`. This allows you to revoke sessions selectively based on their type. For example, when a user logs in on a new device, you might choose to revoke all existing sessions of type `"Mobile"` while leaving `"Web"` sessions intact. To enable this, add the `dtt` claim to the session's custom claims. Then, during the sign-in process, you can specify the session types to revoke using the `loginOptions`. This provides flexible control over session management by allowing you to tag and target sessions based on their assigned type. ### Using Backend SDK ```go ctx := context.Background() deliveryMethod := descope.MethodEmail loginID := "email@company.com" URI := "http://auth.company.com/api/verify_magiclink" // login options: this is where setting "RevokeOtherSessions" & "RevokeOtherSessionsTypes" takes place. loginOptions := &descope.LoginOptions{ RevokeOtherSessions: true, // This ensures previous sessions are revoked CustomClaims: map[string]any{"dtt": "web"}, // This is where the dtt claim is supplied into the custom claims RevokeOtherSessionsTypes: []string{"mobile"} // Example of revoking "mobile" sessions } err := descopeClient.Auth.MagicLink().SignIn(ctx, deliveryMethod, loginID, URI, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize signin flow: ", err) } else { fmt.Println("Successfully initialized signin flow") } ``` ```java String loginId = "desmond@descope.com" User user = User.builder() .name("Desmond Copeland") .phone("212-555-1234") .email(loginId) .build(); // login options: this is where setting "RevokeOtherSessions" & "RevokeOtherSessionsTypes" takes place. LoginOptions loginOptions = new LoginOptions(); loginOptions.setRevokeOtherSessions(true); // This ensures previous sessions are revoked loginOptions.setCustomClaims(new HashMap() {{ put("dtt", "web"); }}); // This is where the dtt claim is supplied into the custom claims String[] otherSessionTypes = new String[] {"mobile"}; loginOptions.setRevokeOtherSessionsTypes(otherSessionTypes); // Example of revoking "mobile" sessions MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { String uri = "http://myapp.com/verify-magic-link"; String maskedAddress = mls.signUp(DeliveryMethod.EMAIL, loginId, uri, user, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ## Conclusion Implementing a single active session ensures a secure and seamless user experience, especially in industries where real-time data synchronization and account security are critical. With Descope, you can: - Prevent Unauthorized Access: Stop multiple sessions from being active simultaneously. - Enhance User Experience: Ensure real-time updates and synchronization across devices. - Boost Security: Reduce the risk of account misuse or session hijacking. Integrating this feature improves user engagement, trust, and satisfaction in your app. # Federated Identity Providers (/fedramp/federated-identity-providers) Background on which external identity providers carry a FedRAMP High authorization. # Federated Identity Providers and FedRAMP Most FedRAMP deployments are handled directly with Descope Customer Success, who will cover identity provider requirements as part of onboarding. For the certification overview, see [FedRAMP High Authorization](/fedramp). Descope's FedRAMP High authorization applies to the Descope platform boundary. If your users authenticate through an external identity provider (IdP), that provider sits outside the Descope boundary and needs its own FedRAMP High authorization for the end-to-end flow to remain in a High boundary. "Usable in FedRAMP High" means the identity provider you federate to holds its own FedRAMP High authorization. This almost always requires the vendor's **government offering**, not the commercial or consumer tier. When you configure [SSO](/auth-methods/sso) or an [identity federation connection](/identity-federation), make sure to point it at the government SSO tenant. ## Providers with a FedRAMP High Government Offering For each provider below, the listed government offering is what carries a FedRAMP High authorization. The commercial or consumer tier of the same product generally does not. | Identity provider | Government offering required for High | FedRAMP Marketplace | |-------------------|---------------------------------------|---------------------| | Okta | Okta for Government High (commercial Workforce Identity Cloud is Moderate) | [FR2131856836](https://marketplace.fedramp.gov/products/FR2131856836) | | Microsoft Entra ID | Azure Government (commercial Azure also holds a High P-ATO) | Search *Microsoft Azure Government* | | PingOne | Ping Government Identity Cloud (also DoD IL5) | [FR2208555094](https://marketplace.fedramp.gov/products/FR2208555094) | | Salesforce Identity | Salesforce Government Cloud Plus | Search *Salesforce Government Cloud Plus* | | Google Workspace | Google Workspace (government-configured editions) | Search *Google Workspace* | | CyberArk | CyberArk Identity GovCloud / ISPSS | [FR2001619337](https://marketplace.fedramp.gov/products/FR2001619337) | | Descope (the platform itself) | Descope on Palantir Federal Cloud Service | [FR2315464863](https://marketplace.fedramp.gov/products/FR2315464863) | Always confirm current authorization status on the [FedRAMP Marketplace](https://marketplace.fedramp.gov/) before relying on a provider for a High deployment. ## Consumer and Social Login Consumer and social login is consumer-grade and falls outside a FedRAMP High boundary. This includes Apple, Discord, Facebook, Google OAuth (distinct from Google Workspace / GCP), LinkedIn, Microsoft consumer accounts (distinct from Entra ID / Azure Government), Slack social login (distinct from GovSlack), and GitHub. "Sign in with Google," "Sign in with Microsoft," and "Sign in with Slack" refer to the consumer identity tiers. They are not the government tenants (Google Workspace government editions, Azure Government, GovSlack) and are not in scope for a FedRAMP High boundary. ## Self-hosted Identity Providers Self-hosted IdPs carry no cloud service provider authorization of their own. Their FedRAMP status is inherited from whatever environment the hosting agency has authorized: | Identity provider | FedRAMP status | |-------------------|----------------| | AD FS (Microsoft) | On-premises Windows Server role with no CSP authorization; inherits the customer's own boundary | | PingFederate | Self-hosted software with no standalone authorization; reaches High only via Ping Government Identity Cloud | | Keycloak | Open-source self-hosted; FedRAMP status depends on the customer's authorized deployment | | Shibboleth | Open-source self-hosted; FedRAMP status depends on the customer's authorized deployment | | Generic OIDC/SAML SSO | Status depends on whichever endpoint the customer connects | # FedRAMP Security Admin Guide (/fedramp/fedramp-security-guide) Complete guide for managing administrative accounts in Descope with FedRAMP compliance requirements and security best practices. # FedRAMP Security Admin Guide This guide provides comprehensive information on securely managing top-level administrative accounts in Descope's customer-facing applications. It covers administrative role definitions, account lifecycle procedures, and security settings to help organizations maintain FedRAMP compliance and follow secure configuration best practices. This guide focuses on customer-facing application administrative accounts (Descopers and Tenant Admins) and does not cover internal backend administration required for Descope's infrastructure operations. ## FedRAMP Compliance Considerations Descope supports FedRAMP security requirements through the following capabilities: 1. **Access Control**: Role-based access control with least-privilege enforcement 2. **Multi-Factor Authentication**: Support for FIDO2/WebAuthn, TOTP, and SMS OTP 3. **Audit Logging**: Comprehensive logging of all authentication and administrative events 4. **Session Management**: Configurable session timeouts and inactivity detection 5. **Data Residency**: Data storage in the **US Gov** 6. **Encryption**: TLS 1.2+ for data in transit; AES-256 for data at rest 7. **Secure Configuration**: Extensive security settings with secure defaults ## FedRAMP guides For connector actions inside a private network (outbound-only gRPC, no inbound ports), see [Descope Engine](/connectors/descope-engine) under **Connectors**. ## Administrative Account Role Definitions Descope provides hierarchical administrative roles with distinct permissions and operational capabilities. Understanding these roles is critical for implementing least-privilege access control. ### Company-Level Administrative Roles Company-level roles apply across all projects within a Descope company and provide varying levels of administrative access. #### Company Admin **Privilege Level**: Highest administrative privilege across the entire company **Permissions and Actions**: - Full read/write access to all company settings - Full read/write access to all projects within the company - Manage all Descopers (create, modify, delete administrative users) - Create and manage management keys with any permission level - Configure company-wide SSO settings - Enforce MFA requirements for all Descopers - Configure SCIM provisioning for company access - Access and modify all project configurations - Manage project creation, cloning, export, and deletion - View and manage all tenants across all projects - Full access to audit logs across all projects **Security Impact**: Company Admins have unrestricted access to all resources and configurations. This role should be assigned only to senior administrators who require full organizational control. **Recommended Assignment**: Limit to 2-3 trusted senior administrators per organization. #### Project Admin **Privilege Level**: Full administrative access to assigned projects **Permissions and Actions**: - Full read/write access to assigned project(s) settings - Configure authentication methods and flows - Manage users, tenants, and access keys within assigned projects - Configure authorization (roles, permissions, RBAC, FGA) - Manage connectors and integrations - Configure Federated Applications and identity federation - Manage project-level security settings - Access audit logs for assigned projects - Cannot access company-level settings - Cannot manage Descopers or management keys **Security Impact**: Project Admins have full control over project resources but are isolated from company-wide settings and other projects. **Recommended Assignment**: Assign to project leads and senior engineers responsible for specific applications. #### Project Developer **Privilege Level**: Read/write access to project configurations, excluding sensitive settings **Permissions and Actions**: - Read/write access to authentication methods and flows - Read/write access to authorization configurations - Read/write access to connectors and integrations - Manage users, tenants, and access keys - View and modify project settings (excluding company-level settings) - Access audit logs for assigned projects - Cannot manage Descopers or company settings - Cannot manage management keys - Cannot delete projects or modify critical security settings **Security Impact**: Developers can configure application behavior but cannot access company-wide administrative functions. **Recommended Assignment**: Assign to development team members who need to configure authentication and authorization. #### Project Support **Privilege Level**: Read-only access to most configurations, read/write access to user management **Permissions and Actions**: - Read-only access to authentication methods, flows, connectors, IdP apps, authorization, and project settings - Full read/write access to users, access keys, tenants, and audit logs - Can assist users with account issues and password resets - Can create and manage test users - Cannot modify authentication methods, flows, or security configurations - Cannot access company-level settings **Security Impact**: Support personnel can assist users without the ability to modify security-critical configurations. **Recommended Assignment**: Assign to customer support team members who need to help users with account issues. ### Tenant-Level Administrative Roles Tenant-level roles provide administrative capabilities scoped to specific tenants (multi-tenant applications). #### Tenant Admin **Privilege Level**: Full administrative access within a specific tenant **Permissions and Actions**: - Full user management within the tenant (create, modify, delete users) - Assign roles and permissions to tenant users - Configure tenant-specific SSO settings (with SSO Admin permission) - Manage tenant-level roles and permissions - Generate and manage access keys for tenant users (with appropriate permissions) - Access tenant-specific audit logs - Configure tenant profile and settings - Impersonate users within the tenant (with Impersonate permission) - Cannot access other tenants or project-level settings **Required Permissions**: - **User Admin**: Required for reading and modifying user data within the tenant - **SSO Admin**: Required for reading and modifying tenant SSO configurations - **Impersonate**: Required for acting on behalf of another user in the tenant **Security Impact**: Tenant Admins have full control over tenant resources but are isolated from other tenants and project-wide settings. **Recommended Assignment**: Assign to customer organization administrators in B2B applications. ### Management Keys and Service Accounts Management keys provide programmatic access to Descope's Management APIs for automated operations. **Privilege Levels**: - **Company-level management keys**: Access to all projects and company settings - **Project-level management keys**: Access to specific project resources - **Tenant-level management keys**: Access to specific tenant resources **Available Roles for Management Keys**: - **Full Management**: Complete read/write access to management APIs - **User Management**: User CRUD operations only - **Tenant Management**: Tenant CRUD operations only - **Access Key Management**: Access key CRUD operations only - **FGA Read/Write**: Fine-grained authorization read/write operations - **Descoper Access (SCIM)**: SCIM provisioning for Descope console access **Security Impact**: Management keys provide powerful programmatic access and should be treated as highly sensitive credentials. **Recommended Practices**: - Use the principle of least privilege when assigning management key roles - Generate separate keys for different automation purposes - Rotate management keys regularly (at least every 90 days) - Store keys securely in secrets management systems (e.g., HashiCorp Vault, AWS Secrets Manager) - Never commit management keys to version control - Monitor management key usage through audit logs ## Admin Account Lifecycle Procedures This section describes procedures for securely managing administrative accounts throughout their lifecycle. ### Initial Account Setup #### Creating Company Admin Accounts 1. **Account Creation**: - Navigate to [Company Settings > Descopers](https://app.descope.com/settings/company/admins) - Click "+ Descoper" to create a new administrative account - Enter the administrator's email address - Select "Full access" or configure granular permissions with "Company Admin" role - Optionally send an invitation email 2. **Initial Authentication**: - If invitation email is sent, the administrator receives an email with authentication instructions - Administrator completes initial authentication using configured methods (email OTP, magic link, SSO, or passkey) - If MFA is enforced (recommended), administrator must enroll in MFA during first login 3. **Post-Setup Verification**: - Verify the account appears in the Descopers list with correct role assignment - Test account access by logging into the Descope console - Verify the administrator can access intended resources and configurations #### Creating Project/Tenant Admin Accounts 1. **Project Admin Creation**: - Navigate to [Company Settings > Descopers](https://app.descope.com/settings/company/admins) - Click "+ Descoper" - Enter administrator's email address - Select "Granular permissions" - Choose specific projects or tags and assign "Admin", "Developer", or "Support" role - Send invitation email 2. **Tenant Admin Creation**: - Navigate to [Users](https://app.descope.com/users) in the relevant project - Click "+ User" to create a new user account - Enter user details and select the appropriate tenant - Navigate to the user's details and assign the "Tenant Admin" role for the tenant - Alternatively, use the User Management Widget to allow existing Tenant Admins to create new admin accounts 3. **Role Assignment Verification**: - Verify role assignment in the user's profile - Test account access to ensure proper tenant isolation - Verify the administrator cannot access resources outside their assigned scope ### Multi-Factor Authentication (MFA) Requirements MFA is a critical security control for all administrative accounts and should be enforced as a baseline security requirement. #### Enforcing MFA for Company Administrators 1. **Company-Wide MFA Enforcement**: - Navigate to [Company Settings > Settings](https://app.descope.com/settings/company/settings) - Under "Console Access", enable "Enforce MFA" - Supported MFA methods: - **Passkeys** (FIDO2/WebAuthn) - Strongest authentication method, recommended - **TOTP (Time-based One-Time Password)** - Compatible with authenticator apps (Google Authenticator, Authy, etc.) - **OTP via SMS** - Text message-based verification (less secure, use only if other methods unavailable) 2. **Administrator MFA Enrollment**: - Upon next login after MFA enforcement, administrators are prompted to enroll in MFA - Administrator selects preferred MFA method and completes enrollment - Backup MFA methods should be configured for account recovery 3. **MFA Verification**: - After enrollment, administrators must complete MFA verification on each login - MFA is required for all administrative sessions #### Enforcing MFA for End-User Administrators (Tenant Admins) 1. **Project-Level MFA Configuration**: - MFA for tenant admins and end users is configured through Descope Flows - Navigate to [Flows](https://app.descope.com/flows) - Edit the relevant authentication flow (e.g., "sign-up-or-in") - Add MFA steps to the flow: - TOTP enrollment and verification - OTP via SMS enrollment and verification - Passkey enrollment and verification (recommended) 2. **Conditional MFA Based on Role**: - Use flow conditions to enforce MFA only for users with administrative roles - Example condition: Check if user has "Tenant Admin" role, then require MFA - This allows flexible MFA policies based on risk level 3. **Step-Up Authentication**: - For highly sensitive operations, implement step-up authentication - Configure step-up flows that require re-authentication with MFA - Use step-up tokens with short expiration times (5-15 minutes) #### MFA Backup and Recovery 1. **Backup Authentication Methods**: - Administrators should enroll in multiple MFA methods - Store backup codes securely for emergency access - Document MFA recovery procedures for locked-out administrators 2. **MFA Reset Procedures**: - Company Admins can reset MFA for Descopers through the Descope console - For Tenant Admins, Project Admins can reset MFA through user management - MFA resets should be logged and monitored for security incidents ### Account Configuration Best Practices #### Strong Authentication Configuration 1. **Disable Weak Authentication Methods**: - For administrative accounts, disable password-based authentication in favor of: - Passkeys (FIDO2/WebAuthn) - Hardware security keys or platform authenticators - SSO with enterprise identity provider - Magic links with email verification - If passwords must be used, enforce strong password policies through flow configurations 2. **Configure Session Timeouts**: - Navigate to [Project Settings > Session Management](https://app.descope.com/settings/project) - Configure appropriate session token timeouts: - **Session Token Timeout**: 15-60 minutes for administrative sessions - **Refresh Token Timeout**: 1-7 days depending on security requirements - Enable **Session Inactivity** detection: - Set inactivity timeout to 15-30 minutes for administrative accounts - Idle sessions will automatically expire to prevent unauthorized access 3. **Enable Refresh Token Rotation**: - Navigate to [Project Settings > Session Management](https://app.descope.com/settings/project) - Enable "Refresh Token Rotation" for enhanced security - Each session refresh generates a new refresh token, invalidating the previous one - Helps detect and prevent token theft #### Audit Logging Configuration 1. **Enable Comprehensive Audit Logging**: - All administrative actions are automatically logged in Descope - Access audit logs at [Audit](https://app.descope.com/audits) - Audit logs include: - User authentication events (login, logout, MFA) - Administrative actions (user creation, role changes, configuration updates) - API calls via management keys - SSO configuration changes - Access key generation and usage 2. **Audit Log Monitoring**: - Regularly review audit logs for suspicious activity - Use the Audit Widget to provide tenant admins with visibility into tenant-specific events - Export audit logs for integration with SIEM systems - Set up alerts for critical administrative actions 3. **Audit Log Retention**: - Descope retains audit logs according to your subscription plan - For compliance requirements, export and archive logs regularly - Maintain logs for at least 90 days (recommended: 1 year for administrative actions) ### Account Modification and Access Reviews #### Regular Access Reviews 1. **Quarterly Access Review Process**: - Review all Descoper accounts every 90 days - Navigate to [Company Settings > Descopers](https://app.descope.com/settings/company/admins) - Verify each administrator still requires their assigned role - Remove access for administrators who no longer need it - Document access review outcomes 2. **Tenant Admin Access Review**: - Review Tenant Admin assignments quarterly - Use the Role Management Widget or Users page to review role assignments - Verify tenant admins are assigned to correct tenants - Remove unnecessary administrative privileges 3. **Management Key Review**: - Review all management keys every 90 days - Navigate to [Company Settings > Management Keys](https://app.descope.com/settings/company/managementkeys) - Verify each key is still needed and being used - Rotate or delete unused management keys - Verify keys follow least-privilege principle #### Role and Permission Changes 1. **Modifying Descoper Roles**: - Navigate to [Company Settings > Descopers](https://app.descope.com/settings/company/admins) - Click the menu (three dots) next to the Descoper's name - Select "Edit" to modify permissions - Change between "Full access" and "Granular permissions" - Update project/tag assignments and roles - Save changes 2. **Modifying Tenant Admin Roles**: - Navigate to [Users](https://app.descope.com/users) - Search for and select the user - In the user details, navigate to the "Roles" section - Add or remove the "Tenant Admin" role or associated permissions - Changes take effect immediately for new sessions 3. **Change Documentation**: - Document all role and permission changes - Record justification for privilege changes - Use audit logs to track who made changes and when ### Account Decommissioning Proper account decommissioning is critical to prevent unauthorized access after an administrator leaves the organization or changes roles. #### Descoper Account Decommissioning 1. **Immediate Deactivation**: - Upon administrator departure or role change, immediately disable account access - Navigate to [Company Settings > Descopers](https://app.descope.com/settings/company/admins) - Click the menu (three dots) next to the Descoper's name - Select "Delete" to remove the Descoper account - Descoper is immediately logged out and cannot access the console 2. **Session Invalidation**: - Deletion of a Descoper account immediately invalidates all active sessions - The administrator is logged out of the Descope console - All associated access tokens are revoked 3. **Management Key Rotation**: - If the departing administrator had access to management keys, rotate those keys immediately - Navigate to [Company Settings > Management Keys](https://app.descope.com/settings/company/managementkeys) - Delete compromised keys - Generate new keys and update applications that use them - Verify no orphaned keys remain #### Tenant Admin Account Decommissioning 1. **Remove Tenant Admin Role**: - Navigate to [Users](https://app.descope.com/users) - Search for and select the user - Remove the "Tenant Admin" role and associated permissions (SSO Admin, User Admin, Impersonate) - Alternatively, use the User Management Widget for tenant-scoped admin removal 2. **Optional: Full User Account Deletion**: - If the user no longer needs any access to the application, delete the user account - In the user details, click the menu (three dots) - Select "Delete User" - User is immediately logged out and cannot access the application 3. **Access Key Revocation**: - If the user had generated any access keys (M2M tokens), revoke them - Navigate to [Access Keys](https://app.descope.com/accesskeys) - Search for keys associated with the user - Deactivate or delete the access keys #### Post-Decommissioning Verification 1. **Verify Account Removal**: - Confirm the account no longer appears in the Descopers list (for Descopers) - Confirm the user no longer has administrative roles (for Tenant Admins) - Attempt to log in with the decommissioned account to verify access is denied 2. **Audit Log Review**: - Review audit logs to confirm decommissioning actions were completed - Document decommissioning date and administrator who performed the action - Verify no suspicious activity occurred during the administrator's final sessions 3. **Resource Handoff**: - If the administrator managed specific resources (flows, connectors, etc.), reassign ownership - Document any pending work or configurations the administrator was responsible for - Update team documentation to reflect role changes ## Security Settings Reference This section provides a comprehensive reference of all administrative security settings, their functions, security impacts, and recommended values. ### Company-Level Security Settings These settings are configured at the company level and apply across all projects or to specific projects. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Configure SSO** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Enables Single Sign-On (SSO) for Descope console access using SAML or OIDC identity provider | Centralizes authentication, enables enterprise identity governance, reduces password-related risks | **Enabled** with SAML or OIDC IdP for enterprise environments | | **Enforce SSO** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Requires all Descopers to authenticate via SSO (disables email/password login) | Ensures all console access goes through enterprise IdP with centralized policies | **Enabled** after SSO configuration and testing | | **Enforce MFA** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Requires all Descopers to use multi-factor authentication (Passkeys, TOTP, or SMS OTP) | Protects against credential theft and unauthorized access to administrative console | **Enabled** for all environments (supports Passkeys, TOTP, SMS) | | **SSO Role Mapping** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Maps SSO groups to Descope Descoper roles (Company Admin, Project Admin, Developer, Support) | Automates role assignment based on IdP group membership, enforces consistent access control | Configure granular permissions mapping SSO groups to appropriate roles | | **SCIM Provisioning** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) + [Management Keys](https://app.descope.com/settings/company/managementkeys) | Enables automated user provisioning/deprovisioning via SCIM protocol from IdP | Automates Descoper lifecycle management, ensures timely access removal | **Enabled** with SCIM bearer token and IdP integration for enterprise environments | | **Management Key Roles** | [Company Settings > Management Keys](https://app.descope.com/settings/company/managementkeys) | Defines permission scope for programmatic API access (Full Management, User Management, etc.) | Controls programmatic access to sensitive operations, enables least-privilege automation | Use specific roles (User Management, Tenant Management) instead of Full Management when possible | | **Allow Developer Success Access** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Permits Descope support team to access project data for troubleshooting | Enables enhanced support but grants Descope personnel access to project data | **Disabled** by default; enable only when troubleshooting with Descope support, disable after resolution | | **Enable AI-Powered Features and Insights** | [Company Settings > Settings](https://app.descope.com/settings/company/settings) | Enables AI analysis of troubleshooting logs, flow activity, and flow diffs in the console | Processes project data through AI, disabling it stops all AI-based analysis in the console | **Enabled** by default; disable if your organization's data governance or legal requirements prohibit AI processing | ### Project-Level Security Settings These settings are configured per project and control security behavior for end users and tenant administrators. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Approved Domains** | [Project Settings > General > Security](https://app.descope.com/settings/project) | Whitelist of domains allowed for redirect and verification URLs | Prevents open redirect vulnerabilities and unauthorized callbacks | **Always configure**; list only trusted application domains; never leave empty | | **Federated App Access** | [Project Settings > General > Security](https://app.descope.com/settings/project) | Defines default access to federated applications for new users | Controls automatic application access provisioning | Require explicit approval for new users (disabled by default) | | **JWK Rotation** | [Project Settings > General > Security](https://app.descope.com/settings/project) | Manages signing keys used for JWT verification | Regularly rotating keys limits impact of key compromise | Rotate keys at least every 90 days; maintain 2 active keys during rotation | | **Block Self-Registration Sign Up** | [Project Settings > General > Sign Ups](https://app.descope.com/settings/project) | Prevents new users from self-registering; requires invitation or SSO | Restricts user base to invited or SSO-provisioned users | **Enabled** for B2B applications or where user registration should be controlled | | **Session Token Timeout** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Expiration time for session tokens (access to application resources) | Shorter timeouts reduce risk of stolen token abuse; longer timeouts improve UX | **15-60 minutes** for admin sessions; 60-120 minutes for standard users | | **Refresh Token Timeout** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Expiration time for refresh tokens (ability to obtain new session tokens) | Shorter timeouts force re-authentication; longer timeouts improve UX | **1-7 days** for admin sessions; 7-30 days for standard users | | **Refresh Token Rotation** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Generates new refresh token on each use, invalidating previous token | Detects token theft (reuse of old token indicates compromise) | **Enabled** for all environments | | **Session Inactivity Timeout** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Automatically expires sessions after specified period of inactivity | Prevents unauthorized access to abandoned sessions | **Enabled** with 15-30 minute timeout for admin sessions | | **Token Response Methods** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Determines how tokens are delivered (cookies vs. response body) | Cookies with HttpOnly/Secure flags provide better security than localStorage | **Cookies** for web applications; response body only for mobile/native apps | | **Access Key Expiration** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Expiration time for M2M access key session tokens | Shorter expiration reduces risk of compromised service account tokens | **1-30 days** depending on use case; use shorter durations for high-privilege keys | ### Authorization and Role-Based Access Control (RBAC) Settings These settings control user roles, permissions, and tenant-based access. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Project-Level Roles** | [Authorization > RBAC](https://app.descope.com/authorization/rbac) | Defines roles available across all tenants in the project | Establishes consistent permission structure for application-wide access | Define roles based on job functions; avoid overly broad permissions | | **Tenant-Level Roles** | [Tenants > \{Tenant\} > Authorization](https://app.descope.com/tenants) | Defines roles specific to individual tenants | Enables tenant-specific access control and multi-tenancy isolation | Create tenant-specific roles for B2B use cases where customers need custom access controls | | **Default Roles** | [Authorization > RBAC](https://app.descope.com/authorization/rbac) or [Tenants > \{Tenant\} > Authorization](https://app.descope.com/tenants) | Automatically assigns specified roles to new users | Provides baseline permissions for new users | Assign least-privilege default role; require explicit assignment for elevated privileges | | **Hidden Roles** | [Authorization > RBAC](https://app.descope.com/authorization/rbac) or [Tenants > \{Tenant\} > Authorization](https://app.descope.com/tenants) | Hides roles from Tenant Admins in admin widgets and SSO attribute mapping | Prevents tenant admins from assigning sensitive system roles | Mark system/internal roles as hidden to prevent unauthorized assignment | | **Tenant Admin Permissions** | [Authorization > RBAC](https://app.descope.com/authorization/rbac) | Grants "User Admin", "SSO Admin", and "Impersonate" permissions to Tenant Admin role | Controls tenant admin capabilities for user management, SSO config, and impersonation | Assign only necessary permissions; consider separate roles for SSO Admin vs User Admin | | **Permission Definitions** | [Authorization > RBAC](https://app.descope.com/authorization/rbac) | Defines granular permissions that can be assigned to roles | Enables fine-grained access control within application | Define permissions based on specific actions/resources; avoid catch-all permissions | ### Authentication Method Security Settings These settings control which authentication methods are available and their security configurations. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Passkey Authentication** | [Authentication Methods > Passkeys](https://app.descope.com/authentication/passkeys) | Enables FIDO2/WebAuthn passkey authentication | Strongest phishing-resistant authentication method; eliminates passwords | **Enabled** and promoted as primary authentication method for admin accounts | | **Magic Link Authentication** | [Authentication Methods > Magic Link](https://app.descope.com/authentication/magiclink) | Enables email-based passwordless authentication | Secure if email is protected; vulnerable to email compromise | Acceptable for standard users; consider requiring MFA for admin accounts | | **OTP Authentication** | [Authentication Methods > OTP](https://app.descope.com/authentication/otp) | Enables one-time password via email or SMS | Security depends on email/SMS security; SMS vulnerable to SIM swap attacks | Acceptable with email; SMS should be backup option only | | **Password Authentication** | [Authentication Methods > Password](https://app.descope.com/authentication/password) | Enables traditional username/password authentication | Weakest authentication method; vulnerable to phishing, credential stuffing | **Disabled** for admin accounts; if required, enforce strong password policy and MFA | | **SSO Authentication** | [Authentication Methods > SSO](https://app.descope.com/authentication/sso) | Enables enterprise SSO (SAML/OIDC) authentication | Delegates authentication to enterprise IdP with centralized policies | **Enabled** for B2B applications; enforce IdP-level MFA | | **Social Login** | [Authentication Methods > Social](https://app.descope.com/authentication/social) | Enables OAuth login with social providers (Google, Microsoft, etc.) | Security depends on social provider; may not meet enterprise security requirements | Avoid for admin accounts; acceptable for standard users in B2C applications | ### Rate Limiting and Abuse Prevention Settings These settings protect against brute force attacks and API abuse. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Authentication Rate Limiting** | [Project Settings > Rate Limiting](https://app.descope.com/settings/project) or configured via flows | Limits authentication attempts per user/IP address | Prevents brute force credential attacks and account enumeration | **Enabled**; 5-10 attempts per 15 minutes per user; stricter limits for admin accounts | | **API Rate Limiting** | Configured via Descope service (contact support for custom limits) | Limits API requests to prevent abuse and DDoS | Protects service availability and prevents resource exhaustion | Default limits are appropriate for most use cases; increase only if legitimate traffic requires it | | **User Enumeration Prevention** | [Security Best Practices > Preventing User Enumeration](/security-best-practices/preventing-user-enumeration) | Returns consistent messages whether user exists or not | Prevents attackers from discovering valid user accounts | **Enabled** by default; do not customize authentication flows to reveal user existence | ### Data Protection and Privacy Settings These settings control data handling, encryption, and privacy. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Data Residency Region** | [Project Settings > General](https://app.descope.com/settings/project) (set at project creation) | Determines geographic location of data storage (**US Gov** for FedRAMP) | Ensures data remains in the United States as required for federal deployments | **US**; set at project creation; cannot be changed after project creation | | **Custom Domain** | [Project Settings > General](https://app.descope.com/settings/project) | Uses custom domain instead of `api.descope.com` for API endpoints | Improves brand consistency and may be required for security policies | Configure custom domain with valid SSL/TLS certificate | | **Encryption in Transit** | Automatically enforced | All API communication uses TLS 1.2+ | Protects data from interception during transmission | Always enforced; ensure applications do not disable certificate verification | | **Encryption at Rest** | Automatically enforced | All stored data is encrypted using industry-standard encryption | Protects data from unauthorized access if storage is compromised | Always enforced; no configuration required | | **PII Handling** | Configurable via user attributes and JWT templates | Controls what personally identifiable information is stored and included in JWTs | Minimizes exposure of sensitive user data | Store only necessary PII; avoid including sensitive data in JWT claims | ### Audit and Monitoring Settings These settings control logging, monitoring, and visibility into system activity. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Audit Log Retention** | Automatic based on subscription plan | Retains logs of all authentication and administrative events | Enables security monitoring, incident investigation, and compliance reporting | Export and archive logs for extended retention (90+ days recommended for admin actions) | | **Audit Log Access** | [Audit](https://app.descope.com/audits) | Controls who can view audit logs | Audit logs contain sensitive information about system activity | Limit access to security personnel and senior administrators | | **Custom Audit Events** | Implemented via SDK | Allows logging of application-specific events to Descope audit trail | Provides comprehensive audit trail including application logic | Log all administrative actions and security-relevant events | | **Webhook Notifications** | [Connectors > Webhooks](https://app.descope.com/connectors) | Sends real-time notifications of authentication and administrative events | Enables real-time security monitoring and SIEM integration | Configure webhooks to send critical events to SIEM or alerting system | ### Advanced Security Settings These settings provide additional security controls for specific use cases. | Setting | Location | Function | Security Impact | Recommended Value | |---------|----------|----------|-----------------|-------------------| | **Step-Up Authentication** | Implemented via flows | Requires re-authentication for sensitive operations | Protects high-risk actions even if session is compromised | **Enable** for admin operations, financial transactions, and sensitive data access | | **Trusted Device Tokens** | [Project Settings > Session Management](https://app.descope.com/settings/project) | Remembers trusted devices to reduce MFA prompts | Improves UX but may weaken security if device is compromised | Disable for admin accounts; acceptable for standard users with short expiration (7-30 days) | | **Content Security Policy** | [Security Best Practices > CSP](/security-best-practices/content-security-policy) | Configures CSP headers for Descope-hosted flows | Protects against XSS attacks in authentication flows | Configure restrictive CSP policy for hosted flows | | **Firewall/ACL** | [Security Best Practices > Firewall ACL](/security-best-practices/firewall-acl) | Restricts API access to specific IP ranges | Prevents access from unauthorized networks | Configure IP allowlist for admin access to management APIs | | **External Token Validation** | [Inbound Apps > External Token Management](/identity-federation/inbound-apps/using-inbound-apps#external-token-management) | Validates JWTs from trusted external issuers (JWT Bearer) and exchanges them for Descope tokens | Enables federation with external identity systems | Configure only if integrating with external token issuers; validate signatures and claims | ## Additional Resources For more information on security features and best practices in Descope, refer to the following documentation: - [Descope Engine](/connectors/descope-engine): self-hosted connector agent for private-network resources (outbound-only gRPC) - [Company Settings](/management/company-settings) - Managing Descopers and company-level configuration - [Role-Based Access Control](/authorization/role-based-access-control) - Configuring roles and permissions - [Admin Widgets](/widgets/admins) - Implementing user, role, and audit management for tenant admins - [Session Management](/sessions) - Configuring session tokens and authentication - Security Best Practices - Additional security hardening guidance (see links throughout this document) - [Rate Limiting](/rate-limiting) - Configuring rate limits to prevent abuse - [Project Settings](/management/project-settings) - Configuring project-level security settings - [FedRAMP Recommended Secure Configuration](https://www.fedramp.gov/docs/rev5/recommended-secure-configuration/) - Official FedRAMP guidance # FedRAMP High Authorization (/fedramp) Descope is FedRAMP High Authorized. Learn how FedRAMP deployments are handled with Descope. # FedRAMP High Authorization Descope is **FedRAMP High Authorized**. The Federal Risk and Authorization Management Program (FedRAMP) is the US government's standardized approach to security assessment and authorization for cloud services, and High is its most stringent impact level. Descope is listed on the [FedRAMP Marketplace](https://marketplace.fedramp.gov/products/FR2315464863), which makes it available to federal agencies, contractors, system integrators, and any other organization that requires FedRAMP High authorized software. For the public administrative configuration guidance required under FedRAMP, see the [FedRAMP Security Admin Guide](/fedramp/fedramp-security-guide). That guide is published because FedRAMP requires customer-responsible configuration guidance to be publicly accessible. ## Working with Descope on FedRAMP Most of a FedRAMP deployment is handled directly with Descope Customer Success, including the FedRAMP onboarding process. If you are planning a deployment, [contact Descope Support](/support) to scope the environment and kick off onboarding. The Descope team will walk you through provisioning, configuration, and any requirements specific to your agency or program. For general dedicated-environment configuration (base URLs, networking, feature enablement), see [Private Cloud Deployments](/how-to-deploy-to-production/private-cloud). ## Connectors in Your Environment Descope [Connectors](/connectors) let you extend identity flows to external services. In a FedRAMP deployment, those services often sit inside a private network with no inbound access. The **Descope Engine** is a component you install within your own environment so connector actions can run against private-network resources. It uses outbound-only gRPC and opens no inbound ports, which keeps it compatible with locked-down government networks. See [Descope Engine](/connectors/descope-engine) for deployment details. ## Federating to an External Identity Provider Descope's authorization covers the Descope platform boundary. If your users authenticate through an external identity provider (such as Okta or Microsoft Entra ID), that provider sits outside the Descope boundary and needs its own FedRAMP High authorization for the end-to-end flow to remain in a High boundary. Your Descope team will cover this as part of onboarding. For background on which providers and tiers qualify, see our doc on [Federated Identity Providers and FedRAMP](/fedramp/federated-identity-providers). # Content Security Policy (/security-best-practices/content-security-policy) Content Security Policy - Understanding how to effectively implement it in your application # Content Security Policy The Content Security Policy (CSP) is a security standard introduced to prevent various attacks, including Cross-Site Scripting (XSS) and data injection attacks. It allows web developers to specify the domains the browser should consider valid sources of executable scripts for a given webpage. By doing this, CSP can effectively reduce the risk of XSS attacks by specifying which sources are trusted, preventing browsers from executing scripts not approved as part of the policy. If you choose to utilize CSP with Descope Flows, below is an example of a valid CSP configuration, including the necessary references to `static.descope.com`. If you're using Descope with a private tenant, then you will need to request a specific CSP policy from Descope support directly. ## Customers without Custom Domain ```html title="index.html" ``` ## Customers with Custom Domain In this example, the CNAME record is configured to be `auth.example.com`, with the App URL being `https://example.com`. ```html title="index.html" ``` ## Nonce Support Nonce, or "number used once," is a cryptographically secure, random value generated by the server for each unique HTTP request. To use nonce, add it to the Descope flow component as seen below: ```javascript const hdrs = await headers(); const nonce = hdrs.get("x-nonce"); return ( ); ``` ```javascript const hdrs = await headers(); const nonce = hdrs.get("x-nonce"); return ( ); ``` ```javascript // .Controller file app.controller('AuthController', function($http, $scope) { $http.get('/headers-endpoint') // Replace with the actual endpoint providing the headers .then(function(response) { const nonce = response.headers('x-nonce'); $scope.nonce = nonce; }) .catch(function(error) { console.error('Failed to fetch headers:', error); }); }); //.html file ``` ```javascript ``` You can refer to this [sample application](https://github.com/descope-sample-apps/strict-csp-demo) which demonstrates proper nonce implementation in a CSP context. Your CSP will need to include these references in order to effectively use flows without browser errors. # Cross-Site Cookies (/security-best-practices/crossite-cookies) Cross-Site Cookies - Understanding the Domain and SameSite Attributes # Cross-Site Cookies This guide covers the details regarding Cross-Site cookies related to the Domain and SameSite Attributes. Cross-site cookies can be convenient and complex for developers, especially regarding authentication. These cookies enable seamless user sessions across different domains. Understanding the domain and SameSite attributes is vital to building robust and secure authentication systems. Let's explore their usage and demystify their complexities. ## Domain Attribute The domain attribute defines the scope of a cookie, determining which domains and subdomains can access it. Below you will find a few examples using Express.js. ### Example 1: Specific Domain In this example, when a user visits the /set-cookie route, a cookie named `user` with the value `Joe Person` is set with the domain attribute set to `www.example.com`. This cookie will only be accessible on the `www.example.com` domain and its subdomains. ```javascript title="app.js" app.get('/set-cookie', (req, res) => { res.cookie('user', 'Joe Person', { domain: 'www.example.com' }); res.send('Cookie set!'); }); ``` ### Example 2: All Subdomains In this example, the domain attribute configured is `.example.com` with a preceding dot. This configuration allows the cookie to be accessible on all subdomains of `example.com`, such as `subdomain.example.com` or `another.subdomain.example.com.` Only the current configured domain can be the value or a domain of a higher order unless it is a [public suffix](https://publicsuffix.org/), which is never allowed. Setting the domain will make the cookie available to it, as well as to all its subdomains. If omitted, this attribute defaults to the host of the current document URL, not including subdomains. e.g., if the response is sent from api.descope.com the only acceptable domain values are api.descope.com and descope.com ```javascript title="app.js" app.get('/set-cookie', (req, res) => { res.cookie('user', 'Joe Person', { domain: '.example.com' }); res.send('Cookie set!'); }); ``` ## Same-Site Attribute The SameSite attribute controls how cookies behave in different contexts. Below you will find a few examples using Express.js. ### Example 1: Strict In this example, the SameSite attribute configuration is `Strict`. This configuration ensures that the cookie will only be sent with requests originating from the same site, providing higher security. `Strict` configuration means that if the user is visiting example.com and a request is made to `api.descope.com`, the cookies will not be sent with that request. ```js title="app.js" app.get('/set-cookie', (req, res) => { res.cookie('user', 'Joe Person', { sameSite: 'Strict', domain: 'api.descope.com' }); res.send('Cookie set!'); }); ``` ### Example 2: Lax In this example, the SameSite attribute configuration is `Lax`. The cookie will be sent with top-level navigation requests and some cross-site requests initiated by the user but not with requests initiated by third-party resources. ```js title="app.js" app.get('/set-cookie', (req, res) => { res.cookie('user', 'Joe Person', { sameSite: 'Lax' }); res.send('Cookie set!'); }); ``` ### Example 3: None In this example, the SameSite attribute is configured as `None` to allow cross-site access. However, it also requires configuration of the Secure attribute, ensuring the cookie is sent only over HTTPS connections. ```js title="app.js" app.get('/set-cookie', (req, res) => { res.cookie('user', 'Joe Person', { sameSite: 'None', secure: true }); res.send('Cookie set!'); }); ``` ## Conclusion By correctly utilizing the domain and SameSite attributes, developers can control cookie accessibility, enhance security, and provide a seamless authentication experience across different domains and subdomains. # JWT Claims (/security-best-practices/custom-claims) A guide and overview of how to properly manage and use custom claims in your tokens. # JWT Claims Custom claims are included in your JWTs (session tokens, refresh tokens, and ID tokens when using Descope as an OIDC provider). You can add custom claims to your JWTs by adding the [Custom Claims](/flows/actions/custom-claims) action in your flow, or by using a [JWT Template](/management/token/jwt-templates). Since refresh tokens in production are typically stored as cookies, be aware that custom claims will contribute to the cookie size and must stay within browser cookie limits. See the [Cookie Size](#cookie-size) section below for more information. ![Descope custom claims within JWT](/assets/custom-claims-jwt.webp) When using custom claims however, it's important to understand all of the functionalities inherent in them, as well as how to use them securely and responsibly. This guide will focus on all of the different security aspects of custom claims, as well as recommended best practices to employ when using them. ## Cookie Size Descope stores your custom claims in the refresh token. In order to properly store your refresh tokens, we typically [suggest](/security-best-practices/crossite-cookies) storing them in a `samesite=strict`, `httpOnly=true`, `secure` cookie. Since cookies have a maximum size of 4kb supported by browsers, storing information in a custom claim while using cookies (the recommended way) might be a problem. Therefore, it's best to not store too much in the JWT to make sure you don't run into any storage limitation issues. Note that each key can have a maximum of 60 chars, each claim value can have a maximum of 500 chars, and each JWT can have a maximum of 100 keys. ## Do Not Store Sensitive Information You should never store any sensitive user information in a JWT. This can be sensitive user PII, or other secrets related to your application or authentication architecture. JWTs are just Base 64 encoded tokens, and even if they are stored securely, that sensitive information could be exposed if your JWT was ever compromised by a malicious hacker. ## Secure vs Non-Secured Custom Claims The backend cannot trust custom claims specified by the client SDK or API due to the following items: - Potential tampering and spoofing by malicious hackers - The lack of a verification mechanism for client-added claims - The violation of defined trust boundaries which treat client data as untrusted - Inconsistent enforcement policies on accepted claims - The foundational security principle that cautions against trusting user input. With this security in mind, custom claims added by the client SDK will be within the `nsec` claim in the session token JWT. When utilizing the management SDK or Descope flows, custom claims that are added will be marked secure, and will not be within the `nsec` claim. Below is an example of a session JWT with custom claims added under the `nsec` claim since they were added by the client. ```json { "amr": [ "email" ], "drn": "DS", "exp": 1692304651, "iat": 1692304051, "iss": "P2RFvFexVaxxNFK6rhP0ePtaGfTK", "nsec": { "email": "example@email.com", "name": "Joe Person" } "sub": "U2RG6grrbT3REKYqk5yC4SjkMqzA", "tenants": { "T2U7vUH1NPy4JzWHruoOVIGyzYlu": { "permissions": [ "AppSecEngineer", "Marketing", "Support" ], "roles": [ "Engineering", "Product Manager" ] }, "T2U7vVBqyZv6HdGtGLdnkgCbNxrC": { "permissions": [ "AppSecEngineer", "Support" ], "roles": [ "Support" ] } } } ``` ## Using the Audience (aud) Claim The `aud` (Audience) claim in a JWT specifies the intended recipients of the token, ensuring the correct application or server uses it. It can be a single entity or multiple entities, defined as a string or an array of strings, to prevent the token from being accepted by unintended parties. Descope allows you to append to the `aud` claim by utilizing the Custom Claims action to append an item. Below is an example of appending to the `aud` claim using the Descope custom claims action. ![Descope custom claims management add to flow](/assets/custom-claims-action-aud.webp) The returned JWT would look similar to the below. ```sh title="Terminal" { "amr": [ "oauth" ], "aud": [ "xxxx", "something" ], "drn": "DS", "eml": "xxxx", "enabled": true, "exp": 1709674989, "iat": 1709674809, "iss": "xxxx", "name": "xxxx", "rexp": "2024-04-02T21:40:09Z", "sub": "xxxx", "tenants": { "xxxx": {} } } ``` ## Overriding the AMR Claim The `amr` (Authentication Methods Reference) claim is a standard claim in JWTs that indicates which authentication methods were used during the authentication process. By default, Descope sets this claim automatically based on the authentication method used (e.g., `oauth`, `email`, `pwd`, `mfa`, etc.). However, in certain scenarios—particularly when using [SSO authentication](/auth-methods/sso)—you may want to override the `amr` claim to reflect the actual authentication methods used by the SSO provider, rather than just `fed` (federated authentication). This is especially useful when your SSO provider includes detailed authentication method information in their OIDC token, and you want to pass that information through to your application's JWT. ### How to Override the AMR Claim You can override the `amr` claim using either: 1. **Custom Claims Action in Flows** - Add a Custom Claims action in your authentication flow and set the `amr` key with your desired value 2. **JWT Templates** - Configure the `amr` claim in your [JWT Template](/management/token/jwt-templates) **Example using Custom Claims Action:** In the Custom Claims action, you can use the advanced configuration to override the `amr` claim: ```json { "amr": ["pwd", "mfa"] } ``` You should only use this override if your SSO provider actually supports and populates the `amr` claim in its OIDC tokens. Since not all providers do (for example, Google and Azure AD typically omit it), you should always verify that your SSO provider includes this information before implementing the override. ## Additional Standard Claims ### Azp Claim When using Descope as an OIDC provider, we will automatically append the `azp` claim to your JWT as well. This is something you will only see if you're using OIDC. # Firewall ACL Configuration (/security-best-practices/firewall-acl) Guide for configuring firewall ACLs for Descope services. # Firewall Configuration for Descope To enable seamless integration with Descope's authentication and authorization services, your organization's firewall Access Control List (ACL) must allow specific domains. This page outlines the necessary configurations to ensure smooth access to Descope services, supporting secure user authentication, API interactions, and asset loading. ## Required Domains for Descope Access You can override the serving of static assets from `static.descope.com` by setting the `baseStaticUrl` parameter in the Descope SDK [configuration](/client-sdk/descope-components#base-url-configuration). To use Descope's services, allow the following domains in your firewall: - **API Access**: `api.descope.com` or your own [Custom Domain](/how-to-deploy-to-production/custom-domain) if configured. - **Purpose**: Manages all API requests, including user authentication, session handling, and user management. - **Protocol**: HTTPS (`port 443`). - **Static Assets**: `static.descope.com` or your own domain if overriding with the `baseStaticUrl` described above. - **Purpose**: Hosts static assets, such as JavaScript files and stylesheets, required for Descope's embedded UIs and widgets. - **Protocol**: HTTPS (`port 443`). These domains must be accessible by your network to ensure the correct functioning of Descope's services. ## Firewall Configuration Recommendations 1. **Allow Only Secure HTTPS Access** - Restrict access to HTTPS (`port 443`) to ensure secure communication. - **Do not** allow HTTP access to enforce secure-only connections. 2. **Domain-Based ACL Rules** - Use domain-based rules (`api.descope.com` and `static.descope.com`) rather than IP-based rules due to Descope's use of a global, dynamic CDN. This ensures that any IP changes do not disrupt connectivity. 3. **Monitoring and Logging** - Regularly audit and log traffic to Descope domains to monitor for anomalies or unauthorized access attempts. - Track error logs for any `403` or `404` responses, as these may indicate firewall misconfigurations. ## Security Best Practices - **Limit Access to Necessary Services**: Only permit access to `api.descope.com` and `static.descope.com` to minimize exposure. - **Rate Limiting**: Apply rate limits to protect your environment from potential abuse. Descope's built-in rate limiting complements this but adding your own can enhance security. - **Periodic Verification**: Confirm accessibility to these domains regularly, especially during network updates or firewall policy changes. ## Frequently Asked Questions (FAQs) **1. What if our firewall requires IP-based rules?** Descope leverages a CDN with dynamic IPs, so domain-based rules are recommended. If IP-based restrictions are mandatory, consider using DNS resolution with dynamic updating. **2. Do we need any other ports open?** Descope only requires HTTPS on `port 443` for both API and static asset requests. **3. How can we check if an issue is firewall-related?** Run a DNS lookup or ping `api.descope.com` and `static.descope.com` from your network. Inaccessibility may indicate a firewall block. For assistance with configurations or troubleshooting, please reach out to [Descope Support](/support) or consult additional Descope documentation for further integration guidance. # Certificate Verify Mode (Go) (/security-best-practices/golang-cert-verification) Learn how to utilize the certificate verify mode within the Descope backend Go SDK. # Certificate Verify Mode (Go) In Go (Golang), `InsecureSkipVerify` is a field in the `tls.Config` struct. When set to true, it controls whether a client verifies the server's certificate chain and host name. If `InsecureSkipVerify` is true, TLS accepts any certificate presented by the server and any host name in that certificate. This guide covers how to utilize the certificate verify mode during the instantiation of the Descope Go SDK. ## Available Verification Configuration When instantiating the Descope Go SDK, you can select which mode you would like to utilize. The available modes are detailed below. - **CertificateVerifyAutomatic (default)**: Always verify server certificate, unless the BaseURL is overridden to a value that uses an ip address, localhost, or a custom port - **CertificateVerifyAlways**: Always verify server certificate, this is only needed if you override the default BaseURL and the automatic behavior isn't suitable - **CertificateVerifyNever**: Never verify server certificate ## Using the Go SDK To configure the method which you verify certificates within the instantiation of the Go SDK, you would use the following as an example. ### Install SDK ```sh title="Terminal" go get github.com/descope/go-sdk ``` ### Import and initialize SDK ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", CertificateVerify:CertificateVerifyAutomatic}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` # M2M Security (/security-best-practices/m2m-security) Security philosophy behind machine-to-machine (M2M) authentication using the client credentials flow or Descope Access Keys to exchange for JWTs with Descope. # M2M Security In this guide, we discuss our philosophy behind implementing machine-to-machine (M2M) authentication using Descope’s **[client credentials flow](/getting-started/oidc-endpoints#client-credentials-flow)** or **[Descope Access Keys](/management/m2m-access-keys)** to securely exchange for JWTs. This method is ideal for services that need to authenticate with each other without user interaction, providing a robust security framework for automated processes and microservices. ## Overview In the **client credentials flow**, Service A (the client) sends its `client_id` (Access Key ID) and `client_secret` (Access Key Secret) to Descope. Descope validates the credentials and returns a JWT (access token), signed with Descope's private key. The JWT can include custom claims relevant to your service’s authorization logic. Service A then passes the JWT to Service B (the downstream service), which uses it to authenticate Service A's request. ### Key Security Benefits One of the key security advantages of this approach is that the **client_secret (Descope Access Key)** is only stored and managed in one place—**Service A**. Here’s how it works: - **Secure Storage**: Service A is responsible for securely storing the access key in a secure environment, such as a key management system or secret manager. - **Token Exchange**: When Service A needs an access token, it exchanges the access key for a short-lived JWT, which it then sends to Service B. - **Key Rotation**: By rotating the access key periodically, Service A minimizes the risk of secret leakage without needing to alter other parts of the infrastructure. - **JWT Transmission**: Only the JWT is transmitted between services, reducing the exposure of long-lived secrets like the client_secret. ### JWT Validation and Custom Claims Service B only needs to validate the JWT it receives. The JWT includes: - **Cryptographic Signature**: JWTs are signed by Descope and are tamper-evident. Service B can verify the signature without calling back to Service A or Descope. - **Custom Claims**: The JWT can include specific claims such as permissions, roles, or identifiers, allowing Service B to perform fine-grained access control. This architecture reduces the attack surface by centralizing secret management in **Service A** while maintaining distributed authorization using short-lived, verifiable tokens (JWTs). By leveraging Descope's client credentials flow or access keys to exchange for JWTs, you can securely handle M2M authentication. This approach ensures centralized management of sensitive credentials, provides short-lived, tamper-proof tokens for authentication, and enables fine-grained access control between services. # Preventing User Enumeration (/security-best-practices/preventing-user-enumeration) This guide explains how to prevent user enumeration with Descope # Preventing User Enumeration This guide describes how to prevent user enumeration, a security vulnerability that allows attackers to enumerate users by checking for the presence of valid usernames or email addresses. ## Overview User enumeration attacks occur when an attacker is able to determine whether a specific user account exists in your system by analyzing error messages. To protect user's privacy and prevent these attacks, Descope provides built-in mechanisms to hide sensitive information and control error-handling behavior. ## Hide Sensitive Information for Password Authentication When using password-based authentication, you can prevent attackers from guessing valid user accounts by hiding detailed error messages. On the [Authentication Methods --> Passwords page](https://app.descope.com/settings/authentication/password) of the Descope Console, toggle on **Hide sensitive error information**. ![Hide sensitive error information](/assets/hide-sensitive-info.webp) When this option is enabled, Descope will return generic error messages for failed password logins instead of revealing whether the issue was due to an invalid user or incorrect password. This helps protect against user enumeration attacks by not confirming the existence of accounts. ## Customize Error Handling By default, Descope provides automatic handling for authentication errors. However, you can also [customize how errors are handled](/handling-flow-errors/customizing-flow-errors) in your flow to protect against user enumeration: In the Flow editor, open the relevant action (e.g., `Sign Up or In / OTP / Email`) and configure the **Error Handling** step. Under handling, select **Mitigate**. This will handle the error silently, and proceed as if the step was successful. This is useful when you want to hide the cause of an error but continue the flow (e.g., display a generic "If this account exists, you'll receive an email" message). ![Mitigate Error Handling](/assets/error-handling-options.webp) ![Mitigate Error](/assets/mitigate-error.webp) By customizing error handling, you can ensure that your users receive consistent, generic messages while preventing attackers from learning which part of the authentication process failed. You can apply this error handling configuration to any authentication method action within your flow. # Storing Refresh Tokens (/security-best-practices/refresh-token-storage) A guide and overview of how refresh token storage works with Descope and how to ensure you manage it securely. # Storing Refresh Tokens When you're using Descope on the client-side, it's imperative that you store your refresh tokens securely. Your refresh tokens are typically valid for 1 month, and if they are exposed, attackers can create their own session tokens to authenticate as someone else, effectively as if they had stolen their password. Follow these guidelines to learn how to store your refresh tokens securely. ## Set Refresh Token Expiry Time The [refresh token expiry time](https://app.descope.com/settings/project) should be decided based on the requirements of your application (tradeoff between higher security and user experience). A shorter expiration time means that the user will need to authenticate frequently. ## Handling Refresh Tokens in Cookies If you are using our Client SDK (including Flows), Descope manages the refresh token storage for your application client. Depending on the [project configuration](https://app.descope.com/settings/project/session), you can handle the refresh token in two different ways - **manage in cookies** and **manage in response body**. ![refresh token management](/assets/refresh-token-management.webp) It is recommended to store your refresh tokens in cookies, rather than in the browser localStorage. This is important because it will help mitigate the risk of XSS (Cross-site-scripting) attacks, since tokens in localStorage are accessible via JavaScript. Since Descope uses `httpOnly` cookies, the refresh token will not be accessible using JavaScript. Descope cookies are also `sameSite=strict` and protected against cross-site request forgery attacks. You can configure this under [Session Management Settings](https://app.descope.com/settings/project/session) in the Descope Console. In order to store your refresh tokens in cookies, you must first [configure a custom domain](/how-to-deploy-to-production/custom-domain) ![Descope manage session in cookies](/assets/refresh-tokens.webp) As a best practice, make sure to limit your custom domain scope as much as possible. This more specific scope will limit the number of places your browser will send the cookie. If your cookie only needs to be sent with requests using the domain "app.company.com", then your scope should be that rather than the entire website "company.com". If you are using cookies to store the refresh token, you might encounter a `401 Unauthorized` when testing your app locally since `localhost` will differ from the custom domain you configure a `CNAME` for. To handle this, it's recommended that you follow this [guide](/unit-testing/local-testing-tokens) to configure multiple descope developer environments. # SAML Security (/security-best-practices/saml-security) How Descope secures the SAML exchange as the service provider, covering request signing, response encryption, certificates, and assertion validation. # SAML Security This page explains how Descope secures the SAML exchange between a tenant's identity provider (IdP) and Descope, which acts as the service provider (SP). ## Request signing Descope signs the SAML `AuthnRequest` it sends to the IdP with its private key. The IdP verifies that signature using Descope's public certificate, which it reads from the SP metadata URL (or a certificate you upload manually). By default Descope generates a key pair per tenant, and you can bring your own. See [SAML Signing and Encryption Keys](/management/tenant-management/sso/saml-signing). ## Response and assertion encryption Descope can decrypt encrypted SAML responses from the IdP using the tenant's private key. This lets the IdP encrypt the assertion in transit. The same [SAML Signing and Encryption Keys](/management/tenant-management/sso/saml-signing) page covers using Descope's keys or your own. ## Certificates Descope publishes its SP certificates in the tenant's metadata URL. If you rotate keys, make sure the IdP picks up the new certificate (via the metadata URL or a fresh manual upload). Expired or wrong-format certificates are a common cause of SSO failures, so check them first when debugging. See [SSO Troubleshooting](/other-troubleshooting/sso-troubleshooting). ## Assertion validation Descope validates the assertion before signing a user in, which protects against impersonation: - The email returned by the IdP must match the email that initiated the login. - The email domain must match the tenant's configured SSO domain. - The user must be associated with the requested SSO application. Assertions that fail these checks are rejected. See the [SSO error codes](/other-troubleshooting/sso-troubleshooting#sso-error-codes) (`E062020`, `E062021`, `E062023`) for the specific failures. ## Single logout (SLO) You cannot sign a user out of Descope using Single Logout (SLO) from a tenant's SSO provider. Signing out of the tenant's IdP does not end the Descope session, and ending the Descope session does not sign the user out of their IdP. Manage your application's session with [session management](/sessions) and sign users out of your own app session directly. # Storing Session Tokens (/security-best-practices/session-token-storage) A guide and overview of how session token storage works with Descope and how to ensure you manage it securely. # Storing Session Tokens Session tokens (JWTs) authenticate requests from a client to your backend. If one is compromised, an attacker can impersonate that user and reach protected resources — so how you store and expire them matters. ## Set Session Token Expiry Time Under [Session Management](https://app.descope.com/settings/project/session), **Session Token Timeout** controls how long a session token stays valid before it must be refreshed with a valid refresh token. A shorter timeout shrinks the window if a token is leaked, but means more frequent refreshes. Pick a value that balances security with how often you're willing to refresh for your users. ## Session Token Management In your [project configuration](https://app.descope.com/settings/project/session), you can deliver the session token in two ways: **manage in cookies**, or **manage in response body**. ### Managing with Cookies Managing session tokens in cookies requires a [custom domain](/how-to-deploy-to-production/custom-domain). When you manage session tokens in cookies, Descope sets them for you as secure `HttpOnly` cookies on your [custom domain](/how-to-deploy-to-production/custom-domain). Because JavaScript can't read `HttpOnly` cookies, this is the stronger option against XSS in production. Those cookies are also `SameSite=Strict` and `Secure` by default, which helps with CSRF protection and ensures they're only sent over HTTPS. You can adjust cookie policy under [Session Management](https://app.descope.com/settings/project/session) — see also [Cross-Site Cookies](/security-best-practices/crossite-cookies). Limit the cookie domain as much as you can. If the cookie only needs to go to `app.example.com`, scope it there rather than to the entire `example.com` site. For cookie names (`DS` by default) and how to customize them, see [Custom Cookie Names](/flows/actions/end-action#custom-cookie-names) on the End action. ![Descope manage session in cookies](/assets/manage-session-token-in-cookie.webp) If cookies are tied to your custom domain, `localhost` won't match and you may see `401 Unauthorized` while developing. Use [Local Testing Tokens](/unit-testing/local-testing-tokens) to set up separate developer environments. ### Managing with Response Body With this approach, the session token comes back in the API response and your app stores it — for example in memory or `localStorage`. That's convenient for local development and gives you direct control over the token lifecycle. The tradeoff is XSS exposure: anything in `localStorage` or reachable JS memory can be read by a malicious script. Prefer cookies for production web apps when you can. ![Descope manage session in response body](/assets/manage-session-token-in-response-body.webp) # Implementing ABAC (/authorization/abac/implement) Learn how to effortlessly implement Attribute-Based Access Control (ABAC) for your app with Descope. # Implementing ABAC ABAC is a way of deciding access based on attributes, and Descope gives you two places to evaluate those attributes. You can declare the attribute check inside the FGA schema as a CEL condition, which Descope evaluates as part of a relation or permission check, or you can store **custom attributes** on users and tenants and check them in your application code. The approach you pick depends on where the attribute comes from: - **In-schema conditions**: The attribute arrives with the request, such as an IP address, a country code, or the time of day. Descope evaluates it inside the check. - **Custom attributes in code**: The attribute is a durable fact about a user or tenant, such as a department or a subscription tier. Your code reads it and decides. From there, the custom attribute approach combines with the rest of your authorization model in three ways: 1. **Standalone ABAC**: Check custom attributes directly in your application code to make authorization decisions 2. **ABAC with RBAC**: Combine attribute checks with role-based permissions 3. **ABAC with ReBAC**: Check attributes first, then perform the ReBAC check ## In-Schema Conditions: Evaluating Attributes Inside the Check When the attribute is part of the request rather than something stored in Descope, you can express it as a condition in the FGA schema and attach it to a relation or permission with `with`. Descope then evaluates the relationship and the attribute together in a single check. ```yaml model AuthZ 1.0 condition IsAdmin(role string) { role == "admin" } type user type doc relation viewer: user with IsAdmin ``` Values for the condition's parameters are supplied at check time: ```javascript const checks = await descopeClient.management.fga.checkWithContext( [ { resource: 'doc-123', resourceType: 'doc', relation: 'viewer', target: 'u1', targetType: 'user', }, ], { role: 'admin' }, ); ``` Descope also ships [built-in constraints](/authorization/rebac/define-schema#built-in-constraints) for common attribute checks such as IP ranges, country codes, expiry timestamps, and numeric bounds, so those need no CEL of your own. For the full condition and constraint syntax, see [Defining a Schema](/authorization/rebac/define-schema#conditions-abac-with-common-expression-language). For the check-time context in every SDK, see [Checking Relations](/authorization/rebac/check-relations#check-a-relation-with-a-condition). ## Standalone ABAC: Checking Attributes in Code The simplest way to implement ABAC is to check custom attributes directly in your application code: ```javascript // Load user and check custom attributes const user = await descopeClient.management.user.load(loginId); // Check subscription tier const hasPremiumAccess = user.customAttributes?.subscriptionTier === 'premium'; // Check department const isInEngineering = user.customAttributes?.department === 'Engineering'; // Check multiple attributes const canAccessFeature = user.customAttributes?.subscriptionTier === 'premium' && user.customAttributes?.licenseStatus === 'active'; ``` ```python # Load user and check custom attributes user = descope_client.mgmt.user.load(login_id) # Check subscription tier custom_attrs = user.custom_attributes or {} has_premium_access = custom_attrs.get('subscriptionTier') == 'premium' # Check department is_in_engineering = custom_attrs.get('department') == 'Engineering' # Check multiple attributes can_access_feature = ( custom_attrs.get('subscriptionTier') == 'premium' and custom_attrs.get('licenseStatus') == 'active' ) ``` ```go // Load user and check custom attributes ctx := context.Background() user, err := descopeClient.Management.User().Load(ctx, loginID) // Check subscription tier tier, _ := user.CustomAttributes["subscriptionTier"].(string) hasPremiumAccess := tier == "premium" // Check department dept, _ := user.CustomAttributes["department"].(string) isInEngineering := dept == "Engineering" // Check multiple attributes licenseStatus, _ := user.CustomAttributes["licenseStatus"].(string) canAccessFeature := tier == "premium" && licenseStatus == "active" ``` ## ABAC with RBAC: Combining Attributes with Roles Combine ABAC attribute checks with RBAC role checks to create more granular authorization. This approach is ideal when you need role-based access with additional attribute constraints. ### Approach: Check Role and Attributes 1. **Load the user** and check their assigned roles 2. **Check custom attributes** to add additional conditions 3. **Combine both checks** to make the final authorization decision ### Example: Department-Based Document Editing Here's how to check if a user with the "Editor" role can edit a document in their department: ```javascript const checkCanEditDocument = async (loginId, documentId) => { // 1. Load user const user = await descopeClient.management.user.load(loginId); if (!user.ok) { return false; } // 2. RBAC: Check if user has Editor role const hasEditorRole = user.roleNames.includes('Editor'); // 3. ABAC: Check if user's department matches document's department const departmentMatch = user.customAttributes?.department === document.department; // 4. ABAC: Check if license is active const hasActiveLicense = user.customAttributes?.licenseStatus === 'active'; // 5. Combined authorization decision return hasEditorRole && departmentMatch && hasActiveLicense; }; ``` ```python def check_can_edit_document(login_id, document_id): # 1. Load user user = descope_client.mgmt.user.load(login_id) if not user: return False # 2. RBAC: Check if user has Editor role has_editor_role = 'Editor' in user.role_names # 3. ABAC: Check if user's department matches document's department custom_attrs = user.custom_attributes or {} department_match = ( custom_attrs.get('department') == document.department ) # 4. ABAC: Check if license is active has_active_license = ( custom_attrs.get('licenseStatus') == 'active' ) # 5. Combined authorization decision return has_editor_role and department_match and has_active_license ``` ```go func checkCanEditDocument(ctx context.Context, loginID string, documentID string) bool { // 1. Load user user, err := descopeClient.Management.User().Load(ctx, loginID) if err != nil { return false } // 2. RBAC: Check if user has Editor role hasEditorRole := false for _, role := range user.RoleNames { if role == "Editor" { hasEditorRole = true break } } // 3. ABAC: Check if user's department matches document's department dept, _ := user.CustomAttributes["department"].(string) departmentMatch := dept == document.Department // 4. ABAC: Check if license is active licenseStatus, _ := user.CustomAttributes["licenseStatus"].(string) hasActiveLicense := licenseStatus == "active" // 5. Combined authorization decision return hasEditorRole && departmentMatch && hasActiveLicense } ``` ## ABAC with ReBAC: Checking Attributes Before ReBAC When an attribute is stored on the user or tenant, you can read it first and only perform the ReBAC check if it passes. This combines attribute-based filtering with relationship-based access without requiring you to maintain relations that mirror attributes. If the attribute instead arrives with the request, the in-schema condition approach described above is usually the better fit, since the rule lives with the schema and applies to every caller. Check out the [ReBAC Docs](/authorization/rebac) to learn about creating a schema and relations. ### Approach: Check Attributes, Then ReBAC 1. **Load the user** and check their custom attributes 2. **If attributes pass**, perform the ReBAC check to verify relationship-based access 3. **Use the DSL** to define permissions and leverage relation inheritance This approach ensures you're using the DSL schema properly while incorporating attribute-based filtering without maintenance overhead. ### Example: Department-Based Access Here's how to check if a user in a specific department can access a document: ```javascript const checkDepartmentDocumentAccess = async (loginId, documentId) => { // 1. Load user and check department attribute const user = await descopeClient.management.user.load(loginId); if (!user.ok) { return false; } // ABAC: Check if user is in Engineering department if (user.customAttributes?.department !== 'Engineering') { return false; // Attribute check failed } // 2. If attributes pass, check ReBAC relation const canView = await descopeClient.management.fga.check([{ resource: documentId, resourceType: 'doc', relation: 'can_view', target: user.userId, targetType: 'user', }]); return canView[0].allowed; }; ``` ```python def check_department_document_access(login_id, document_id): # 1. Load user and check department attribute user = descope_client.mgmt.user.load(login_id) if not user: return False # ABAC: Check if user is in Engineering department custom_attrs = user.custom_attributes or {} if custom_attrs.get('department') != 'Engineering': return False # Attribute check failed # 2. If attributes pass, check ReBAC relation can_view = descope_client.mgmt.fga.check([ { "resource": document_id, "resourceType": "doc", "relation": "can_view", "target": user.user_id, "targetType": "user", } ]) return can_view[0]["allowed"] ``` ```go func checkDepartmentDocumentAccess(ctx context.Context, loginID string, documentID string) bool { // 1. Load user and check department attribute user, err := descopeClient.Management.User().Load(ctx, loginID) if err != nil { return false } // ABAC: Check if user is in Engineering department dept, _ := user.CustomAttributes["department"].(string) if dept != "Engineering" { return false // Attribute check failed } // 2. If attributes pass, check ReBAC relation canView, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: documentID, ResourceType: "doc", Relation: "can_view", Target: user.UserID, TargetType: "user", }, }) if err != nil { return false } return canView[0].Allowed } ``` # Overview (/authorization/abac) Learn how Descope supports Attribute-Based Access Control (ABAC) using schema conditions and custom attributes for fine-grained authorization decisions. # Attribute-Based Access Control Attribute-Based Access Control (ABAC) is an authorization model, which means it is a way of deciding access rather than a separate service you turn on. Each of Descope's authorization models answers a different question: - **RBAC** asks what role the user holds. - **ReBAC** asks how the user is related to the resource. - **ABAC** asks what is true about the user, the resource, or the request at the moment of the decision. Attributes are the facts that answer that last question. They can describe the user (department, clearance level, license status, subscription tier), the tenant (organization type, paid tier, region, compliance level), or the request itself (IP address, country, time of day). Because ABAC answers a different question than the other models, it composes with them instead of replacing them. You can use ABAC on its own, layer it on top of RBAC roles, or attach it directly to ReBAC relations and permissions. ABAC is part of Descope's **Fine-Grained Authorization (FGA)** system, alongside [ReBAC](/authorization/rebac) and usable together with [RBAC](/authorization/role-based-access-control). ## Two Ways to Implement ABAC Descope supports two ways to apply attributes to an authorization decision. Both are ABAC. They differ in where the attribute comes from and where the decision gets evaluated. | | In-schema conditions (CEL) | Custom attributes in your code | |---|---|---| | **Where the attribute comes from** | Supplied with the check request as context | Stored on the user or tenant in Descope | | **Where the decision happens** | Inside the FGA check, as part of the schema | In your application code | | **How it is defined** | `condition` and `constraint` declarations in the FGA schema DSL | Custom attribute definitions in the Descope Console | | **Best suited for** | Request-time context such as IP, geography, time, or a role claim gating a relation | Durable facts about a user or tenant | | **Combines with** | ReBAC relations and permissions | RBAC, ReBAC, or on its own | Nothing stops you from using both in the same application. A schema condition can gate the ReBAC check while your code separately reads a stored custom attribute for a decision that never reaches the authz service. ### In-Schema Conditions with CEL When the attribute arrives with the request, you can express the attribute check inside the FGA schema itself using [CEL (Common Expression Language)](https://cel.dev/). A `condition` takes typed parameters and evaluates a CEL expression, and you attach it to a relation or permission with `with`: ```yaml model AuthZ 1.0 condition IsAdmin(role string) { role == "admin" } type user type doc relation viewer: user with IsAdmin ``` Here `viewer` is the ReBAC relation and `with IsAdmin` is the ABAC gate on top of it. The relation only holds if the caller supplies a `role` of `admin` at check time. Descope also ships [built-in constraints](/authorization/rebac/define-schema#built-in-constraints) for common attribute checks such as IP ranges, country codes, expiry timestamps, and numeric bounds, so you can attach those without writing CEL by hand. The advantage of this approach is that the attribute logic lives with the rest of your authorization model and is evaluated by Descope in a single check, so your application code does not need to know which relations carry attribute constraints. For the full syntax, see [Conditions (ABAC with Common Expression Language)](/authorization/rebac/define-schema#conditions-abac-with-common-expression-language). To pass values at check time, see [checking a relation with a condition](/authorization/rebac/check-relations#check-a-relation-with-a-condition). ### Custom Attributes Checked in Your Code When the attribute is a durable fact about a user or tenant, you can store it as a custom attribute in Descope and check it in your application code. You would: 1. **Define the custom attribute** in the Descope Console: - For users: [Users > Custom Attributes](https://app.descope.com/users/attributes) - For tenants: [Tenants > Custom Attributes](https://app.descope.com/tenants/attributes) 2. **Set attribute values** on users or tenants through the [Management SDK](/management/user-management/sdks) or the Console. 3. **Check those attributes** in your application code to make the authorization decision, either on their own or alongside an RBAC role check or a ReBAC check. ### How the Custom Attribute Approach Compares to ReBAC | Feature | ReBAC | ABAC with custom attributes | |---------|-------|------| | **Schema/DSL** | Uses the schema DSL to define types, relations, and permissions | No schema needed, since the decision reads attribute values | | **Relations** | Creates explicit relation tuples between users and resources | Checks attribute values in code | | **Implementation** | Define schema, create relations, check permissions | Set attributes, check attributes in code | | **Use Case** | Resource-specific access, such as "user owns document X" | Attribute-based access, such as "users in department Y can access" | Note that in-schema CEL conditions sit in the ReBAC column of this table as far as mechanics go, since they are declared in the DSL and evaluated by our FGA service. They are still ABAC in terms of what they express. ## When to Use ABAC ABAC fits when the authorization decision depends on: - **User Characteristics**: Department, clearance level, license status, subscription tier - **Tenant Characteristics**: Organization type, paid tier, geographical region - **Dynamic Conditions**: Time-based access, location-based access, status-based access - **Compliance Requirements**: Regulatory compliance based on user or tenant attributes - **Complementing RBAC**: Adding attribute checks to role-based permissions, such as "Editor role AND department match" - **Complementing ReBAC**: Gating a relation or permission with a condition, or filtering by attributes before a check ## How ABAC Relates to RBAC and ReBAC ABAC can be used **standalone** or **in combination** with RBAC and ReBAC. Here are the common patterns. ### ABAC with ReBAC There are two ways to bring attributes into a relationship-based decision, and which one fits depends on where the attribute lives. **Option 1: Attach a condition in the schema.** When the attribute arrives with the request, such as an IP address, a country code, or the current time, the cleanest approach is a CEL condition or a built-in constraint attached to the relation or permission. Descope evaluates the relationship and the attribute together in one check: ```yaml model AuthZ 1.0 constraint GeoCountry("US", "GB") condition DuringBusinessHours(hour int) { hour >= 9 && hour < 17 } type user type doc relation viewer: user with GeoCountry relation editor: user with DuringBusinessHours ``` The values are then supplied with the check: ```javascript const canView = await descopeClient.management.fga.checkWithContext( [ { resource: 'engineering-roadmap', resourceType: 'doc', relation: 'viewer', target: 'u1', targetType: 'user', }, ], { country_code: 'US' }, ); return canView[0].allowed; ``` See [Defining a Schema](/authorization/rebac/define-schema#conditions-abac-with-common-expression-language) for the condition syntax and [Checking Relations](/authorization/rebac/check-relations#check-a-relation-with-a-condition) for how context is passed. **Option 2: Check stored attributes before the ReBAC check.** When the attribute is stored on the user or tenant, you can read it first and only perform the ReBAC check if it passes. This keeps attribute-based filtering out of the schema and avoids maintaining relations that mirror attributes. **Example: Department-Based Document Access** Check if a user in the "Engineering" department can view a document: ```javascript // 1. Check user's department attribute (ABAC) const user = await descopeClient.management.user.load(loginId); if (user.customAttributes?.department !== 'Engineering') { return false; // Attribute check failed } // 2. If attributes pass, check ReBAC relation const canView = await descopeClient.management.fga.check([{ resource: 'engineering-roadmap', resourceType: 'doc', relation: 'can_view', target: user.userId, targetType: 'user', }]); return canView[0].allowed; ``` **Example: Time-Based Access with ReBAC** Grant access to a document only if the user is in the correct department and it is during business hours: ```javascript const user = await descopeClient.management.user.load(loginId); const currentHour = new Date().getHours(); // ABAC checks const departmentMatch = user.customAttributes?.department === 'Engineering'; const isBusinessHours = currentHour >= 9 && currentHour < 17; if (!departmentMatch || !isBusinessHours) { return false; } // ReBAC check const canView = await descopeClient.management.fga.check([{ resource: 'engineering-roadmap', resourceType: 'doc', relation: 'can_view', target: user.userId, targetType: 'user', }]); return canView[0].allowed; ``` A time check like this one is a good candidate for moving into the schema as a CEL condition, since the hour is request-time context rather than a stored fact. Keeping it in the schema means every caller gets the same rule without each of them remembering to apply it. ### ABAC with RBAC Attributes can also add conditions to role-based permissions. This pattern suits cases where a role grants broad access and attributes narrow it. **Example: Healthcare Records Access** A doctor can only access patient records if they meet multiple conditions: - They have the "Doctor" role (RBAC) - Their department matches the patient's department (ABAC) - Their medical license status is "active" (ABAC) - Their clearance level meets or exceeds the record's sensitivity level (ABAC) ```javascript // Check RBAC role const hasDoctorRole = user.roleNames.includes('Doctor'); // Check ABAC attributes const departmentMatch = user.customAttributes.department === patient.department; const hasActiveLicense = user.customAttributes.licenseStatus === 'active'; const hasRequiredClearance = user.customAttributes.clearanceLevel >= record.sensitivityLevel; // Combined authorization decision const canAccessRecord = hasDoctorRole && departmentMatch && hasActiveLicense && hasRequiredClearance; ``` **Example: Department-Based Document Editing** An editor can only edit documents in their department: - They have the "Editor" role (RBAC) - Their department matches the document's department (ABAC) - Their account status is "active" (ABAC) ```javascript const canEdit = user.roleNames.includes('Editor') && user.customAttributes.department === document.department && user.customAttributes.accountStatus === 'active'; ``` ### Standalone ABAC When authorization is purely attribute-based, with no roles or relations involved, ABAC can stand on its own. This suits feature flags, subscription tiers, and other simple attribute gates. **Example: Subscription-Based Feature Access** Allow access to premium features based on subscription tier: ```javascript const canAccessPremiumFeature = user.customAttributes.subscriptionTier === 'premium' || user.customAttributes.subscriptionTier === 'enterprise'; ``` **Example: Multi-Tenant Feature Access** Control feature access based on tenant attributes: ```javascript const tenant = await descopeClient.management.tenant.load(tenantId); // Premium features only for premium tier tenants const canAccessPremiumFeature = tenant.customAttributes.subscriptionTier === 'premium'; // Geographic restrictions const canAccessRegionalFeature = tenant.customAttributes.region === 'US' || tenant.customAttributes.region === 'EU'; // Compliance-based features const canAccessHIPAAFeature = tenant.customAttributes.complianceLevel === 'HIPAA'; ``` **Example: Time-Based Access Control** Restrict access based on time attributes: ```javascript const currentHour = new Date().getHours(); const userShift = user.customAttributes.shift; // 'morning', 'afternoon', 'night' // Only allow access during user's shift const canAccess = (userShift === 'morning' && currentHour >= 6 && currentHour < 14) || (userShift === 'afternoon' && currentHour >= 14 && currentHour < 22) || (userShift === 'night' && (currentHour >= 22 || currentHour < 6)); ``` ## Next Continue to [implementing ABAC](/authorization/abac/implement) for detailed code examples and best practices. # Checking Relations (/authorization/rebac/check-relations) Learn how to implement relations checking with Descope, a comprehensive access control solution. # Checking Relations Now that you've created your [definitions](/authorization/rebac/define-schema) and [relations](/authorization/rebac/create-relations), you can check them to determine access control. Most of our SDKs provide a `check` function, which is the primary method for checking relations. This function checks if the given relations are satisfied and returns detailed results. ## Check if relation exists The `check` function checks if the given relations are satisfied. Pass in an array of `FGARelation` objects and the function returns an array of `FGACheck` results indicating whether each relation is allowed. ```javascript const relations = [ { resource: 'some-doc', resourceType: 'doc', relation: 'can_view', target: 'u1', targetType: 'user', }, ]; const checks = await descopeClient.management.fga.check(relations); // checks[0].allowed indicates if the relation is satisfied ``` ```python # Check if the given relations are satisfied # Args: # relations (List[dict]): List of relation queries each in the format of: # { # "resource": "id of the resource that has the relation", # "resourceType": "the type of the resource (namespace)", # "relation": "the relation definition for the relation", # "target": "the target that has the relation - usually users or other resources", # "targetType": "the type of the target (namespace)" # } # # Return value (List[dict]): # Return List in the format # [ # { # "allowed": True|False, # "relation": { # "resource": "id of the resource that has the relation", # "resourceType": "the type of the resource (namespace)", # "relation": "the relation definition for the relation", # "target": "the target that has the relation - usually users or other resources", # "targetType": "the type of the target (namespace)" # } # } # ] # Raise: # AuthException: raised if query fails checks = descope_client.mgmt.fga.check( [ { "resource": "some-doc", "resourceType": "doc", "relation": "viewer", "target": "u1", "targetType": "user", } ] ) # checks[0]["allowed"] indicates if the relation is satisfied ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() checks, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "some-doc", ResourceType: "doc", Relation: "can_view", Target: "u1", TargetType: "user", }, }) if err != nil { // handle error } // checks[0].Allowed indicates if the relation is satisfied ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); List relations = Arrays.asList( new FGARelation("some-doc", "doc", "can_view", "u1", "user") ); try { List results = fs.check(relations); for (FGACheckResult result : results) { // result.isAllowed() indicates if the relation is satisfied } } catch (DescopeException de) { // Handle the error } ``` ### Check a relation with a condition If the relation or permission being checked has a [condition](/authorization/rebac/define-schema#conditions-abac-with-common-expression-language) attached via `with`, you can supply the values that condition's CEL expression should evaluate against. ```javascript const relations = [ { resource: 'doc-123', resourceType: 'doc', relation: 'viewer', target: 'u1', targetType: 'user', }, ]; const context = { role: 'admin' }; const checks = await descopeClient.management.fga.checkWithContext(relations, context); // checks[0].allowed indicates if the relation is satisfied given this context ``` ```go // Utilizing context within the Descope Go SDK is supported in versions 1.6.0 and higher. ctx := context.Background() extraContext := map[string]any{ "role": "admin", } checks, err := descopeClient.Management.FGA().CheckWithContext(ctx, []*descope.FGARelation{ { Resource: "doc-123", ResourceType: "doc", Relation: "viewer", Target: "u1", TargetType: "user", }, }, extraContext) if err != nil { // handle error } // checks[0].Allowed indicates if the relation is satisfied given this context // checks[0].Info.Conditional is true if a CEL condition decided the result // checks[0].Info.MissingContext lists any evaluated condition parameters that weren't supplied // checks[0].Info.ConditionalErr holds a CEL evaluation error message, if the condition couldn't be evaluated ``` Even if a parameter is missing, the condition can still evaluate to `true` as long as short-circuit evaluation prevents that parameter from being evaluated. For example, in an inclusive OR (`||`) expression, if any operand evaluates to `true`, the remaining operands don't need to be evaluated, including ones with missing values. In that case, a missing parameter has no effect on the result. ## Check who can access a particular resource The `whoCanAccess` function takes in a resource, a relation definition, and a namespace corresponding to the relation definition and returns an array of users with the given relation definitions to the resource. ```javascript const resource: string = "The resource of a relation"; const relationDefinition: string = "The relation definition name"; const namespace: string = "The relation definition namespace"; const users: string[] = await descopeClient.management.authz.whoCanAccess(resource, relationDefinition, namespace); ``` ```python # Finds the list of targets (usually users) who can access the given resource with the given RD # Args: # resource (str): the resource we are checking # relation_definition (str): the RD we are checking # namespace (str): the namespace for the RD # Return value (List[str]): list of targets (user IDs usually that have the access) # Raise: # AuthException: raised if query fails client.mgmt.authz.who_can_access("a", "b", "c") ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() mgmt.Authz().WhoCanAccess(ctx, "r", "rd", "") ``` ```java var respWho = authzService.whoCanAccess("roadmap.ppt", "editor", "doc"); ``` ### Check what relations a resource has The `resourceRelations` function returns an array of relations for a given resource. ```javascript const resource: string = "The resource for the relations"; const relations: AuthzRelation[] = descopeClient.management.authz.resourceRelations(resource); ``` ```python # Returns the list of all defined relations (not recursive) on the given resource. # Args: # resource (str): the resource we are listing relations for # Return value (List[dict]): # Return List of relations each in the format of a relation as documented in create_relations # Raise: # AuthException: raised if query fails client.mgmt.authz.resource_relations("a") ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := mgmt.Authz().ResourceRelations(ctx, "r") ``` ```java var respResourceRelations = authzService.resourceRelations("roadmap.ppt"); ``` ### Check what relations a resource has (with target set filter) ```javascript const resource: string = "The resource for the relations"; // Get relations including target set relations (default) const relations: AuthzRelation[] = await descopeClient.management.authz.resourceRelations(resource); // Get relations excluding target set relations const relationsWithoutTargetSets: AuthzRelation[] = await descopeClient.management.authz.resourceRelations(resource, true); ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. // resource: the resource to get relations for // includeTargetSetRelations: whether to include relations where the target is a set (group) ctx := context.Background() // Get relations including target set relations res, err := descopeClient.Management.Authz().ResourceRelationsWithTargetSetsFilter(ctx, "r", true) // Get relations excluding target set relations res, err := descopeClient.Management.Authz().ResourceRelationsWithTargetSetsFilter(ctx, "r", false) ``` ### Check what relations a target has directly The `targetsRelations` function takes an array of targets (eg. users) and returns an array of relations that [exist for the target directly](/authorization/rebac#direct-and-implied-relations). This will **NOT** return relations that exist for the target implicitly, such as through a parent node with a matching relation definition. ```javascript const targets: string[] = ["The targets to check relations for directly."]; const relations: AuthzRelation[] = descopeClient.management.authz.targetsRelations(targets); ``` ```python # Returns the list of all defined relations (not recursive) for the given targets. # Args: # targets (List[str]): the list of targets we are returning the relations for # Return value (List[dict]): # Return List of relations each in the format of a relation as documented in create_relations # Raise: # AuthException: raised if query fails client.mgmt.authz.targets_relations(["a"]) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := mgmt.Authz().TargetsRelations(ctx, []string{"u1"}) ``` ```java var respUsersRelations = authzService.targetsRelations(List.of("u1")); ``` ### Check what relations a target has directly (with target set filter) ```javascript const targets: string[] = ["The targets to check relations for directly."]; // Get relations excluding target set relations (default) const relations: AuthzRelation[] = await descopeClient.management.authz.targetsRelations(targets); // Get relations including target set relations const relationsWithTargetSets: AuthzRelation[] = await descopeClient.management.authz.targetsRelations(targets, true); ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. // targets: the list of targets to get relations for // includeTargetSetRelations: whether to include relations where the target is a set (group) ctx := context.Background() // Get relations including target set relations res, err := descopeClient.Management.Authz().TargetsRelationsWithTargetSetsFilter(ctx, []string{"u1"}, true) // Get relations excluding target set relations res, err := descopeClient.Management.Authz().TargetsRelationsWithTargetSetsFilter(ctx, []string{"u1"}, false) ``` ### Check what relations a target has directly and implicitly (recursive) The `whatCanTargetAccess` function is similar to the `targetsRelations` function in that it takes a single target and returns an array of relations that exist for it, but instead of simply including direct relations, it returns implicit ones as well, traversing the tree of relations recursively. ```javascript const target: string = "The target to check relations for directly and implicitly."; const relations: AuthzRelation[] = descopeClient.management.authz.whatCanTargetAccess(target); ``` ```python # Returns the list of all relations for the given target including derived relations from the schema tree. # Args: # target (str): the target we are returning the relations for # Return value (List[dict]): # Return List of relations each in the format of a relation as documented in create_relations # Raise: # AuthException: raised if query fails ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() _, err = mgmt.Authz().WhatCanTargetAccess(ctx, "u1") ``` ```java var respWhat = authzService.whatCanTargetAccess("u1"); ``` ## Check what resources a target can access through a particular relation definition The `whatCanTargetAccessWithRelation` function is similar to `whatCanTargetAccess`, but instead of returning all relations for a given target, it specifically finds resources that the target can access through relation paths ending in a given relation definition. ```javascript const target: string = "The target to check relations for directly and implicitly."; const resources: AuthzResource[] = await descopeClient.management.authz.whatCanTargetAccessWithRelation( target, relationDefinition, namespace, ); ``` ```python # Returns the list of all resources that the target has the given relation to including all derived relations # Args: # target (str): the target we are returning the relations for # relation_definition (str): the RD we are checking # namespace (str): the namespace for the RD # Return value (List[dict]): # Return List of relations each in the format of a relation as documented in create_relations # Raise: # AuthException: raised if query fails resources = client.management.authz.what_can_target_access_with_relation( [ { "target": "u1", "relationDefinition": "viewer", "namespace": "doc", } ] ) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() _, err = mgmt.Authz().WhatCanTargetAccessWithRelation(ctx, "u1", "rd", "") ``` ```java var respWhat = authzService.whatCanTargetAccessWithRelation("u1", "rd", "ns"); ``` ## Get Modified Resources and Targets The `getModified` function returns a list of targets and resources that have changed since a given date. This is useful for invalidating local caches and keeping your application's authorization data in sync. `getModified` returns changes for up to 1,000 modified targets and resources. If more changes exist since the given date, the call fails with an error instead of returning a partial or paginated result. Treat that error as a signal to do a full resync rather than retrying with the same `since` value. ```javascript // Get all changes since a specific date const since = new Date('2024-01-01T00:00:00Z'); const modified = await descopeClient.management.authz.getModified(since); // Returns: { resources: string[], targets: string[], schemaChanged: boolean } ``` ```python # Get all targets and resources changed since the given date. # Args: # since (datetime): optional, only return changes from this given datetime # Return value (dict): # Dict including "resources" list of strings, "targets" list of strings and "schemaChanged" bool # Raise: # AuthException: raised if query fails from datetime import datetime, timezone since = datetime(2024, 1, 1, tzinfo=timezone.utc) modified = descope_client.mgmt.authz.get_modified(since) # Returns: {"resources": ["r1", "r2"], "targets": ["u1", "u2"], "schemaChanged": False} ``` ```go // Get all changes since a specific date // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. // since: time to get changes since (must be within last 24 hours) ctx := context.Background() since := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) modified, err := descopeClient.Management.Authz().GetModified(ctx, since) // Returns: &AuthzModified{Resources: []string{"r1", "r2"}, Targets: []string{"u1", "u2"}, SchemaChanged: false} ``` ```java // Get all changes since a specific date import java.time.Instant; Instant since = Instant.parse("2024-01-01T00:00:00Z"); AuthzModified modified = authzService.getModified(since); // Returns: AuthzModified with resources, targets, and schemaChanged fields ``` # Creating Relations (/authorization/rebac/create-relations) Learn how to create, update, filter, and delete relations in Descope's Fine-Grained Authorization (FGA) system. # Creating Relations After you've [defined your schema](/authorization/rebac/define-schema) and [implemented it in Descope](/authorization/rebac/implement-schema), you can start creating relations between resources and targets. Relations are the actual data that represents who has access to what resources in your authorization model. ## Prerequisites Before creating relations, make sure you have: 1. [Defined your FGA schema](/authorization/rebac/define-schema) with relation definitions 2. [Implemented your schema](/authorization/rebac/implement-schema) 3. Installed and initialized the Descope Management SDK (see setup below) ## Data Types When working with relations, you'll use the following data types: ### FGARelation The `FGARelation` type defines a relation between a resource and a target: ```typescript type FGARelation = { resource: string; // The resource identifier (e.g., "engineering-team", "project-plan.pdf") resourceType?: string; // The resource type/namespace (e.g., "Group", "File", "Folder") relation: string; // The relation name from your schema (e.g., "member", "owner", "viewer") target: string; // The target identifier, usually a user ID (e.g., "U2abc123def456") targetType?: string; // The target type/namespace (e.g., "user") }; ``` ### CheckResponseRelation When checking relations (covered in [Checking Relations](/authorization/rebac/check-relations)), the response includes: ```typescript type CheckResponseRelation = { allowed: boolean; // Whether the relation is allowed tuple: FGARelation; // The relation that was checked }; ``` ## Creating Relations You can create relations either through the Descope Console or programmatically using the SDKs. ### Using the Console A [saved schema](/authorization/rebac/implement-schema) defines the types relations can reference, so **Create Relation** stays disabled until one exists. If no relations exist yet, the Relations tab shows an empty state instead of the list. You can create relations directly in the Descope Console: 1. Navigate to **Authorization** > **FGA** > **Relations** 2. Click **Create Relation** 3. Fill in the relation details: - **Resource Type**: The type of resource (e.g., `Group`, `File`, `Folder`) - **Resource Name**: The identifier for the specific resource (e.g., `engineering-team`, `project-plan.pdf`) - **Relation**: The relation name from your schema (e.g., `member`, `owner`, `viewer`) - **Target Type**: The type of target (e.g., `user`) - **Target**: The target identifier, usually a user ID (e.g., `U2abc123def456`) Here's an example of creating a relation for a user as a member of a group: ![Create Relation](/assets/create-relation.webp) #### Filtering Relations The Smart Filter matches direct relations only. For example, if you filter by a document, the results won't include users who can access that document only through a parent folder. You can filter relations in the Descope Console by resource or target instead of scrolling through the full list: Here's an example of creating a filter: ![Create Relation](/assets/fga-relations-add-filter.webp) ### Using the SDK Use `createRelations` to create one or more relations programmatically. This function accepts an array of `FGARelation` objects and creates them all in a single operation. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Initialize Management SDK Initialize the Descope Management SDK with your project ID and management key. The management key must have FGA read/write permissions. ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try { // Optional: configure baseUrl for custom domain // baseUrl="https://auth.company.com" const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import DescopeClient try: # Optional: configure baseURL via environment variable # export DESCOPE_BASE_URI="https://auth.company.com" descope_client = DescopeClient( project_id='__ProjectID__', management_key="xxxx" ) except Exception as error: # handle the error print("failed to initialize. Error:") print(error) ``` ```go import ( "context" "github.com/descope/go-sdk/descope/client" ) managementKey := "xxxx" // Optional: configure baseUrl in client.Config // DescopeBaseURL: "https://auth.company.com" descopeClient, err := client.NewWithConfig(&client.Config{ ProjectID: "__ProjectID__", ManagementKey: managementKey, }) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client.DescopeClient; import com.descope.client.Config; // Option 1: Using environment variables // Set DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY var descopeClient = new DescopeClient(); // Option 2: Direct configuration var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Create Relations Programmatically ```javascript // Example: Add a user as a member of a group const relations = [ { resource: 'engineering-team', resourceType: 'Group', relation: 'member', target: 'U2abc123def456', targetType: 'user', }, ]; await descopeClient.management.fga.createRelations(relations); ``` ```python # Example: Add a user as a member of a group descope_client.mgmt.fga.create_relations([ { "resource": "engineering-team", "resourceType": "Group", "relation": "member", "target": "U2abc123def456", "targetType": "user", } ]) ``` ```go ctx := context.Background() // Example: Add a user as a member of a group err := descopeClient.Management.FGA().CreateRelations(ctx, []*descope.FGARelation{ { Resource: "engineering-team", ResourceType: "Group", Relation: "member", Target: "U2abc123def456", TargetType: "user", }, }) if err != nil { // handle error } ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); // Example: Add a user as a member of a group List relations = Arrays.asList( new FGARelation("engineering-team", "Group", "member", "U2abc123def456", "user") ); try { fs.createRelations(relations); } catch (DescopeException de) { // Handle the error } ``` ## Deleting Relations ### Delete Specific Relations Use `deleteRelations` to remove specific relations. This function accepts an array of `FGARelation` objects and deletes all matching relations. ```javascript // Example: Remove a user from a group const relations = [ { resource: 'engineering-team', resourceType: 'Group', relation: 'member', target: 'U2abc123def456', targetType: 'user', }, ]; await descopeClient.management.fga.deleteRelations(relations); ``` ```python # Example: Remove a user from a group descope_client.mgmt.fga.delete_relations([ { "resource": "engineering-team", "resourceType": "Group", "relation": "member", "target": "U2abc123def456", "targetType": "user", } ]) ``` ```go ctx := context.Background() // Example: Remove a user from a group err := descopeClient.Management.FGA().DeleteRelations(ctx, []*descope.FGARelation{ { Resource: "engineering-team", ResourceType: "Group", Relation: "member", Target: "U2abc123def456", TargetType: "user", }, }) if err != nil { // handle error } ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); // Example: Remove a user from a group List relations = Arrays.asList( new FGARelation("engineering-team", "Group", "member", "U2abc123def456", "user") ); try { fs.deleteRelations(relations); } catch (DescopeException de) { // Handle the error } ``` ### Delete All Relations Currently, this function is only available in the Node.js SDK. Use `deleteAllRelations` to delete all relations in your project. **Warning**: This is a destructive operation that cannot be undone. ```javascript // Delete all relations in the project await descopeClient.management.fga.deleteAllRelations(); ``` ## Resource Details Resource details allow you to store additional metadata about resources that can be used for filtering, display, or other purposes. This is optional metadata that doesn't affect authorization decisions but can be useful for building user interfaces or generating reports. ### Load Resource Details Use `loadResourcesDetails` to retrieve metadata for one or more resources. ```javascript const resourceIdentifiers = [ { resource: 'doc-1', resourceType: 'doc' }, { resource: 'doc-2', resourceType: 'doc' } ]; const resourceDetails = await descopeClient.management.fga.loadResourcesDetails(resourceIdentifiers); ``` ```python resource_details = descope_client.mgmt.fga.load_resources_details([ {"resource": "doc-1", "resourceType": "doc"}, {"resource": "doc-2", "resourceType": "doc"} ]) ``` ```go ctx := context.Background() resourceIdentifiers := []*descope.ResourceIdentifier{ {Resource: "doc-1", ResourceType: "doc"}, {Resource: "doc-2", ResourceType: "doc"}, } resourceDetails, err := descopeClient.Management.FGA().LoadResourcesDetails(ctx, resourceIdentifiers) if err != nil { // handle error } ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); List identifiers = Arrays.asList( new FGAResourceIdentifier("doc-1", "doc"), new FGAResourceIdentifier("doc-2", "doc") ); try { List details = fs.loadResourcesDetails(identifiers); for (FGAResourceDetails detail : details) { // Access detail.getDisplayName(), detail.getDescription(), etc. } } catch (DescopeException de) { // Handle the error } ``` ### Save Resource Details Use `saveResourcesDetails` to store or update metadata for resources. This metadata can include display names, descriptions, and custom metadata fields. ```javascript const resourcesDetails = [ { resource: 'doc-1', resourceType: 'doc', displayName: 'Document 1', description: 'First document', metadata: { category: 'important' } }, { resource: 'doc-2', resourceType: 'doc', displayName: 'Document 2', description: 'Second document', metadata: { category: 'normal' } } ]; await descopeClient.management.fga.saveResourcesDetails(resourcesDetails); ``` ```python descope_client.mgmt.fga.save_resources_details([ { "resource": "doc-1", "resourceType": "doc", "displayName": "Document 1", "description": "First document", "metadata": {"category": "important"} }, { "resource": "doc-2", "resourceType": "doc", "displayName": "Document 2", "description": "Second document", "metadata": {"category": "normal"} } ]) ``` ```go ctx := context.Background() resourcesDetails := []*descope.ResourceDetails{ { Resource: "doc-1", ResourceType: "doc", DisplayName: "Document 1", Description: "First document", Metadata: map[string]interface{}{"category": "important"}, }, { Resource: "doc-2", ResourceType: "doc", DisplayName: "Document 2", Description: "Second document", Metadata: map[string]interface{}{"category": "normal"}, }, } err := descopeClient.Management.FGA().SaveResourcesDetails(ctx, resourcesDetails) if err != nil { // handle error } ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); List resourceDetails = Arrays.asList( new FGAResourceDetails("doc-1", "doc", "Document 1"), new FGAResourceDetails("doc-2", "doc", "Document 2") ); try { fs.saveResourcesDetails(resourceDetails); } catch (DescopeException de) { // Handle the error } ``` ## Next Steps After creating relations, you can: - [Check relations](/authorization/rebac/check-relations) to verify access permissions - Use the relations in your application logic to enforce authorization # Defining a Schema (/authorization/rebac/define-schema) Learn how to define a ReBAC (Relationship-Based Access Control) schema for your application with Descope. # Defining a Schema The ReBAC schema is comprised of three main components: - **Namespaces**: Top-level entities in your application, like documents, folders, and organizations - **Relation definitions**: Define the possible relationships between namespaces, like a user being a member of an organization - **Relations**: The actual relationships between specific entities, like a specific user being a member of a particular organization The general flow of defining a schema involves creating namespaces and relation definitions. ## Schema Templates The templates will only appear if you do not have a pre-existing schema present. You can remove the schema by clicking the delete (trashcan) button in the top right corner of the schema editor. The Descope Console provides several pre-built schema templates to help you get started quickly. You can access these templates in the Console under the **Schema** tab of the [FGA](https://app.descope.com/authorization/fga) page. ![Schema Templates](/assets/schema-templates.webp) ## Understanding the DSL (Domain-Specific Language) The ReBAC schema uses a Domain-Specific Language (DSL) to define your authorization model. Understanding the DSL syntax is essential for creating effective schemas. ### Schema Structure Every schema begins with a model declaration: ```yaml model AuthZ 1.0 ``` This declares that you're using the AuthZ 1.0 model format. ### Types Types represent the entities in your system. They are the building blocks of your authorization model: ```yaml type user type account type folder ``` Types can be simple (like `user`) or complex with relations and permissions (like `account` or `folder`). ### Relations Relations define direct relationships between entities. They specify who or what can have a particular relationship with a resource. #### Basic Relations ```yaml type account relation owner: user relation manager: user ``` This defines that an `account` can have an `owner` relation to a `user`, and a `manager` relation to a `user`. #### Union Relations (OR) You can define that a relation can be satisfied by multiple types using the union operator (`|`): ```yaml type account relation owner: user | group ``` This means an account's owner can be either a `user` or a `group`. #### Relation References You can reference relations from other types using the `#` operator: ```yaml type account relation owner: user | Group#member ``` This means an account's owner can be: - A direct `user` - A `Group` that has a `member` relation (which resolves to users who are members of that group) #### Self-Referential Relations Types can reference themselves to create hierarchical structures: ```yaml type folder relation parent: folder ``` This allows folders to have parent folders, creating a folder hierarchy. ### Permissions Permissions define what actions can be performed. They are computed from relations and can reference other permissions. #### Basic Permissions ```yaml type account relation owner: user permission can_close: owner ``` This defines a `can_close` permission that is granted to the account's owner. #### Permission Composition (OR) Permissions can be composed using the union operator (`|`): ```yaml type account relation owner: user relation manager: user permission can_withdraw: owner | manager ``` This means `can_withdraw` is granted if the user is either the owner OR the manager. #### Permission Chaining Permissions can reference other permissions: ```yaml type account relation owner: user relation manager: user permission can_withdraw: owner | manager permission can_view: can_withdraw | beneficiary ``` The `can_view` permission is granted if: - The user has `can_withdraw` permission (which means they're owner or manager), OR - The user is a beneficiary #### Traversing Relations You can traverse relations using dot notation: ```yaml type account relation managed_by: branch type branch relation manager: user type account permission can_withdraw: managed_by.manager ``` This means `can_withdraw` is granted to users who are managers of the branch that manages the account. #### Complex Permission Examples ```yaml type folder relation parent: folder relation owner: user relation editor: user permission can_edit: editor | parent.owner | parent.can_edit ``` This permission is granted if: - The user is a direct editor of the folder, OR - The user is the owner of the parent folder, OR - The user has `can_edit` permission on the parent folder (which recursively checks up the hierarchy) ### DSL Syntax Summary | Syntax | Description | Example | |--------|-------------|---------| | `type ` | Define a type | `type user` | | `relation : ` | Define a relation | `relation owner: user` | | `\|` | Type union (OR), before `with` | `user \| group` | | `#` | Relation reference | `Group#member` | | `.` | Traverse relation | `parent.owner` | | `permission : ` | Define a permission | `permission can_edit: owner` | | `&` | Intersect (AND) of relations/permissions inside a permission's expression, before `with` | `permission can_edit: editor & owner` | | `-` | Difference (exclude) of relations/permissions inside a permission's expression, before `with`; this operator cannot appear after `with` | `permission can_view: viewer - suspended_user` | | `condition () { }` | Define a CEL condition | `condition IsAdmin(role string) { role == "admin" }` | | `constraint ()` | Define a built-in constraint | `constraint GeoCountry("US")` | | `with ` | Attach a condition/constraint to a relation or permission | `relation viewer: user with IsAdmin` | | `&` | Condition AND, only after `with` | `with IsAdmin & CanEdit` | | `\|` | Condition OR, only after `with` | `with IsAdmin \| CanEdit` | | `!` | Condition NOT, only after `with`; unary negation of a single condition, unlike the binary `-` operator above | `with !IsSuspended` | ### Conditions (ABAC with Common Expression Language) In addition to relations and permissions, the DSL supports attribute-based conditions written in [CEL (Common Expression Language)](https://cel.dev/). The `condition` keyword is used to declare a condition that takes typed parameters and evaluates a CEL expression against a caller-supplied context that may include additional details such as a user's IP or the time a request was made. ReBAC on its own answers the class of problem such as "can this user do X" by walking a series of relationships between entities in the form of a graph. ABAC builds on this by adding the use of attributes. Attributes can be used to specify ideas like time, geography, clearance level, etc. Say you want to limit when an action can happen by working hours; this is where attributes, and thus ABAC, can be used on top of a typical ReBAC check. Put another way, attributes can function as a *lookup* where a *literal* or *constant* are previously-defined. Additionally, a condition could also be defined to receive and evaluate two (or more) arguments. A simple check can be written in CEL like `request.weekday in allowed_workday`. In this case, `request.weekday` is an attribute and `allowed_workday` could be a literal/constant that is previously defined (i.e. [Monday, Wednesday, Friday]). A typical example involving role lookups would look like the following: ```yaml model AuthZ 1.0 condition IsAdmin(role string) { role == "admin" } type user type doc relation viewer: user with IsAdmin ``` In the previous example, `viewer` is a ReBAC relation. The value of `role` is provided with the request's context. `with IsAdmin` is an ABAC gate with the condition that checks the passed-in role against `admin`. The condition `role == "admin"` is written in CEL. This shows how to build ABAC relations into a ReBAC definition through the use of a `with` clause. #### Built-in Constraints The DSL also ships built-in constraints for common attribute checks, so you don't have to write CEL by hand. Declare one with the `constraint` keyword and attach it using `with`, the same way as a custom condition: ```yaml constraint IpRange constraint GeoCountry("US", "GB") constraint TrustedGeoCountry: GeoCountry("US", "GB", "CA", "AU", "NZ") type user type resource relation regional_reader: user with GeoCountry relation trusted_reader: user with TrustedGeoCountry relation ip_gated_reader: user with IpRange ``` If a constraint is defined with arguments in a schema, then they cannot be changed at Check time. On the other hand, constraints defined without arguments passed into them can accept arguments from check-time context, like `IpRange` in the above example. The following constraints are all included by default: | Constraint | Purpose | Context parameters (when called with no args) | |---|---|---| | `IpRange` | IP falls within a CIDR range | `ip` (ipaddress), `ip_range` (string) | | `IpList` | IP is one of an allow-list of addresses | `ip` (ipaddress), `allowed_ips` (list) | | `GeoCountry` | Country code is in an allowed set | `country_code` (string), `allowed_countries` (list) | | `DateExpiryEpochSeconds` | Current time is before an expiry timestamp | `now_epoch_seconds` (int), `expiry_epoch_seconds` (int) | | `StringMatchRegex` | String matches a regular expression | `str` (string); pattern is always required at declaration, never omitted | | `NumAtLeast` / `NumAtMost` | Number is above/below a bound | `num` (double), `min`/`max` (int) | | `NumRange` | Number falls within a min/max range | `num` (double), `min` (int), `max` (int) | | `BoolCheck` | Boolean equals an expected value | `bool` (bool), `expected` (bool) | | `IntList` | Integer is one of an allowed set | `int` (int), `allowed_ints` (list) | | `LabelList` | String label is one of an allowed set | `label` (string), `allowed_labels` (list) | As noted above, the `StringMatchRegex` constraint is the exception when it comes to providing arguments: you must always supply its pattern argument at declaration time, e.g. `StringMatchRegex("^admin_.*")`; it has no argument-free form. ## Complete Schema Example For a complete, real-world schema example, see the [Google Drive example](/authorization/rebac/examples/google-docs) which demonstrates a document platform with hierarchical folder structures, file permissions, and permission inheritance. ## Managing Your Schema You can learn how to insert your schema into Descope and manage it on the [Managing a Schema](/authorization/rebac/implement-schema) page. # Managing a Schema (/authorization/rebac/implement-schema) Learn how to implement a ReBAC (Relationship-Based Access Control) schema for your application with Descope. # Managing a Schema Once you've [defined your schema](/authorization/rebac/define-schema), you can implement it in Descope using either the Console or programmatically via SDKs and APIs. ## Managing Schemas in the Console The easiest way to create and manage schemas is through the Descope Console. Navigate to the **Authorization** page, select [FGA](https://app.descope.com/authorization/fga), and open the **Schema** tab. From the Schema tab, you can: - **Create new schemas** - Start from scratch or use a template - **Edit existing schemas** - Modify your schema directly in the editor - **Delete schemas** - Remove schemas and all associated relations The Console provides a code editor where you can write your schema in DSL format, validate it, and save it directly to your project. ### Visualizing Your Schema Alongside the editor, the Schema tab includes a graphs panel with two views: - **Relations Graph** - Shows how relations connect your types - **Permission Graphs** - Shows how relations resolve into your permissions ![Schema Graphs](/assets/fga-schema-graphs.webp) The permission graph merges relation leaves that feed the same permissions in the exact same way into a single node, so the graph stays readable even when several relations grant identical access. The graphs reflect your last saved schema, not unsaved edits in the editor: - Unsaved changes show a placeholder in both graphs until you save - A schema with no permissions defined shows a placeholder on the Permission Graphs view instead of an empty graph - Narrower screens hide the graphs panel so the editor has room to work ### Viewing schema graphs Next to the schema editor, the **Relations Graph** and **Permission Graphs** tabs visualize your saved schema as diagrams. Unsaved edits in the editor clear these diagrams until you save again. Each rendered graph has a copy button in its top-right corner. Clicking it copies the graph's underlying Mermaid source code to your clipboard, letting you reuse the diagram elsewhere, such as in your own documentation or an external Mermaid editor. The button appears only alongside a rendered graph, so it's absent while edits are unsaved, and on the Permission Graphs tab if the schema defines no permissions. ![Copy button on an FGA schema graph](/assets/fga-schema-graph-copy-button.webp) ## Managing Schemas with SDKs and APIs To implement a schema programmatically, you can create a YAML or JSON file that defines the schema and make a `saveSchema` call via [API](/api/management/authz/save-schema) or [SDK](/authorization/rebac/implement-schema#install-sdk). Or, use the other schema management functions to create, update, and delete schemas, namespaces, and relation definitions. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) management_key = "xxxx" try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key) except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Save (create or update) a schema The `saveSchema` function allows for creation or updating of a schema. If the schema already exists, the `upgrade` parameter determines whether the existing schema will be overwritten entirely. This code shows how to save the schema defined in a given file (YAML/JSON). An example of this schema file can be found in the [Define Schema page](/authorization/rebac/define-schema). ```javascript const schema = ``; await descopeClient.management.fga.saveSchema(schema); ``` ```python # Create or update an FGA schema. # Args: # schema (str): the schema in the AuthZ 1.0 DSL # model AuthZ 1.0 # type user # type doc # relation owner: user # relation viewer: user # Raise: # AuthException: raised if saving fails descope_client.mgmt.fga.save_schema(schema) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // Save schema err := descopeClient.Management.FGA().SaveSchema(ctx, schema) ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); String dsl = "model AuthZ 1.0\n" + "type user\n" + "type document\n" + " relation owner: user\n" + " relation editor: user\n" + " relation viewer: user"; try { FGASchema schema = new FGASchema(dsl); fs.saveSchema(schema); } catch (DescopeException de) { // Handle the error } ``` ### Delete a schema The `deleteSchema` function deletes an existing schema, including all relations. ```javascript await descopeClient.management.fga.deleteSchema() ``` ```python # Note: delete_schema may not be available in the FGA interface # For schema management, use save_schema to update schemas ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() err := descopeClient.Management.FGA().DeleteSchema(ctx) ``` ```java // Note: deleteSchema may not be available in the FGA interface // For schema management, use saveSchema to update schemas ``` ### Load a schema The `loadSchema` function returns the current project's schema. ```javascript // Note: loadSchema may be available through the fga namespace const schema = await descopeClient.management.fga.loadSchema(); ``` ```python # Note: load_schema may not be available in the FGA interface # For schema management, use the Console or save_schema to manage schemas ``` ```go // Load the existing schema // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() schema, err := descopeClient.Management.FGA().LoadSchema(ctx) if err != nil { // handle error } ``` ```java FGAService fs = descopeClient.getManagementServices().getFgaService(); try { FGASchema schema = fs.loadSchema(); // Do something with schema.getDsl() } catch (DescopeException de) { // Handle the error } ``` ### Dry run a schema The `dryRunSchema` function validates a schema without saving it and returns what would be deleted from the current schema. This is useful for testing schema changes before applying them. ```javascript // Dry run a schema to see what would be deleted const dryRunResponse = await descopeClient.management.fga.dryRunSchema(schema); // dryRunResponse contains information about what would be deleted // This allows you to review changes before applying them ``` ```go // Dry run a schema to see what would be deleted // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() dryRunResponse, err := descopeClient.Management.FGA().DryRunSchema(ctx, schema) if err != nil { // handle error } // dryRunResponse contains information about what would be deleted // This allows you to review changes before applying them ``` ### Delete a namespace The `deleteNamespace` function deletes a specific namespace and all related relations. This operation cannot be undone. ```python # Delete a namespace # Args: # name (str): namespace name to delete # schema_name (str): optional, can be used to track the current schema version # Legacy Authz interface - not recommended for new code descope_client.mgmt.authz.delete_namespace("doc", schema_name="") ``` ```go // Delete a namespace // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // Legacy Authz interface - not recommended for new code err := descopeClient.Management.Authz().DeleteNamespace(ctx, "doc", "") ``` # Overview (/authorization/rebac) Relationship-Based Access Control (ReBAC) allows you to manage access permissions in your application based on relationships between entities # Relationship-Based Access Control Relationship-Based Access Control (ReBAC) is part of Descope's **Fine-Grained Authorization (FGA)** system, which uses the authz service (`/v1/mgmt/authz/*` endpoints) for authorization operations. For performance optimization, you can use the [FGA Cache](/authorization/fga-cache) to accelerate ReBAC checks. ## Understanding ReBAC Descope supports full Relationship-Based Access Control (ReBAC) to manage access permissions in your application based on relationships between entities. You create a [schema](/authorization/rebac/define-schema) to define your authorization model, [create relations](/authorization/rebac/create-relations) between entities, and [check relations](/authorization/rebac/check-relations) to determine access. ### ReBAC with SSO Providers ReBAC can be integrated with [SSO](/auth-methods/sso) providers to automatically create relations based on group membership from your Identity Provider (IdP). When users authenticate via SSO, groups from the IdP can be mapped to ReBAC relations, automatically granting access based on their group membership. For example, if a user belongs to the "Engineering" group in your IdP, you can configure ReBAC to automatically create a `member` relation to an `engineering-team` resource, granting them access to engineering-specific resources. For instructions on how to configure ReBAC / FGA group mapping with SSO providers, see [SSO user and group mapping → Groups → FGA](/sso/sso-mapping#groups-to-fga-relations). ## Why Use Relational Authorization? ReBAC introduces relationship-based access control to handle scenarios where access depends on the specific relationships between users and resources, not just their roles. You can use ReBAC as a **replacement** for RBAC when you need more granular control, or as a **complement** to RBAC when you need both role-based and relationship-based authorization. ### When to Use ReBAC Introduce relational authorization when you need: - **Resource-Specific Access**: Access control that varies per resource (e.g., a user owns document A but not document B) - **Dynamic Relationships**: Permissions that change based on relationships that evolve over time - **Hierarchical Structures**: Access control for nested resources (e.g., folders containing documents) - **Collaborative Access**: Multi-user scenarios where access depends on relationships between users and resources - **Contextual Permissions**: Permissions that depend on the relationship context (e.g., a user can edit a document they own, but only view documents shared with them) ### ReBAC as a Replacement for RBAC You can use ReBAC to completely replace RBAC when your authorization model is primarily relationship-based. This is ideal when: - Most access decisions depend on relationships rather than roles - You need fine-grained, per-resource access control - Your authorization model is too complex for simple role assignments ### ReBAC as a Complement to RBAC You can use ReBAC alongside RBAC to handle different authorization scenarios: - **RBAC for broad permissions**: Use roles for high-level permissions (e.g., "Admin", "Editor", "Viewer") - **ReBAC for resource-specific access**: Use relations for fine-grained, per-resource permissions (e.g., "user owns document X", "user is a member of organization Y") **Example**: In a document management system: - **RBAC**: Defines that users with the "Editor" role can edit documents - **ReBAC**: Defines that a specific user owns document "roadmap-2024" and can delete it, while another user is a "viewer" of that same document Both authorization checks can be performed together to make comprehensive access decisions. ### Practical Scenario: E-commerce Platform Consider an e-commerce platform where you might use both RBAC and ReBAC: **With RBAC alone**, access is granted based on roles like Seller, Buyer, or Admin. All sellers have identical access privileges regardless of their specific relationship with buyers or products. This creates limitations: - **One-Size-Fits-All**: All sellers have identical access privileges regardless of their specific relationship with the buyer or the product - **Static Permissions**: Cannot easily accommodate temporary or conditional permissions **With ReBAC (as complement or replacement)**, you can add relationship-based access: - **Seller-Customer Relationships**: Sellers can give specific customers special access to pre-orders or exclusive products based on their purchase history - **Collaborative Features**: For custom-made items, the relationship between a buyer and seller allows direct communication channels - **Dynamic Permissions**: Access permissions evolve in real-time (e.g., temporary access to a seller's design tool that expires after order completion) **Using Both Together**: You might use RBAC to define that users with the "Seller" role can manage listings, while using ReBAC to define that a specific seller has a "preferred_customer" relationship with certain buyers, granting them exclusive access to certain products. ## Key Concepts Before implementing ReBAC in Descope, it's important to understand the key terms and concepts. For detailed explanations and examples, see the [Defining a Schema](/authorization/rebac/define-schema) guide. ### Core Components - **Schema**: A combination of one or more types that defines your permission model. Learn more about [schema structure and DSL syntax](/authorization/rebac/define-schema#understanding-the-dsl-domain-specific-language). - **Type**: Defines a class of objects with similar characteristics (e.g., `user`, `document`, `folder`). See [Types in the DSL guide](/authorization/rebac/define-schema#types). - **Relation Definitions**: Define all possible relations between entities (e.g., `owner`, `editor`, `viewer`). See [Relations in the DSL guide](/authorization/rebac/define-schema#relations). - **Resource**: An entity identifier in your system (e.g., `note-1`, `descope.com`). - **Target**: A unique identifier for a user or entity that can have relations to resources. - **Relation**: A tuple connecting a Target to a Resource with a specific relation definition. Learn how to [create relations](/authorization/rebac/create-relations). ### Direct and Implied Relations - **Direct Relations**: A direct relationship exists when a Relation tuple is explicitly created (e.g., user `u1` is the `owner` of document `doc-1`). - **Implied Relations**: An implied relationship is computed based on the schema. For example, if a user is a `member` of a `group` that has `owner` relation to a document, the user may have implied access through permission definitions. For complete schema examples and DSL syntax, see the [Defining a Schema](/authorization/rebac/define-schema) documentation. ## Implementing ReBAC in Descope The general process of setting up ReBAC in Descope is as follows: 1. **[Define a schema](/authorization/rebac/define-schema)**: Establish the types of entities (e.g., user, document) and the possible relationships (e.g., owner, editor) that exist within your application using the DSL (Domain-Specific Language). 2. **[Create relations](/authorization/rebac/create-relations)**: Build out the actual linkages between specific entities based on your defined schema. These relations represent the real-world relationships (e.g., "user-123 is owner of doc-1") that serve as the foundation for making access decisions. 3. **[Check relations](/authorization/rebac/check-relations)**: Integrate authorization checks into your application to consult the defined relationships and confirm if a user's action is authorized. ## Next Steps Now that you have a high-level overview of ReBAC, let's move on to [defining a schema](/authorization/rebac/define-schema). # Role-Based Access Control (RBAC) (/authorization/role-based-access-control) Learn how to assign roles and permissions to the application's end users and configure user roles with Descope. # Role-Based Access Control Descope allows you to assign roles and permissions to the application's end user. Users with Descope admin privileges can define roles and permissions in the [Descope console](https://app.descope.com/authorization/rbac) or using our [Management SDKs](/authorization/role-based-access-control/with-sdks). In Descope, roles and permissions are stored as **Strings** and can appear at both the project and tenant levels in the user's JWT. Your application is responsible for interpreting and enforcing these values. ## Preconfigured Roles and Permissions Descope projects come preconfigured with a few roles and permissions required for core admin functionalities. ### Preconfigured Roles #### Tenant Admin role You can also edit the **Tenant Admin** role to change its name, add or remove permissions, set it as a default role, and more. Click the menu next to the role name to access the edit options. By default, the **Tenant Admin** role comes bundled with the SSO Admin, User Admin, and Impersonate permissions described below, giving users assigned to it for a specific tenant the capabilities required to administer that tenant. #### SCIM role After a [SCIM Access Key](/management/tenant-management/scim#creating-scim-access-keys) is created for a Tenant, a **SCIM** role will also appear in the Roles table. ### Built-in Permissions Descope also provides a set of built-in permissions that unlock specific administrative capabilities. These permissions aren't exclusive to any single role, you can assign them to any [project-level or tenant-level role](#tenants-and-roles) you create, including custom roles, not just the preconfigured **Tenant Admin** role above. #### SSO Admin permission A user must have this permission to allow them to read and modify SSO configurations. #### Super User permission A user must have this permission to allow them full system access, including viewing sensitive configurations, managing all user roles, and assigning permissions beyond their own scope. #### User Admin permission A user must have this permission to allow them to read and modify user data. #### Impersonate permission A user must have this permission to allow them to act on behalf of another user. ## Creating Roles and Permissions On the [**Authorization > RBAC**](https://app.descope.com/authorization/rbac) page of the Descope console, you can create and manage your project-level roles and permissions. Clicking the `+ Permission` Button allows you to create a new permission. You can also click the three dots to the right of the permission to delete the permission or change its description. Clicking the `+ Role` Button allows you to create a new role, with associated permissions. You can also click the three dots to the right of the role to delete the role, change its description, or change its associated permissions. ![Descope permissions page shown as an example](/assets/rbac-permissions.webp) ### Setting Default and Hidden Roles When creating or modifying a project or tenant-level role, you can designate it as either a **default role** or a **hidden role**. When a role is set as a **default role**, when new users are created, they will automatically be assigned the role. When a role is set as a **hidden role**, Tenant Admins will not be able to view the role in [Admin Widgets](/widgets/admins) or the [SSO Setup Suite attribute mapping](/auth-methods/sso/sso-setup-suite#attribute-mapping-user-and-group). ![Descope creating a default role shown as an example](/assets/rbac-default-role.webp) ## Configuring User's Roles Each user that is created in your application can be assigned roles and permissions. The user can have more than one role and will include the role's associated permissions. Roles and permissions can be assigned manually in the console in the [users table](https://app.descope.com/users), assigned programatically using the [management SDK](/management/user-management/sdks#add-a-role-to-a-user), or can be [mapped from a SAML SSO provider](/auth-methods/sso/saml#group-mapping). ## Tenants and Roles Roles can be created and assigned on a Tenant and/or Project level. This flexibility allows you to implement different access control strategies based on your application's needs. ### Project-Level Roles Project-level roles are available to all users across all tenants in your project. These roles are useful for defining global permissions that should be consistent throughout your application. ### Tenant-Level Roles Tenant-level roles are specific to individual tenants and can be used to implement tenant-specific access control policies. To create a tenant-level role: 1. Select a tenant from the [Tenants Page](https://app.descope.com/tenants) of the Descope Console 2. Select the Authorization tab on the left 3. Define the roles and permissions ![tenant level roles](/assets/tenant-level-roles.webp) If you are using tenants for user management, the same user can be assigned: - The same role across different tenants - Different roles for different tenants - A combination of project-level and tenant-level roles This multi-tenant role management capability enables you to: - Implement tenant-specific access control policies - Maintain consistent permissions across tenants when needed - Provide different levels of access to the same user in different tenant contexts You can set a project-level role as a default role for a specific tenant. This means that when a new user is created for the tenant, they will automatically be assigned this project-level role. You can also set a project-level role as a default role for a specific tenant in the [SSO Setup Suite](/sso/sso-setup-suite-rbac#configuration-steps). ![project role as default role for a specific tenant](/assets/project-role-as-default-tenant.webp) ## JWT Example After successful end-user authentication, the roles and permissions are delivered to your application as part of the JWT token. Below is a sample JWT token that contains roles and permissions with a user logged into a tenant. ``` json { "amr": [ "email" ], "drn": "DS", "exp": 1692304651, "iat": 1692304051, "iss": "P2RFvFexVaxxNFK6rhP0ePtaGfTK", "sub": "U2RG6grrbT3REKYqk5yC4SjkMqzA", "tenants": { "T2U7vUH1NPy4JzWHruoOVIGyzYlu": { "permissions": [ "AppSecEngineer", "Marketing", "Support" ], "roles": [ "Engineering", "Product Manager" ] }, "T2U7vVBqyZv6HdGtGLdnkgCbNxrC": { "permissions": [ "AppSecEngineer", "Support" ], "roles": [ "Support" ] } } } ``` ## Validating Roles and Permissions For examples of how to validate roles and permissions using the backend SDK, refer to our [Session Validation](/sessions/validation) articles. # RBAC Management (/authorization/role-based-access-control/with-sdks) Learn how to assign roles and permissions to the application's end users and configure user roles with Descope SDKs. # RBAC Management with SDKs Descope's Role-Based Access Control (RBAC) system provides a flexible and powerful way to manage user permissions in your applications. You can use these SDK functions to implement RBAC in your application. ## Load All Permissions This function allows administrators to return all details for permissions configured within the Descope instance. The response includes an array of permissions and the details of each permission. ```javascript const resp = await descopeClient.management.permission.loadAll() if (!resp.ok) { console.log("Failed to load permissions.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded permissions.") console.log(resp.data) } ``` ```python try: resp = descope_client.mgmt.permission.load_all() print("Successfully loaded permissions.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to load permissions.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := descopeClient.Management.Permission().LoadAll(ctx) if (err != nil){ fmt.Println("Unable to load permissions.", err) } else { fmt.Println("Successfully loaded permissions.") for _, permission := range res { fmt.Println(permission) } } ``` ```java // You can optionally set a description for a permission. PermissionService ps = descopeClient.getManagementServices().getPermissionService(); // Load all permissions try { PermissionResponse resp = ps.loadAll(); for (Permission p : resp.getPermissions()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // (none) — Loads all permissions configured in the project. try { var response = await descopeClient.Mgmt.V1.Permission.All.GetAsync(); foreach (var permission in response!.Permissions!) { // Do something } } catch (DescopeException ex) { // Handle the error } ``` ## Create a Permission This function allows administrators to create a new permission. ```javascript // Args: // name (str): permission name. const name = "Test Permission" // description (str): Optional description to briefly explain what this permission allows. const description = "My description" const resp = await descopeClient.management.permission.create(name, description) if (!resp.ok) { console.log("Failed to create permission.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created permission.") } ``` ```python # Args: # name (str): permission name. name = "Test Permission" # description (str): Optional description to briefly explain what this permission allows. description = "My description" try: descope_client.mgmt.permission.create(name=name,description=description) print("Successfully created permission") except AuthException as error: print ("Unable to create permission.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): permission name. name := "Test Permission" // description (str): Optional description to briefly explain what this permission allows. description := "My description" err := descopeClient.Management.Permission().Create(ctx, name, description) if (err != nil){ fmt.Println("Unable to create permission.", err) } else { fmt.Println("Successfully created permission") } ``` ```java // You can optionally set a description for a permission. PermissionService ps = descopeClient.getManagementServices().getPermissionService(); String name = "My Permission"; String description = "Optional description to briefly explain what this permission allows."; try { ps.create(name, description); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // createRequest (CreatePermissionRequest): Name required; Description optional. var createRequest = new CreatePermissionRequest { Name = "my-permission-name", Description = "Optional description to briefly explain what this permission allows.", }; try { await descopeClient.Mgmt.V1.Permission.Create.PostAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ## Update a Permission This function allows administrators to update an existing permission with the given various fields. It is important to note that parameters are used as overrides to the existing permission; empty fields will override populated fields. ```javascript // Args: // name (str): permission name. const name = "Test Permission" // newName (str): permission updated name. const newName = "Updated Test Permission" // description (str): Optional description to briefly explain what this permission allows. const description = "My updated description" const resp = await descopeClient.management.permission.update(name, newName, description) if (!resp.ok) { console.log("Failed to update permission.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated permission.") } ``` ```python # Args: # name (str): permission name. name = "Test Permission" # new_name (str): permission updated name. new_name = "Updated Test Permission" # description (str): Optional description to briefly explain what this permission allows. description = "My updated description" try: descope_client.mgmt.permission.update(name=name, new_name=new_name, description=description) print("Successfully updated permission") except AuthException as error: print ("Unable to update permission.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): permission name. name := "Test Permission" // newName (str): permission updated name. newName := "Updated Test Permission" // description (str): Optional description to briefly explain what this permission allows. description := "My updated description" err := descopeClient.Management.Permission().Update(ctx, name, newName, description) if (err != nil){ fmt.Println("Unable to update permission.", err) } else { fmt.Println("Successfully updated permission") } ``` ```java // You can optionally set a description for a permission. PermissionService ps = descopeClient.getManagementServices().getPermissionService(); // Update will override all fields as is. Use carefully. String newName = "My Updated Permission"; description = "A revised description"; try { ps.update(name, newName, description); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // updateRequest (UpdatePermissionRequest): Name required; NewName and Description optional. var updateRequest = new UpdatePermissionRequest { Name = "my-permission-name", NewName = "new-permission-name", Description = "A revised description", }; try { await descopeClient.Mgmt.V1.Permission.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ## Delete a Permission This function allows administrators to delete an existing permission. It is important to note that this action is irreversible. ```javascript // Args: // name (str): The name of the permission to be deleted. const name = "Updated Test Permission" const resp = await descopeClient.management.permission.delete(name) if (!resp.ok) { console.log("Failed to delete permission.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted permission.") } ``` ```python # Args: # name (str): The name of the permission to be deleted. name = "Updated Test Permission" # Permission deletion cannot be undone. Use carefully. try: descope_client.mgmt.permission.delete(name=name) print("Successfully deleted permission") except AuthException as error: print ("Unable to delete permission.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): The name of the permission to be deleted. name := "Updated Test Permission" // Permission deletion cannot be undone. Use carefully. err := descopeClient.Management.Permission().Delete(ctx, name) if (err != nil){ fmt.Println("Unable to delete permission.", err) } else { fmt.Println("Successfully deleted permission") } ``` ```java // You can optionally set a description for a permission. PermissionService ps = descopeClient.getManagementServices().getPermissionService(); // Permission deletion cannot be undone. Use carefully. try { ps.delete(newName); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // name (string): The name of the permission to delete (irreversible). var name = "my-permission-name"; try { await descopeClient.Mgmt.V1.Permission.DeletePath.PostAsync(new DeletePermissionRequest { Name = name, }); } catch (DescopeException ex) { // Handle the error } ``` ## Load All Roles This function allows administrators to return all details for roles configured within the Descope instance. The response includes an array of roles and the details of each role. ```javascript const resp = await descopeClient.management.role.loadAll() if (!resp.ok) { console.log("Failed to load roles.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded roles.") console.log(resp.data) } ``` ```python try: resp = roles_resp = descope_client.mgmt.role.load_all() print("Successfully loaded roles.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to load roles.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := descopeClient.Management.Role().LoadAll(ctx) if (err != nil){ fmt.Println("Unable to load roles.", err) } else { fmt.Println("Successfully loaded roles.") for _, role := range res { fmt.Println(role) } } ``` ```java // You can optionally set a description and associated permission for a roles. RolesService rs = descopeClient.getManagementServices().getRolesService(); // Load all roles try { RoleResponse resp = rs.loadAll(); for (Role r : resp.getRoles()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // (none) — Loads all roles configured in the project. try { var response = await descopeClient.Mgmt.V1.Role.All.GetAsync(); foreach (var role in response!.Roles!) { // Do something } } catch (DescopeException ex) { // Handle the error } ``` ## Search for Roles This function allows administrators to return specific roles configured within the Descope instance using several parameters. The response includes an array of roles and the details of each role. ```javascript // Args: // tenant_ids (List[str]): List of tenant ids to filter by const tenant_ids = ["Tenant ID"] // role_names (List[str]): Only return matching roles to the given names const role_names = ["Role name"] // role_name_like (str): Return roles that contain the given string ignoring case const role_name_like = "string in role" // permission_names (List[str]): Only return roles that have the given permissions const permission_names = ["TestPermission"] const resp = await descopeClient.management.role.search(tenant_ids, role_names, role_name_like, permission_names) if (!resp.ok) { console.log("Failed to Search for roles.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully searched for roles.") console.log(resp.data) } ``` ```python # Args: # tenant_ids (List[str]): List of tenant ids to filter by tenant_ids = ["My Test Role"] # role_names (List[str]): Only return matching roles to the given names role_names = ["Role name"] # role_name_like (str): Return roles that contain the given string ignoring case role_name_like = "string in role" # permission_names (List[str]): Only return roles that have the given permissions permission_names = ["Permission name"] try: resp = descope_client.mgmt.role.search(tenant_ids=tenant_ids, role_names=role_names, role_name_like=role_name_like, permission_names=permission_names) print("Successfully searched for role.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to search role.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenant_ids (List[str]): List of tenant ids to filter by tenant_ids := []string{"Tenant ID"} // role_names (List[str]): Only return matching roles to the given names role_names := []string{"Role name"} // role_name_like (str): Return roles that contain the given string ignoring case role_name_like := "string in role" // permission_names (List[str]): Only return roles that have the given permissions permission_names := []string{"Permission Name"} res, err := descopeClient.Management.Role().Search(context.Background(), &descope.RoleSearchOptions{ tenant_ids, role_names, role_name_like, permission_names }) if err == nil { for _, role := range res { fmt.Println(role) } } ``` ```java // You can optionally set a description and associated permission for a roles. RolesService rs = descopeClient.getManagementServices().getRolesService(); List tenant_ids = Arrays.asList("Tenant ID"); List role_names = Arrays.asList("Role Name"); String role_name_like = "string in role"; List permission_names = Arrays.asList("Permission Name"); try { RoleResponse resp = rs.search(tenant_ids, role_names, role_name_like, permission_names); for (Role r : resp.getRoles()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // searchRequest (SearchRolesRequest): Optional filters for tenant IDs, role names, and permissions. var searchRequest = new SearchRolesRequest { TenantIds = new List { "tenant1", "tenant2" }, RoleNames = new List { "name1" }, RoleNameLike = "string in role", PermissionNames = new List { "TestPermission" }, }; try { var response = await descopeClient.Mgmt.V1.Role.Search.PostAsync(searchRequest); foreach (var role in response!.Roles!) { // Do something } } catch (DescopeException ex) { // Handle the error } ``` ## Create a Role This function allows administrators to create a new role. ```javascript // Args: // name (str): role name. const name = "My Test Role" // description (str): Optional description to briefly explain what this role allows. const description = "My Role Description" // permissionNames (List[str]): Optional list of names of permissions this role grants. const permissionNames = ["TestPermission"] // tenantId (str): Optional Tenant ID to assign new role to specific tenant const tenantId = "Tenant ID" const resp = await descopeClient.management.role.create(name, description, permissionNames, tenantId) if (!resp.ok) { console.log("Failed to create role.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created role.") } ``` ```python # Args: # name (str): role name. name = "My Test Role" # description (str): Optional description to briefly explain what this role allows. description = "My Role Description" # permission_names (List[str]): Optional list of names of permissions this role grants. permission_names = ["TestPermission"] # tenant_id (str): Optional Tenant ID to assign new role to specific tenant tenant_id = "Tenant ID" # private (bool): Optional marks this role as private role. private = False try: descope_client.mgmt.role.create(name=name, description=description, permission_names=permission_names, tenant_id=tenant_id, private=private) print("Successfully created role.") except AuthException as error: print ("Unable to create role.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): role name. name := "My Test Role" // description (str): Optional description to briefly explain what this role allows. description := "My Role Description" // permissionNames (List[str]): Optional list of names of permissions this role grants. permissionNames := []string{"TestPermission"} // tenantId (str): Optional Tenant ID to assign new role to specific tenant tenantID := "Tenant ID" err := descopeClient.Management.Role().Create(ctx, name, description, permissionNames, tenantID) if (err != nil){ fmt.Println("Unable to create role.", err) } else { fmt.Println("Successfully created role.") } ``` ```java // You can optionally set a description and associated permission for a roles. RolesService rs = descopeClient.getManagementServices().getRolesService(); String name = "My Role"; String description = "Optional description to briefly explain what this role allows."; List permissionNames = Arrays.asList("My Updated Permission"); // Pending release of Optional Tenant ID try { rs.create(name, description, permissionNames); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // createRequest (CreateRoleRequest): Name required; Description, PermissionNames, and TenantId optional. var createRequest = new CreateRoleRequest { Name = "my-role-name", Description = "Optional description to briefly explain what this role allows.", PermissionNames = new List { "permission-name1", "permission-name2" }, TenantId = null, // e.g., "Tenant-ID1" }; try { await descopeClient.Mgmt.V1.Role.Create.PostAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ## Update a Role This function allows administrators to update an existing role with the given various fields. It is important to note that parameters are used as overrides to the existing role; empty fields will override populated fields. ```javascript // Args: // name (str): role name. const name = "My Test Role" // newName (str): role updated name. const newName = "My Updated Test Role" // description (str): Optional description to briefly explain what this role allows. const description = "My Updated Role Description" // permissionNames (List[str]): Optional list of names of permissions this role grants. const permissionNames = ["TestPermission", "TestPermission2"] // tenantId (str): Optional Tenant ID to assign new role to specific tenant const tenantId = "Tenant ID" const resp = await descopeClient.management.role.update(name, newName, description, permissionNames, tenantId) if (!resp.ok) { console.log("Failed to update role.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated role.") } ``` ```python # Args: # name (str): role name. name = "My Test Role" # new_name (str): role updated name. new_name = "My Updated Test Role" # description (str): Optional description to briefly explain what this role allows. description = "My Updated Role Description" # permission_names (List[str]): Optional list of names of permissions this role grants. permission_names = ["TestPermission", "TestPermission2"] # tenant_id (str): Optional Tenant ID to assign new role to specific tenant tenant_id = "Tenant ID" # private (bool): Optional marks this role as private role. private = True # Update will override all fields as is. Use carefully. try: descope_client.mgmt.role.update(name=name, new_name=new_name, description=description, permission_names=permission_names, tenant_id=tenant_id, private=private) print("Successfully updated role.") except AuthException as error: print ("Unable to update role.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): role name. name := "My Test Role" // newName (str): role updated name. newName := "My Updated Test Role" // description (str): Optional description to briefly explain what this role allows. description := "My Updated Role Description" // permissionNames (List[str]): Optional list of names of permissions this role grants. permissionNames := []string{"TestPermission", "TestPermission2"} // tenantId (str): Optional Tenant ID to assign new role to specific tenant tenantID := "Tenant ID" // Update will override all fields as is. Use carefully. err := descopeClient.Management.Role().Update(ctx, name, newName, description, permissionNames, tenantID) if (err != nil){ fmt.Println("Unable to update role.", err) } else { fmt.Println("Successfully updated role.") } ``` ```java // Update will override all fields as is. Use carefully. String newName = "My Updated Role"; description = "A revised description"; permissionNames.add("Another Permission"); // Pending release of Optional Tenant ID try { rs.update(name, newName, description, permissionNames); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // updateRequest (UpdateRoleRequest): Name required; NewName, Description, PermissionNames, and TenantId optional. var updateRequest = new UpdateRoleRequest { Name = "my-role-name", NewName = "new-role-name", Description = "A revised description", PermissionNames = new List { "permission-name1", "permission-name2" }, TenantId = null, // e.g., "Tenant-ID1" }; try { await descopeClient.Mgmt.V1.Role.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ## Delete a Role This function allows administrators to delete an existing role. It is important to note that this action is irreversible. ```javascript // Args: // name (str): The name of the role to be deleted. const name = "My Updated Test Role" // tenantId (str): Optional Tenant ID to assign new role to specific tenant const tenantId = "Tenant ID" const resp = await descopeClient.management.role.delete(name, tenantId) if (!resp.ok) { console.log("Failed to delete role.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted role.") } ``` ```python # Args: # name (str): The name of the role to be deleted. name = "My Updated Test Role" # tenant_id (str): Optional Tenant ID to assign new role to specific tenant tenant_id = "Tenant ID" try: descope_client.mgmt.role.delete(name=name, tenant_id=tenant_id) print("Successfully deleted role.") except AuthException as error: print ("Unable to delete role.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): The name of the role to be deleted. name := "My Updated Test Role" // tenantId (str): Optional Tenant ID to assign new role to specific tenant tenantID := "Tenant ID" err := descopeClient.Management.Role().Delete(ctx, name, tenantID) if (err != nil){ fmt.Println("Unable to delete role.", err) } else { fmt.Println("Successfully deleted role.") } ``` ```java // You can optionally set a description and associated permission for a roles. RolesService rs = descopeClient.getManagementServices().getRolesService(); // Pending release of Optional Tenant ID // Role deletion cannot be undone. Use carefully. try { rs.delete(newName); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // name (string): The name of the role to delete (irreversible). var name = "my-role-name"; // tenantId (string?): Optional tenant ID if the role is tenant-scoped. string? tenantId = null; // e.g., "Tenant-ID1" try { await descopeClient.Mgmt.V1.Role.DeletePath.PostAsync(new DeleteRoleRequest { Name = name, TenantId = tenantId, }); } catch (DescopeException ex) { // Handle the error } ``` # Checking for Disposable Email Type (/flows/conditions/disposable-email) Condition to check if the email domain is disposable email type # Checking if it's a Disposable Email If you want to check if the email domain is a disposable email type, we can use an Action and Condition in Descope Flows: 1. Click on the plus icon in the top-left cornor and select an Action called: "Check if Disposable Email used": 2. Then select a Condition to check: 3. Connect the two: ![Disposable Email Action](/assets/disposable-email-action.webp) ![Disposable Email Condition](/assets/disposable-email-condition.webp) In the condition above, we can check the ```isDisposableEmail``` key that the result is automaticaly saved to by the action. The condition can check two different boolean values: ```isFalse``` or ```isTrue```. For more information, please refer to [this](https://github.com/wesbos/burner-email-providers/blob/master/emails.txt) link to get a list of all the disposable email domains. # Flow A/B Testing (/flows/conditions/flow-ab-testing) This guide covers how to A/B test in Descope Flows. # Flow A/B Testing A/B testing (also known as split testing) is a powerful method for optimizing your authentication flows by comparing different versions to determine which performs better. In Descope, you can easily implement A/B tests to experiment with different authentication experiences and measure their impact on user conversion and satisfaction. ## Descope Flows When a user starts a flow, Descope automatically assigns them a random testing key between 0 and 100. This key remains consistent for that user throughout their session, ensuring they see the same flow version each time they authenticate. This key can be used in an A/B testing condition to send the user to a specific "branch" of the flow. ### Setting Up A/B Tests 1. **Create Test Groups**: Use the condition block to split users into different groups based on their testing key: ![A/B Testing Condition Setup](/assets/ab-testing-condition.webp) 2. **Configure Test Variations**: Create different paths in your flow for each test group: ![A/B Testing Flow Paths](/assets/ab-testing-condition-2.webp) 3. **Monitor Results**: Track the performance of each variation in the [Flow Analytics dashboard](/management/project-settings/project-dashboard#flow-analytics-dashboard): ![A/B Testing Analytics](/assets/ab-test-results.webp) ## Best Practices for A/B Testing - **Test One Variable at a Time**: Change only one element between versions to accurately measure its impact - **Ensure Statistical Significance**: Run tests until you have enough data to make confident decisions - **Consider User Context**: Account for factors like device type, location, and user history - **Set Clear Success Metrics**: Define what success looks like before starting the test ## Advanced Analytics Integration For more detailed analytics, you can use connectors to integrate with third-party analytics tools like [Segment](/connectors/connector-configuration-guides/analytics/segment), [Amplitude,](/connectors/connector-configuration-guides/analytics/amplitude) and [more](/connectors/connector-configuration-guides/analytics). ## Common Use Cases 1. **Authentication Methods**: Test different authentication methods in your flow 2. **UI/UX Variations**: Experiment with different layouts, colors, or messaging 3. **Progressive Profiling**: Experiment on the right time to collect additional user information For more detailed examples and case studies, check out our [A/B Testing Blog](https://www.descope.com/blog/post/user-journey-ab-testing). # How to Check Free Email Type (/flows/conditions/free-email) Condition to check if the email domain is free email type # Checking if it's a Free Email Free emails are emails created by users at no cost including sites like Gmail, Hotmail, or Protonmail. If you want to check if the email domain is a free email type, we can use an Action and Condition in Descope Flows: 1. Click on the plus icon in the top-left corner and select an Action called: "Check if Free Email used": 2. Then select a Condition to check: 3. Connect the two: ![Free Email Action](/assets/free-email-action-2.webp) ![Free Email Condition](/assets/free-email-condition-2.webp) In the condition above, we can check the ```isFreeEmailProvider``` key that the result is automatically saved to by the action. The condition can check two different boolean values: ```isFalse``` or ```isTrue```. For more information, please refer to [this](https://github.com/descope/go-free-email-providers/blob/main/free/list.go) link to get a list of all the free email domains. # Conditions (/flows/conditions) Learn how to customize your authentication flow with conditions # Conditions Conditions in Descope Flows allow you to create dynamic, branching authentication journeys based on user attributes, environmental factors, and/or authentication results. They act as decision points in your flow, determining which path a user should take next. For example, you might want to: - Require additional verification for high-risk users - Show different screens based on the user's country - Branch to different authentication methods based on the user's domain - Skip certain steps for returning users You can add conditions to your flow by dragging and dropping them in the flow builder. Each condition can evaluate multiple attributes and use different operators to create complex decision logic. ## Using Conditions in Flows If you want to use conditions in your authentication flow, you can follow these simple steps: ### Add the Condition Select the flow you want to edit in the [Flows Tab](https://app.descope.com/flows) of the Descope Console, and select the Blue `+` button in the top left corner: ![Descope user conditions guide adding a condition](/assets/descope-user-conditions-guide-adding-condition.webp) Click on Condition, and a condition box will appear. Place this anywhere on the Flows panel and a popup will appear will all of the possible customizable options: ![Descope user conditions conditional box](/assets/descope-user-conditions-conditional-box.webp) ### Design the Condition To create a conditional statement, select which key you would like to use, along with the respective operator. For example, in the case of "Is new user?", you will set key to `authInfo.firstSeen`, and operator to `Is True`. If that condition is not met, we can conclude that the user already exists: ![Descope user conditions conditional statements](/assets/descope-user-conditions-conditional-statements.webp) When you have multiple steps in your condition, you can change their evaluation order by dragging and dropping them in the condition editor. #### Dynamic Fields You can check keys for specific strings, integers, boolean values, location data, and more. The `Operator` and `Value` options will change depending on the key you wish to add a conditional statement for. The following operators are available when creating conditions in your flows. The available operators will depend on the data type of the key you're evaluating. Operators marked with "No" in the "Requires Value" column only evaluate the target value itself and don't need an additional comparison value. | Operator | Description | Applicable Types | Requires Value | |----------|-------------|------------------|----------------| | **Equals** | Determine if two values are equivalent | String, Number, IP Address | Yes | | **Doesn't Equal** | Determine if two values are not equivalent | String, Number, IP Address | Yes | | **Contains** | Determine if the target contains the predicate (supports both lists and strings) | String | Yes | | **Doesn't Contain** | Determine if the target does not contain the predicate, supports both lists and strings | String | Yes | | **Matches** | Determine if the target matches the regex predicate | String | Yes | | **Greater Than** | Determine if the target is greater than its predicate | Number | Yes | | **Greater Than or Equal** | Determine if the target is greater than or equal to its predicate | Number | Yes | | **Less Than** | Determine if the target is less than its predicate | Number | Yes | | **Less Than or Equal** | Determine if the target is less than or equal to its predicate | Number | Yes | | **Is Empty** | Determine if value is empty | String, IP Address | No | | **Is Not Empty** | Determine if value is not empty | String, IP Address | No | | **Is True** | Determine if value is true | Boolean | No | | **Is False** | Determine if value is false | Boolean | No | | **In** | Determine if value is in the array | String | Yes | | **Not In** | Determine if value is not in the array | String | Yes | | **Is Email** | Determine if value is a valid email address | String | No | | **Is Phone** | Determine if value is a valid phone number | String | No | | **Within x hours ago** | Determine if value is within x hours from now | Time | Yes | | **Within x days ago** | Determine if value is within x days from now | Time | Yes | | **Within x minutes ago** | Determine if value is within x minutes from now | Time | Yes | | **Over x hours ago** | Determine if value is over x hours from now | Time | Yes | | **Over x days ago** | Determine if value is over x days from now | Time | Yes | | **Over x minutes ago** | Determine if value is over x minutes from now | Time | Yes | | **IP Address In** | Determine if an IP address is within a list of ranges | String, IP Address | Yes | | **Is Approved Domain** | Validate if domain is in the approved domain list | String | No | | **In Range** | Determine if value is in range (requires min, max values) | Number | Yes | | **Not In Range** | Determine if value is not in range (requires min, max values) | Number | Yes | | **Divided By** | Determine if value is evenly divisible by predicate | Number | Yes | For a full list of dynamic fields available, check out our [dynamic values doc](/flows/dynamic-keys). In this example, to check if a user is located in the US, we check if the `geo.country` key equals "US". ![Descope user conditions dynamic fields](/assets/descope-user-conditions-dynamic-fields.webp) ### Use the Condition Finally, you can take your conditional statements and add them to your flow by dragging arrows to and from the previous and next step, as shown below. In this example, if the user is new, they will be prompted to set up passkeys after authentication. If they are an existing user, they will proceed to the end of the flow: ![Descope user conditions final result](/assets/descope-user-conditions-final-result.webp) ## Common Use cases ### First Seen A common use case for conditions is differentiating between new users and those already onboard. Refer to the [design the condition step](/flows/conditions#design-the-condition) for implementation details. ### User Roles You can utilize user specific keys, like user roles, within a flow to handle users with different roles accordingly. In this example, we add [MFA](/mfa-and-step-up/mfa) only for "Admin" users, since Admins typically have the ability to take higher-risk actions: ![Descope user conditions roles](/assets/descope-user-conditions-roles.webp) ![Descope user conditions flow roles triage](/assets/descope-user-conditions-flow-roles-triage.webp) ### Authentication Methods You can use conditions to check whether a certain authentication method has been used by the user. For example, you might want to prompt a user to use TOTP for MFA if they have it configured, and prompt them to use an OTP via SMS if not: ![Descope user conditions user auth methods condition](/assets/descope-user-conditions-user-auth-methods-condition.webp) ![Descope user conditions user auth methods flow](/assets/descope-user-conditions-user-auth-methods-flow.webp) ### Adaptive MFA Adaptive MFA dynamically adjusts authentication requirements during login attempts based on real-time risk factors. You can implement Adaptive MFA in your authentication flows using conditional logic and dynamic values. For details on setting this up, see our [Adaptive MFA Guide](/mfa-and-step-up/mfa/adaptive-mfa). ### IsEmail and IsPhone Handling After prompting the user to enter their login Id, you can use a condition to check if it is an email or phone number. You can then use the result of the condition to call the corresponding authentication action. In this example, if the login Id is a phone number, we will send an OTP via SMS, and if the login Id is an email address, we will send the OTP via email. ![IsEmail IsPhone Condition](/assets/isemail-isphone-condition.webp) ![IsEmail IsPhone Example Flow](/assets/isemail-isphone-example-flow.webp) ### Check for Disabled Users You can check for disabled users using the `unauthUser.status` key, and present a customized screen accordingly. ![IsDisabled Example Condition](/assets/isdisabled-example.webp) ![IsDisabled Example Flow](/assets/isdisabled-example-flow.webp) ### Check for Approved Domain For use cases where you need to validate if a given domain is in the approved domain list, Descope supports "is Approved Domain" operator in a flow condition. This operator will check against the approved domain list within the project settings. This is usually helpful in scenarios where a malicious user could potentially manipulate the current hostname using browser injection techniques which could inturn be used to send embedded links (or other types) with a fraudulent URL. This operator ensures that the current domain matches against the trusted domains for the project. ![Approved Domains Operator](/assets/check-approved-domains.webp) ### Access External Cookies Users can access non-Descope cookies on the same domain within a flow. In flow conditions, a cookie can be accessed using the `cookies.` key, where `` represents the cookie's name. ![Non Descope Cookie Access](/assets/cookie-access.webp) ### Failed Authentication Attempts Descope tracks how many times a user has failed a given authentication step, which you can use to warn users before they are locked out, or to handle the lockout with a message of your own: - [`failedPasswordAttempts`](/flows/conditions/password-attempts) returns the number of failed password attempts the user has made. - [`remainingOTPAttempts`](/flows/conditions/remaining-otp-attempts) returns the number of OTP code entry attempts the user has left. # IP Address Check (/flows/conditions/ipaddress) Learn how to use the ipAddress dynamic value in Descope Flows to streamline authentication processes and customize user experiences. # Leveraging ipAddress in Descope Flows In your flow you can use the `ipAddress` value as a tool to enhance your authentication flows. This value will return the IP Address of the current end user. Below is an example of using the ipAddress dynamic value in a flow. ## Allowing particular IP Addresses In scenarios where only users from a certain IP Address are allowed to authenticate, you can use regular expression (regex) to search for a pattern in a string to check for a particular IP. `/^192\.168\.\d{1,3}\.\d{1,3}$/` This expression is a regular expression (regex), which is used to define a search pattern for strings. Regular expressions are commonly used for string matching, searching, and replacement operations in programming and text processing. - `^`: This asserts the start of a string. The pattern must match from the beginning of the string. - `192\.168\.`: This matches the literal string "192.168.". The backslash `\` is used as an escape character to indicate that the following dot `.` is a literal character rather than a wildcard character that matches any single character. - `\d{1,3}`: This matches any digit (`\d`) between 1 and 3 times. This is used to match a part of an IP address where each octet (the number between dots) can range from 0 to 255, but this regex does not enforce the maximum value of 255. - `\.`: Again, this matches the literal dot character, used to separate octets in an IP address. - `\d{1,3}`: This is repeated to match another octet, with the same criteria as before. - `$`: This asserts the end of a string, meaning the pattern must match up to the end of the string. Putting it all together, this regex matches strings that represent IP addresses starting with "192.168." followed by two more octets, each can be any number from 0 to 999 (due to the `\d{1,3}` pattern, but logically should be 0 to 255 to be a valid IP address). For example, it would match `192.168.0.1` or `192.168.123.456`, but it would not match `192.168.256.1` (even though this is outside the valid range for an IP address) because the regex itself doesn't enforce the maximum value of 255 per octet. ![flow ipAddress widget config](/assets/ipaddress-widget-in-flow-config.webp) Now, the widget can be leveraged in a flow to allow only particular ipAddresses. ![ipAddress widget in flow](/assets/ipaddress-widget-in-flow.webp) ## Adding Permitted IPs within Tenant Descope supports the ability to set IP addresses via custom attributes per tenant. Following this, Descopers can have specific allow list for each of their tenant. ![permitted ips within tenant via custom attributes](/assets/permitted-ips-tenant.webp) This functionality can also be leveraged in flow conditions. Users can have different authentication experiences or be restricted from signing up, based on their IP address. You can provide multiple values or CIDR ranges as values for the context key to match and use from. ![permitted ips flow condition](/assets/permitted-ips-flow-condition.webp) As an example, a Descoper can provide a range of whitelisted IP addresses and route their users to different screens based on the condition below. ![permitted ips in flow](/assets/permitted-ips-flow.webp) # User.loggedIn Condition (/flows/conditions/isloggedin) Learn how to use the isLoggedIn conditional in Descope Flows to streamline authentication processes and customize user experiences. # Leveraging User.loggedIn in Descope Flows In your flow you can use the `user.loggedIn` value as a tool to enhance your authentication flows. This value will rely on refresh tokens, present or not, in the client to be able to check if a user is already logged in or not. This feature allows you to dynamically adjust user experience based on their authentication status. Below are some example use cases for this specific conditional statement. ## Use Cases for user.loggedIn ### 1. Bypassing the Login Flow In scenarios where users are already authenticated, you might want to bypass the login flow entirely and directly navigate them to a post-login page or dashboard. ![flow conditional action](/assets/user-loggedin-flow.webp) ![if logged in, end the flow](/assets/user-loggedin-flow-end.webp) This approach ensures a seamless experience for returning users. ### 2. Role Check Before Running SSO Config Flow For applications implementing Single Sign-On (SSO) with SAML, you may need to verify if the authenticated user has the necessary role before allowing them to configure SAML settings. This ensures that only authorized users can access sensitive SSO Configuration settings. ### 3. Securing Internal Pages and Updating User Details Protecting internal pages of a site and providing flows for users to update their details can also be managed using `user.loggedIn`. This logic ensures that only authenticated users can access internal pages or update their personal details, enhancing the security and integrity of user data. # failedPasswordAttempts Condition (/flows/conditions/password-attempts) Condition to check failedPasswordAttempts for custom handling in Flows # failedPasswordAttempts Condition In your flow you can use the `failedPasswordAttempts` value to return the current number of failed password attempts, enabling you to configure custom handling for failed attempts. While it is possible to enable temporary user lockout settings for your password authentication method, time delays may introduce new complexities to your flow. In this case, use the `failedPasswordAttempts` condition. Below are some example use cases for this specific conditional statement. ## Use Cases for failedPasswordAttempts ### 1. Error after several failed attempts If you want to avoid the challenges of password resets in your Flow, you can treat a certain threshold of failed password attempts as an error. ![treat as an error](/assets/password-attempts-error.webp) ### 2. Pairing with various password actions In your flow, you can combine your `failedPasswordAttempts` condition with various password-related actions, such as the `Send Password Reset` action. To add the action to your Descope Flow, click the blue `+` button in the top-left corner. Then, select `Actions`, and pick the `Send Password Reset` action. ![send password reset action](/assets/password-attempts-and-reset-action.webp) You can connect the results of your condition to this action in your flow for custom handling. # remainingOTPAttempts Condition (/flows/conditions/remaining-otp-attempts) Condition to check remainingOTPAttempts for custom OTP lockout handling in Flows # remainingOTPAttempts Condition In your flow, check the `remainingOTPAttempts` dynamic value to see how many OTP attempts a user has left. Descope returns the same action error whether a user mistypes the code once or exhausts every attempt, `remainingOTPAttempts` is what lets you tell the two apart, so you can warn users as they approach the limit. Descope sets `remainingOTPAttempts` to your configured limit when it sends the OTP, decrements it after each incorrect attempt, and clears it to an empty value on lockout. It's only populated for OTP sent by email, SMS, voice, WhatsApp, or instant message — for any other authentication method, the value is empty. ## Detect a Locked Out User Set the OTP verification action's error handling to [`Continue`](/handling-flow-errors/customizing-flow-errors#continue) and connect its error output into a condition, with the key set to `remainingOTPAttempts`, the operator set to `Greater Than`, and the value set to `0`. With the default `Automatic` handling the user is returned to the previous screen and the condition never runs. ![remaining otp attempts condition showing the available numeric operators](/assets/remaining-otp-attempts-lockout-condition.webp) The `If` branch means the user still has attempts left, so return them to the code entry screen. The `Else` branch means they are locked out, and you can route them to a dedicated screen, a custom error message, or an account recovery step. ## Warn Before Lockout To warn the user on their final attempt, change the `If` value from `0` to `1`, then add an `Else if` with the operator set to `Equals` and the value set to `1`. Connect it to a screen that makes the consequence explicit, such as prompting the user to request a new code rather than risk being locked out. This gives you three branches, evaluated in order: `Greater Than` `1` for users with attempts to spare, `Equals` `1` for the final attempt warning, and `Else` for the lockout. Leaving the `If` at `0` makes the `Else if` unreachable, since a value of `1` already satisfies `Greater Than` `0`. ## Show the Remaining Count in an Error Message Instead of (or in addition to) branching with a condition, you can insert `remainingOTPAttempts` directly into a customized error message on an action or a condition, so the live count shows up in the same message the user already sees. This uses the same mechanism described in [Dynamic Values in Flow Errors](/handling-flow-errors/customizing-flow-errors#dynamic-values-in-flow-errors): open the message field, then use its dynamic value picker to insert `remainingOTPAttempts` at the point in the text where the count should appear. ### On the Verify Code / OTP action 1. On the OTP verification action, leave (or set) the error's handling to [`Automatic`](/handling-flow-errors/customizing-flow-errors#automatic). 2. Open the **Custom Error Message** field and write your message, inserting `remainingOTPAttempts` as a dynamic value where the count should appear, for example: `Incorrect code. You have {{remainingOTPAttempts}} attempts left before your account is locked.` 3. At runtime, Descope substitutes the live count, so a user on their second wrong attempt (of a 3-attempt limit) sees: `Incorrect code. You have 1 attempts left before your account is locked.` ![Error handling with dynamic values](/assets/remaining-attempt-error-handling.webp) ### On a condition, alongside the branches from above Because a condition lets you branch on the exact value, you can pair a custom error message with each branch from [Warn Before Lockout](#warn-before-lockout), each branch only ever shows the message written for it: - `Greater Than` `1`: route back to the code entry screen with a generic retry message, optionally still including the count, for example `Incorrect code. You have `remainingOTPAttempts` attempts left.` - `Equals` `1`: route to a dedicated warning screen (or the same screen with a distinct message) with wording like `Incorrect code. This is your last attempt before your account is locked. Consider requesting a new code instead.` - `Else` (locked out): since `remainingOTPAttempts` is already empty here, use a fixed message with no dynamic value, for example `You've used all your attempts. Request a new code or contact support to continue.` # Restrictions (/flows/conditions/restrictions) Learn how to manage user access with restrictions in Descope # User Access Restrictions User Access Restrictions in Descope provide advanced control over who can access your application. These restrictions help you manage sign-ups and prevent specific identifiers, like email addresses, from accessing your application. ## Examples of Restrictions Descope offers several restriction options to secure and control your application environment effectively: 1. **Allow or Block (Custom)**: Permits or prevent only specified identifiers to sign up for your application. Ideal for internal tools or applications restricted to a particular user group. 3. **Block Email Subaddresses**: Blocks email addresses containing characters like `+`, `=`, or `#`, preventing users from using modified versions of allowed emails to sign up. 4. **Block Disposable Emails**: Check out the Disposable Email article [here](/flows/conditions/disposable-email). ## Setting Up Restrictions ### Allow or Block (Custom) A powerful mechanism of user restriction in Descope Flows is using Conditions to check whether a particular parameter matches certain requirements, often using regex. To customize whether certain users are blocked or allowed, a Descope Flow can be created that takes an input such as an email and then checks if it matches certain requirements. For instance, we can include a condition to check whether an email address ends with `@descope.com`by checking if the value in the input (`form.email`) matches this regex: `^[^@]+@descope\.com$`. ![Email is Descope condition](/assets/email-descope-condition.webp) This could be placed in the flow anywhere after the `email` or other identifier has been inputted. ![Email is Descope condition](/assets/email-descope-flow.webp) ### Block Email Subaddresses This feature blocks any sign-up attempts using email addresses with subaddressing, commonly used to create multiple accounts from a single email address. - To block email subaddresses: - Add a condition in the flow to check if the inputted email matches this regex: `^[^@]+[\+=#][^@]*@[^@]+$` - This checks for the presence of `+`, `=`, or `#` in the email address. - `^[^@]+`: Ensures the string starts with one or more characters that are not an @. - `[\+=#]`: Matches any of the characters +, =, or #. - `[^@]*`: Allows any number of characters that are not an @ following the +, =, or #. - `@`: Matches the @ symbol. - `[^@]+$`: Matches one or more characters that are not an @ until the end of the string, ensuring there's at least one character in the domain part. - This regex will match emails like `acme+tag@acme.com`, `user=name@acme.com`, and `person#info@acme.com`, which all contain a subaddress. It will not match simple emails like `acme@acme.com` which lack the special characters indicating a subaddress. ![Email subaddress condition](/assets/email-subaddress-condition.webp) - The condition could fit in a flow just after the email address of a user is inputted. ![Email subaddress flow](/assets/email-subaddress-flow.webp) # Enforcing SSO (/flows/conditions/sso-enforced) Learn how to use the ssoEnabled and tenant.enforceSSO conditions in Descope Flows to streamline authentication processes and customize user experiences. # Enforcing SSO When your application serves multiple tenants, with some enforcing SSO for users and others not, you can automatically direct users to the correct authentication method by including a condition leveraging the `ssoEnabled` and `tenant.enforceSSO` keys within your flow. In scenarios in which additional authentication methods other than SSO are being used, it is necessary to utilize both the `ssoEnabled` and `tenant.enforceSSO` keys to correctly route users within your flow. `tenant.enforceSSO` enforces SSO but doesn't guarantee an SSO domain is configured. If a tenant enforces SSO but has no SSO domain, Descope can't route the user to their SSO connection and they can't sign in. Add **SSO Domains** to [Mandatory User Attributes](/auth-methods/sso/settings#requiring-an-sso-domain) to require an SSO domain on every tenant SSO connection in your project. ## Understanding the Keys - **ssoEnabled**: This key returns if SSO is available for the user by verifying if there is a tenant with an SSO configuration matching the user's domain. - **tenant.enforceSSO**: This key returns if SSO is mandatory for the user's tenant. When enforced, users must authenticate via SSO, ensuring compliance with organizational security policies. ## Constructing the Condition To correctly enforce SSO policies in a flow, construct a condition like the following: 1. First, check if `tenant.enforceSSO` is **True**. If SSO is enforced, you can automatically route users to the `SSO` action, bypassing other authentication methods. 2. Else, if SSO is not enforced, check if `ssoEnabled` is **True**. If SSO is enabled for the user, route them to the `SSO` action. 3. Else, in the case where SSO is neither enforced nor enabled, route the user to a non-SSO authentication action. ![sso routing condition](/assets/sso-routing-condition.webp) ![sso routing flow](/assets/sso-routing-flow.webp) # Flow Inputs (/flows/dynamic-keys/flow-inputs) Learn how to configure and utilize Descope flow inputs within your application. # Flow Inputs This guide will cover how to utilize flow inputs within your application using Descope flows. Passing inputs into your flow allows you to change certain behaviors within your flow. ## Using Flow Inputs ### Types of Flow Inputs - `form`: Used to pass flow inputs, such as email addresses, names, etc., from the app's frontend to the flow. These form inputs can be used within flow screens, actions, and conditionals. - `client`: Arbitrary metadata passed from your app into the flow. You can pass any JSON-serializable value—such as app versions or referral codes—and reference it in screens, actions, or conditions using `{{client.}}`. - `tenant`: Used for associating a user to a tenant. This input does not assign the tenant to a user, but rather used as a hint and will populate the `dct` claim. ### Example: ```javascript // to get the browser name and version import { browserName, browserVersion } from "react-device-detect"; ``` ```javascript ``` ```html ``` ### Using Flow Inputs in Widgets The `form` and `client` inputs work the same way when your flows run inside [Descope Widgets](/widgets). Pass them as props (React/Next.js) or attributes (Web Component) on the widget and they will be forwarded into every flow the widget runs: ```jsx import { UserProfile } from '@descope/react-sdk'; ``` ```jsx import { UserProfile } from '@descope/nextjs-sdk'; ``` ```html ``` See [Passing Flow Inputs to Widgets](/widgets/flows#passing-flow-inputs-to-widgets) for details on supported widgets, SDK availability, and merge behavior. ### Flow Inputs in Native Mobile Flows The Descope mobile SDKs can pass **client inputs** into a flow running natively in your app. A native flow renders a hosted flow page inside a webview, so the SDK forwards the values you provide into that page when the flow initializes. **Note that these inputs must be valid, non-null JSON types, and cannot be updated dynamically once the flow has started.** Once the flow is running, those values resolve as `{{client.}}` within flow screens, actions, and conditions, exactly as they do on the web. The `form` and `tenant` input types are not available in the mobile SDKs. On mobile, pass your values as client inputs and reference them in the flow editor as `{{client.}}`. Set the inputs on the flow object or flow configuration before running the flow: ```swift let flow = DescopeFlow(url: "https://example.com/myflow") flow.clientInputs = [ "appVersion": "3.2.1", "referral": "in-app-banner", ] let flowViewController = DescopeFlowViewController() flowViewController.delegate = self flowViewController.start(flow: flow) ``` ```kotlin val descopeFlow = DescopeFlow("").apply { clientInputs = mapOf( "appVersion" to "3.2.1", "referral" to "in-app-banner", ) } descopeFlowView.run(descopeFlow) ``` ```dart DescopeFlowView( config: DescopeFlowConfig( url: 'https://api.descope.com/login/?flow=', clientInputs: { 'appVersion': '3.2.1', 'referral': 'in-app-banner', }, ), callbacks: DescopeFlowCallbacks( onSuccess: (AuthenticationResponse response) { final session = DescopeSession.fromAuthenticationResponse(response); Descope.sessionManager.manageSession(session); }, onError: (DescopeException error) { // handle flow errors }, ), ); ``` ```javascript import { FlowView, useSession } from '@descope/react-native-sdk' const { manageSession } = useSession() { await manageSession(jwtResponse) }} onError={(error) => { // handle flow errors here }} /> ``` With the example above, a text component on a flow screen that references `{{client.referral}}` renders `in-app-banner`, and a condition can branch on `{{client.appVersion}}` to show a different screen to users on an older build of your app. To learn more about how flows run inside a native mobile app, see [Native Flows](/mobile-sdk/native-vs-browser-flows). ### Display Form Data in screens Once the `form` is populated, the data passed to the flow under the `form` field will populate within the screens for the items you display on the screen. Below is an example of a configured screen and how it appears within the application with the `form` inputs. These items can also be utilized within actions and conditions within Descope flow. ![Descope flow inputs example](/assets/flow-inputs-display.webp) # Dynamic Values (/flows/dynamic-keys) Learn how to utilize Descope's dynamic keys, values, and placeholders to enhance your Descope flows and messaging templates. # Dynamic Values This guide will cover using dynamic values within Descope flows, conditions, messaging templates, etc. Dynamic values in Descope hold values of attributes regarding the user, device, etc., which users can utilize for various authentication flows. Dynamic values can be used within Descope conditions, actions, connectors, etc. ## Usage 1. Dynamic values can be specific items you may want to pass to the email or SMS for the various authentication methods as a part of custom templates. Refer to custom templates [here](/flows/actions/email-sms-templates-in-flows). 2. These also can be utilized under conditional flows where users can check dynamic values to create conditional statements. 3. You can also use these values to display to the user within the Descope flow screens. 4. Many action arguments accept dynamic values, including the redirect URL and URI fields on the authentication actions. See [Dynamic Redirect URLs](#dynamic-redirect-urls). Wrap every key in double curly braces: `{{user.email}}`. One value can combine static text with several keys, such as `{{device.location.origin}}/welcome/{{tenant.id}}`. ## Default Dynamic Values Dynamic Values in Descope belong to a few categories. ### User - `user`: Keys prefixed with `user` are used for authenticated users. | Dynamic Key | Description | | ------- | ------------------------ | |`user.userId`| A unique identifier for a user | |`user.loginIds`| All unique identifiers for a user. Usually an email and/or phone | |`user.name`| The user's name | |`user.givenName`| The user's given name | |`user.middleName`| The user's middle name | |`user.familyName`| The user's family name | |`user.email`| The user's email address | |`user.emailDomain`| The domain of the user's email address | |`user.phone`| The user's phone number | |`user.verifiedEmail`| Whether the user's email address has been verified | |`user.verifiedPhone`| Whether the user's phone number has been verified | |`user.userTenants`| An array of tenants associated with the user | |`user.picture`| The user's picture | |`user.status`| The user's status including `enabled` , `invited` or `disabled` | |`user.tenantNames`| All user tenant names as array | |`user.tenantIds`| All user tenant IDs as array | |`user.tenant.roles` | The tenant level user roles | |`user.fingerprint.knownDevice`| Indication of whether the unauthenticated user is using a device that has been spotted before in your application | |`user.test`| Will be set to true if this user is a test user| |`user.lastAuth.country`| Last country user logged in from | |`user.lastAuth.countries`| Latest countries user logged in from | |`user.lastAuth.city`| Last city user logged in from | |`user.lastAuth.cities`| Latest cities user logged in from | |`user.lastAuth.ip`| Last IP user logged in from | |`user.lastAuth.ips`| Latest IPs user logged in from | |`user.lastAuth.time`| Last time user logged in | |`user.password`| Indicates whether the user has ever logged in with a password | |`user.passwordExpired`| Indicates whether the user's password has expired | |`user.totp`| Indicates whether user has TOTP set | |`user.project.roles`| All project level roles associated with this user| |`user.project.permissions` | All project level permissions associated with this user | |`user.saml`| Indicates whether the user has ever logged in with SAML | |`user.webauthn` | Indicates whether the user has ever logged in with Passkeys (Webauthn) | |`user.loggedIn` | Indicates whether the user is already logged in | |`user.customAttributes.` | The custom attribute for the user | Example of utilizing `user` Dynamic keys in a flow condition: ![User key Example in Flow](/assets/dynamic-keys-usersaml.webp) ### UnauthUser Descope allows you to load data for both authenticated and unauthenticated users. When a user first supplies their login ID (email, phone number, custom login ID, etc.) but has not completed authentication, you can load their details within your flow using the `unauthUser` dynamic keys. - `unauthUser` : Keys prefixed with `unauthUser` is used for unauthenticated users. | Dynamic Key | Description | | ------- | ------------------------ | |`unauthUser.userId`| A unique identifier for a user | |`unauthUser.loginIds`| All unique identifiers for a user. Usually an email and/or phone | |`unauthUser.name`| The user's name | |`unauthUser.givenName`| The user's given name | |`unauthUser.middleName`| The user's middle name | |`unauthUser.familyName`| The user's family name | |`unauthUser.email`| The user's email address | |`unauthUser.emailDomain`| The domain of the user's email address | |`unauthUser.phone`| The user's phone number | |`unauthUser.verifiedEmail`| Whether the user's email address has been verified | |`unauthUser.verifiedPhone`| Whether the user's phone number has been verified | |`unauthUser.userTenants`| An array of tenants associated with the user | |`unauthUser.picture`| The user's picture | |`unauthUser.status`| The user's status including `active` , `invited` or `disabled` | |`unauthUser.tenantNames`| All user tenant names as array | |`unauthUser.tenantIds`| All user tenant IDs as array | |`unauthUser.tenant.roles` | The tenant level user roles | |`unauthUser.fingerprint.knownDevice`| Indication of whether the unauthenticated user is using a device that has been spotted before in your application | |`unauthUser.test`| Will be set to true if this user is a test user| |`unauthUser.lastAuth.country`| Last country user logged in from | |`unauthUser.lastAuth.countries`| Latest countries user logged in from | |`unauthUser.lastAuth.city`| Last city user logged in from | |`unauthUser.lastAuth.cities`| Latest cities user logged in from | |`unauthUser.lastAuth.ip`| Last IP user logged in from | |`unauthUser.lastAuth.ips`| Latest IPs user logged in from | |`unauthUser.lastAuth.time`| Last time user logged in | |`unauthUser.password`| Indicates whether the user has ever logged in with a password | |`unauthUser.totp`| Indicates whether user has TOTP set | |`unauthUser.project.roles`| All project level roles associated with this user| |`unauthUser.project.permissions` | All project level permissions associated with this user | |`unauthUser.saml`| Indicates whether the user has ever logged in with SAML | |`unauthUser.webauthn` | Indicates whether the user has ever logged in with Passkeys (Webauthn) | |`unauthUser.customAttributes.` | The custom attribute for the user | Example of utilizing `unauthUser` Dynamic keys in a flow condition: ![UnauthUser key Example in Flow](/assets/unauthUser-dynamic-key-ex.webp) Example of utilizing `unauthUser.lastAuth.time` in a flow condition: ![UnauthUser last login key Example in Flow](/assets/unauthUser-last-login-time-ex.webp) Using the dynamic key `unauthUser.lastAuth.time`, Descoper can control the behavior of the flow based on how long ago an unauthenticated user had logged in. The operator in the condition supports checking the time in terms of minutes, hours or days. This can help in use cases where user's last logged in time logic needs to be applied. A similar key is also available for check for authenticated users (`user.lastAuth.time`). #### unauthUser Variations By default, when using `unauthUser` context keys, the user is loaded by email; however, there may be scenarios where the user needs to be loaded by another variation. Descope supports loading unauthenticated users using `unauthUser.byEmail` (default), `unauthUser.byPhone`, and `unauthUser.byTenant`. ### AuthInfo - `authInfo`: Values prefixed with `authInfo` provide authentication related information to the user. | Dynamic Key | Description | | ------- | ------------------------ | |`authInfo.sessionJwt`| A JSON web token used for session authentication and authorization purposes | |`authInfo.refreshJwt`| A JSON web token used for obtaining a new sessionJwt after expiration | |`authInfo.cookieDomain`| The domain that the cookie is valid for, used for cross-domain authentication | |`authInfo.cookiePath`| The path that the cookie is valid for, used for limiting cookie access to specific paths | |`authInfo.cookieMaxAge`| The maximum duration for which the cookie is valid, in seconds | |`authInfo.cookieExpiration`| The date and time at which the cookie expires, used for setting an explicit expiration date | |`authInfo.firstSeen`| Whether the user is logging in for the first time | Example of utilizing `authInfo` dynamic values in a flow condition: ![AuthInfo Example in Flow](/assets/dynamic-key-firstSeen.webp) ### LastAuth - `lastAuth`: Fields prefixed with `lastAuth` contain metadata about the user's most recent successful authentication. This information can be used to apply tenant-specific logic or personalize user flows. You can learn more about persisting `lastAuth` information in your flow, using custom `AuthProvider` parameters, in our [Auth Helpers Doc](/client-sdk/auth-helpers). | Dynamic Key | Description | | ------- | ------------------------ | |`lastAuth.authMethod`| The method used for the user's most recent authentication, such as a password or biometric authentication | |`lastAuth.loginId`| The unique identified associated with the user's most recent login session | |`lastAuth.ssoTenantId`| The tenant ID resolved from the user's SSO/OAuth login itself, and not a value the user selects | |`lastAuth.tenantId`| The tenant ID the user explicitly selected during a tenant-selection step in the login flow. If there is no tenant selection component in the flow, this value will typically be empty but `lastauth.ssoTenantId` will have a value | |`lastAuth.ssoId`| The SSO ID, for tenants with [multiple SSO configurations](/sso/multi-sso), associated with the user's most recent authentication | `lastAuth` is populated when a user completes the authentication flow, including for [Federated Applications](/identity-federation/applications) where Descope is the IdP (OIDC and SAML). If the user already has a valid session and Force Authentication is disabled ([SAML](/identity-federation/applications/saml-apps#force-authentication), [OIDC](/identity-federation/applications/oidc-apps#force-authentication)), Descope redirects the user back without running the flow, and `lastAuth` is not set for that request. Enable Force Authentication if your flow logic depends on `lastAuth` being present on every login. Examples of utilizing `lastAuth` dynamic values in a flow condition: Using the `lastAuth.authMethod`: ![LastAuth Example in Flow](/assets/dynamic-values-last-auth-method.webp) Using a composite condition of `lastAuth.authMethod` and `lastAuth.tenantId` to enforce tenant-specific logic: ![LastAuth Example in Flow](/assets/dynamic-values-last-auth-composite-condition.webp) ### Device - `device`: Values prefixed with `device` give information with respect to the user's device. | Dynamic Key | Description | | ------- | ------------------------ | | `device.location.origin` | The origin of where the flow is used. Such as `https://app.domain.com` | | `device.location.scheme` | The scheme of the URL where the flow is used. Such as `http` or `https`. | | `device.location.uri` | The full URI where the flow is used. Such as `https://app.domain.com/path?q=v#fragment` | | `device.location.hostname` | The hostname where the flow is used. Such as `app.domain.com`. | | `device.location.port` | The port where the flow is used. Such as `3000` | | `device.location.path` | The path where the flow is used. Such as `/path` | | `device.location.rawQuery` | The raw query string where the flow is used. Such as `"q=v"` | | `device.location.fragment` | The fragment (the URI part after #) where the flow is used. Such as `fragment`, in case the full URI is `https://app.domain.com#fragment` | | `device.location.query` | The query object where the flow is used. Such as `{"q":"v"}` | | `device.webAuthnSupport` | Indicates whether the device supports Passkeys (Webauthn) | Example of utilizing `device` dynamic values in a flow condition: ![Device Key Example in Flow](/assets/dynamic-key-devicepath.webp) ### Form - `form`: Values prefixed with `form` are specific to inputs provided by the user while authenticating into their application. | Dynamic Key | Description | | ------- | ------------------------ | | `form.email` | The email form input | | `form.newPassword` | The new password form input. Useful for JIT migration flows | | `form.password` | The login password form input | | `form.emailDomain` | The domain of the email form input | | `form.phone` | The phone form input | | `form.photo` | The picture uploaded by the "Take Photo" component | | `form.invitees`| The invitees form input | | `form.externalId`| The custom login ID form input | | `form.fullName`| The display name form input | | `form.firstName`| The given name form input | | `form.lastName` | The family name form input| | `form.tenantDomain` | The tenant domain form input | | `form.middleName` | The middle name form input | | `form.selfProvisionDomains` | The self provision domains form input | | `form.impersonateId` | The impersonate ID selected from the form input | | `form.entityId` | The entity ID form input | | `form.tenantName` | The tenant name form input | | `form.idpCert` | The IdP certificate form input | | `form.idpMetadataURL` | The IdP metadata URL form input | | `form.idpURL` | The IDP URL form input | | `form.displayName` | The display name form input | | `form.givenName` | The given name form input | | `form.familyName` | The family name form input | | `form.trustThisDevice` | Mark this device as trusted | Example of utilizing `form` dynamic values in a flow condition: ![Form Email Domain Example in Flow](/assets/dynamic-key-emaildomain.webp) ### RiskInfo - `riskInfo`: Values prefixed with `riskInfo` are specifically used for risked based authentication. For more details on using `riskInfo` key, refer [here](/flows/use-cases/implementing-fingerprinting). Fraud / Risk based connectors will have their own context keys that will only show up if the connector is configured. | Dynamic Key | Description | | ------- | ------------------------ | | `riskInfo.botDetected` | Indicates whether a user is likely a bot based on their behavior or activity | | `riskInfo.riskScore` | A numerical value representing the risk score associated with a user's behavior or activity | | `riskInfo.trustedDevice`| Indication of whether this device has been marked as trusted before or not | | `riskInfo.impossibleTravel`| Indication of whether this user has connected from two different countries and the time between those connections can't be made through conventional air travel | Example of utilizing `riskInfo` dynamic values in a flow condition: ![Risk Info Example in Flow](/assets/dynamic-key-riskinfo.webp) For more use cases on using `riskInfo` key, refer [here](/flows/use-cases/implementing-fingerprinting) ### UserAgent - `userAgent`: Values prefixed with `userAgent` provide details about the system, operating system, host application etc. | Dynamic Key | Description | Example | | ------- | ------------------------ | ------- | |`userAgent.mobile`| Is request coming from a mobile device? | `false` | |`userAgent.desktop`| Is request coming from a PC? | `true` | |`userAgent.device`| Device type (present for iOS and Android devices) | `iOS` | |`userAgent.raw`| Raw User Agent header (tells server what browser, OS, and rendering engine the request came from) | `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36` | |`userAgent.name`| Short agent name | `Chrome` | |`userAgent.os`| Operating system | `macOS` | |`userAgent.osVersion`| Operating system version | `10.15.7` | |`userAgent.tablet`| Is request coming from a Tablet? | `false` | |`userAgent.url`| URL provided in case of a bot | Example of utilizing `userAgent` dynamic values in a flow condition: ![User Agent Example in Flow](/assets/dynamic-key-useragent.webp) ### jwtClaims - `jwtClaims`: Values prefixed with `jwtClaims` provide post-authentication details that are stored in the JWT response. Please note that these values will only be available after the user goes through an authentication flow. | Dynamic Key | Description | | ------- | ------------------------ | |`jwtClaims.amr`| Identifiers Used | |`jwtClaims.drn`| Type of Token | |`jwtClaims.exp`| Timestamp of Expiration Time | |`jwtClaims.iss`| Timestamp of Issued Time | |`jwtClaims.sub`| User Id | In addition to the values above, [custom claims](/flows/actions/custom-claims) on the JWT that are Strings will also be available. They can be accessed as `jwtClaims.custom-claim-key`. Example of utilizing `jwtClaims` dynamic values in a flow condition: ![jwtClaims Example in Flow](/assets/dynamic-key-jwtclaims.webp) ### Tenant - `tenant`: Values prefixed with `tenant` provide details about the tenant associated with the user's account, and if the user is associated with multiple tenants, the values apply to the selected tenant in the context of the current flow. | Dynamic Key | Description | | ------- | ------------------------ | |`tenant.enforceSSO`| Indicates whether SSO is enforced for the tenant | |`tenant.enforceSSOExclusions`| Specify login IDs that can bypass SSO authentication | |`tenant.disabled`| Indicates whether tenant is disabled | |`tenant.domain`| Tenant domain | |`tenant.domains`| Tenant domains as array | |`tenant.id`| Tenant unique ID | |`tenant.name`| Tenant name | |`tenant.outbound.token.valid`| Indicates whether the tenant has a valid outbound token | |`tenant.selfProvisionDomain`| Email domain that allows tenant self provisioning | |`tenant.selfProvisionDomains`| Array of email domains that allows tenant self provisioning | |`tenant.subtenant`| Indicates whether the tenant is a [sub-tenant](/management/tenant-management/sub-tenants) (has a parent tenant) | Example of utilizing `tenant` dynamic values in a flow condition: ![tenant Example in Flow](/assets/tenant-dynamic-key.webp) ### Project - `project`: Values prefixed with `project` provide information about the current Descope project. | Dynamic Key | Description | | ------- | ------------------------ | |`project.name`| The project name | |`project.production`| Indicates whether the project is marked as a production environment using the [production tag](/management/project-settings#project-creation)| Example of utilizing `project` dynamic values in a flow condition: ![project Example in Flow](/assets/project-dynamic-key.webp) ### Event Triggers - `triggeringEvent`: Values prefixed with `triggeringEvent` provide details about the event that triggered the management flow. | Dynamic Key | Description | | ------- | ------------------------ | |`triggeringEvent.type`| The type of event that triggered the flow (for example, a user, tenant, group, or SCIM event type) | |`triggeringEvent.userId`| The single user ID affected by the event (when applicable) | |`triggeringEvent.metadata.actorId`| Who performed the action that triggered the flow: the Descoper's user ID for actions taken in the console, or the management key ID for actions taken through the SDK or API. Use it to set the **Actor ID** on the [Generate Audit Event](/flows/actions/generate-audit-event#attributing-the-actor) action. Empty for events with no actor | |`triggeringEvent.metadata.occurred`| When the triggering event occurred, matching the **Occurred** column on the Audit page | |`triggeringEvent.metadata.externalRequestId`| The [External Request ID](/audit-trails-and-integrations/audit-events#using-external-request-id) passed on the call that caused the event (when applicable) | |`triggeringEvent.metadata.remoteAddress`| The IP address the triggering action came from | |`triggeringEvent.metadata.country`| The country the triggering action came from | |`triggeringEvent.metadata.userAgent`| The user agent of the triggering request | |`triggeringEvent.loginIds`| The list of login IDs associated with the affected user(s) | |`triggeringEvent.projectId`| The project ID associated with the event | |`triggeringEvent.tenants`| The list of tenant IDs associated with the event | |`triggeringEvent.scim`| The indication of whether the event originated from SCIM | |`triggeringEvent.tenantId`| The tenant ID associated with the event (when a single tenant is affected) | |`triggeringEvent.tenantName`| The current tenant name associated with the event | |`triggeringEvent.tenantPreviousName`| The previous tenant name before the change (when applicable) | |`triggeringEvent.tenantDomains`| Current list of tenant domains associated with the event | |`triggeringEvent.tenantPreviousDomains`| Previous list of tenant domains before the change (when applicable) | |`triggeringEvent.tenantNames`| List of tenant names associated with the event (for multi-tenant operations) | |`triggeringEvent.changeData`| Structured object describing what changed on the user (for example, updated email, roles, tenants, auth methods, or custom attributes) | |`triggeringEvent.changeData.addedRoles`| The added roles change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.addedTenants`| The added tenants change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.addedCustomAttributes`| The added custom attributes change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.displayName`| The display name change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.givenName`| The given name change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.middleName`| The middle name change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.familyName`| The family name change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.email`| The email change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.verifiedEmail`| The verified email change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.verifiedPhone`| The verified phone change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.phone`| The phone change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.picture`| The picture change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.loginId`| The login ID change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.oldLoginId`| The old login ID change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.originalLoginId`| The original login ID change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.status`| The status change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.logoutTime`| The logout time change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.logoutToken`| The logout token change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.logoutDeviceRemoved`| The logout device removed change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.typedLogoutToken`| The typed logout token change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.invalidateJwtFamily`| The invalidate JWT family change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.impersonateConsent`| The impersonate consent change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.userExpiration`| The user expiration change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.removedRoles`| The removed roles change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.removedTenants`| The removed tenants change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.ssoApps`| The SSO apps change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingTOTP`| The using TOTP change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingPasskeys`| The using Passkeys change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingPassword`| The using Password change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingSaml`| The using SAML change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingScim`| The using SCIM change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingPush`| The using Push change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingGoogle`| The using Google change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingGithub`| The using Github change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingApple`| The using Apple change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingMicrosoft`| The using Microsoft change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingFacebook`| The using Facebook change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingGitlab`| The using Gitlab change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingDiscord`| The using Discord change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingLinkedin`| The using Linkedin change data associated with the event that triggered the flow execution | |`triggeringEvent.changeData.usingSlack`| The using Slack change data associated with the event that triggered the flow execution | ### Cookies Within a flow, users can access any cookies sent automatically by the browser for the same domain using the `cookies.` dynamic value. These cookies are not limited to Descope-related cookies. You can then use the value of the cookies in scriptlets, conditions, connector requests, and screens. ![cookie access](/assets/cookie-access.webp) ### InteractionID - `interactionID`: The unique identifier assigned to the last interaction (button click or state change) that triggered the flow to advance to the current step. | Dynamic Key | Description | | ------- | ------------------------ | | `interactionID` | The ID of the most recent interaction that caused the flow to advance, such as a button click or a timer expiring | This value is useful when a single screen has multiple exit paths (e.g., a **Sign In** button and a **Sign Up** button) and you need to distinguish which one was triggered in subsequent screens or conditions. For more details, see [Interaction IDs](/flows/screens/buttons#interaction-ids). ### oauthRequest - `oauthRequest`: Keys prefixed with `oauthRequest` expose parameters from the incoming OAuth/OIDC `/authorize` request. These are available when a user is redirected to a Descope flow through an [Inbound Application](/identity-federation/inbound-apps). | Dynamic Key | Description | | ------- | ------------------------ | | `oauthRequest.redirectUrl` | The `redirect_uri` from the `/authorize` request — the URL the user is sent to after authentication completes. | | `oauthRequest.scopes` | The OAuth scopes requested in the `/authorize` request. | | `oauthRequest.resources` | The `resource` parameter from the `/authorize` request, when the client specifies a target resource (RFC 8707). | This value is useful when you want to customize flow behavior based on details from the OAuth/OIDC authorization request, such as the requested redirect URL, scopes, or resources. ### Inbound App and MCP Client Custom Attributes - `thirdPartyApp` / `mcpClient`: Keys prefixed with `thirdPartyApp.customAttributes` or `mcpClient.customAttributes` expose the custom attributes configured on the [Inbound App](/identity-federation/inbound-apps) or [MCP server client](/agentic-identity-hub/core-components/mcp-servers#clients) that triggered the current flow. Both prefixes read from the same underlying custom attributes. Use `thirdPartyApp` for a general OAuth/OIDC Inbound App consent flow, or `mcpClient` for an MCP server's user consent flow. Set custom attributes on the app or client via the Management API or SDKs, for example when you [create a third-party application](/api/management/third-party-apps/create-third-party-application) or [create an MCP server client](/api/management/mcp-server-client-management/create-mcp-server-client). There is no console UI for setting them yet. Use these keys to personalize a consent or login flow based on attributes specific to the calling app or agent, such as its environment, tier, or other metadata your organization tracks. ### Federated App - `ssoApp`: Keys prefixed with `ssoApp` expose details about the [Federated Application](/identity-federation/applications) the user is signing in to. | Dynamic Key | Description | | ------- | ------------------------ | | `ssoAppId` | The ID of the Federated Application associated with the flow | | `ssoApp.name` | The name of the Federated Application associated with the flow | | `ssoApp.customAttributes.` | A custom attribute configured on the Federated Application | Use these keys to tailor the flow to the application. Show the application name on the login screen, or branch a condition on which application started the flow. ### Miscellaneous - Other dynamic values that don't fall into the above categories and are available for use in flows, conditions, and templates. | Dynamic Key | Description | | ------- | ------------------------ | | `abTestingKey` | A random persistent number between 1-100 assigned to the client, that will help you split the flow for A/B testing | | `asn` | The Autonomous System Number (ASN) of the user's network for the current request | | `embeddedCode` | Embedded OTP code | | `externalToken` | Indicates whether flow has started with an external token (from invite of embedded link) to be verified | | `failedPasswordAttempts` | The number of failed password attempts made by the user | | `failedSQAttempts` | The number of failed security question verification attempts made by the user | | `flowExecutionId` | The unique ID of the specific flow execution | | `flowId` | The ID of the flow being executed. e.g. 'sign-up-or-in' | | `idpGroups` | IdP Groups (SSO JIT / SCIM) | | `idpInitiated` | Indicates whether the authentication request was initiated by an identity provider (IdP) | | `idpOIDCClaims` | IdP OIDC claims | | `idpSAMLAttributes` | IdP SAML attributes | | `ipAddress` | The IP address of the user who initiated the current request | | `isDisposableEmail` | Indicates whether the email domain is of type disposable or burner email, need to run action "Check if Disposable Email used" prior to this condition | | `isEmailScanner` | Possibility of an e-mail scanner attempting to click a magic link detected | | `isFreeEmailProvider` | Indicates whether the email domain is of type free email provider, like gmail.com, need to run action "Check if Free Email used" prior to this condition | | `ja4` | The TLS fingerprint (JA4) of the user who initiated the current request | | `mailProviderHost` | The hostname of the user's email provider, used for authentication purposes | | `outboundAppId` | Outbound application ID | | `provider` | The authentication provider used for authentication purposes | | `redirectURL` | The URL to which the user will be redirected after authentication | | `remainingOTPAttempts` | The number of remaining OTP code entry attempts | | `sourceIP` | The originating IP address of the request that triggered the flow | | `ssoEnabled` | Indicates whether single sign-on (SSO) is enabled for a user tenant, by user's email domain or tenant id | | `ssoEnabledIgnoreUser` | Indicates whether single sign-on (SSO) is enabled based on the logged in user's email domain or tenant id. Should be used after login actions such as sign up / in | | `ssoId` | SSO ID | | `tenant` | The tenant associated with a user account | ## Dynamic Redirect URLs The redirect field on authentication actions accepts dynamic values, so one flow can return users to whichever site or tenant they started from. Select the action in the [flow editor](https://app.descope.com/flows), then either choose a key from the field's dropdown or type the value in. The dropdown lists keys drawn from the flow's own screens, so you enter keys such as `device.location.hostname` yourself. ### Returning Users to the Site that Hosted the Flow When one flow serves several domains, such as a staging site and production, build the URL from the location of the page running the flow: ``` {{device.location.scheme}}://{{device.location.hostname}}/login ``` `{{device.location.origin}}` gives the same result in a single key, but Descope populates it only after the end user interacts with a screen. On an action that runs as the flow's first step, such as a flow that opens straight into an OAuth redirect, use `scheme` and `hostname` instead. In native mobile flows, `origin` holds the value your app passes in its native options, not a web address. ### Redirect URL Precedence An action's redirect field is one of several places a redirect URL can come from. Therefore, there is an order that Descope uses to determine which redirect URL to use, if defined in multiple places. Descope uses the first of these that is set: 1. The redirect URL your application passes when it starts the flow, through the SDK or the `redirect-url` attribute on the Descope component 2. The action's redirect field, where Descope resolves dynamic values 3. The redirect URL configured in the authentication method's project or tenant settings 4. The location of the page running the flow ### When a Dynamic Value Does Not Resolve A dynamic redirect URL that fails to produce a usable address behaves in one of two ways: - A key that holds a list or an object, used on its own as the entire field, leaves the field empty. Descope falls back to the redirect URL in the authentication method's settings. - A URL that resolves to a domain outside your [Approved Domains](/management/project-settings#approved-domains), or that is incomplete because the flow has not collected the referenced values yet, fails the action. Place the action after the screen that collects those values and handle the failure through the action's [error handling](/flows/actions#handling-action-errors-in-flows). ## Additional Context Keys In addition to the default dynamic values mentioned above, Descope supports values created by connectors or scriptlets used in flows. For more information on these, please refer to the following documents: - [Scriptlets](/flows/actions/scriptlets#context-key): Keys as a result of executing scriptlets in flows are in the format `scripts.scriptletResult.xyz` where `xyz` is the variable holding the result of the scriptlet action. - [Connectors](/connectors/connector-configuration-guides/network/generic-http): Keys being returned from connectors are prefixed with `connectors`. - [Document](/flows/screens/inputs/uploaddocument-component): Key used specifically for document being uploaded via flows. The list of available Dynamic Values is continually expanding, but the guide above provides a solid foundation for understanding how they are used in Descope. # Add SAML Attributes (/flows/actions/add-saml-attributes) Use the SSO / Add SAML Attributes flow action to include custom or SSO-provider attributes in a SAML assertion issued by a SAML federated application. # Add SAML Attributes Use the **SSO / Add SAML Attributes** action to add custom attributes to the SAML assertion Descope issues when acting as a SAML Identity Provider. Use it for attributes that [User Attribute Mapping](/identity-federation/applications/saml-apps#user-attribute-mapping) and [Group Mapping](/identity-federation/applications/saml-apps#group-mapping) don't cover — for example, attributes computed during the flow, or attributes received from a tenant's own SSO provider. ## Add the Action 1. Open the flow attached to your SAML federated application. 2. Click the **(+) icon**, search for **SSO / Add SAML Attributes**, and add it before the flow ends. 3. You can rename the step (defaults to **SAML Additional Attributes**). ## Configure the Action ### Attributes Add one row per attribute you want in the assertion: | Key | Type | Value | | --- | ---- | ----- | | `department` | String | `Engineering` | **Key** is the SAML attribute name. **Type** determines how Descope reads **Value**: | Type | Description | | ---- | ----------- | | **String / Boolean / Number / Time** | A static value of that type. | | **Dynamic** | A value read from flow context at runtime — for example a value resolved from a [connector](/connectors/connectors-in-flows) or a [Scriptlet](/flows/actions/scriptlets) earlier in the flow. | | **List** | Multiple values for a single multi-valued SAML attribute. | ![Configuring the SSO / Add SAML Attributes action](/assets/add-saml-attributes-action.webp) ### Context Key for SAML Attribute Map (Optional) Instead of (or in addition to) the rows above, point this at a flow context key holding a map of attribute names to values — for example a map returned by a connector or scriptlet. Descope adds every entry in that map to the assertion. This must resolve to an actual object, not a JSON string. In particular, a verified [Verify Token](/flows/actions/verify-token) token's claims live on the flow's `customClaims` context value as a JSON-encoded string. Parse it into an object with a Scriptlet and store the result under its own context key before pointing this field at it. ### Add attributes from incoming SAML assertion Turn this on if the user signed in through the tenant's own SSO provider (a SAML identity provider), and you want to pass those same attributes through to the SP you're issuing this assertion to. Run the flow and check flow context in the [Flow Runner](/handling-flow-errors/troubleshooting-flows#flow-runner) to confirm what's stored under your chosen context key before relying on it here. # Authentication Methods (/flows/actions/authentication-methods) Learn about different authentication methods available in Descope and how to use them in your flows. # Authentication Methods Descope supports a wide range of authentication methods, and each one can be added to a flow as an **Action** step. This page gives an overview of the authentication method actions available in the flow builder. For method-specific setup (like connectors, templates, or provider configuration), see each method's dedicated [Auth Methods](/auth-methods) guide. ## The Sign Up / Sign In Action Pattern Most authentication methods expose the same four actions in the flow builder, differing only in how they handle whether the user already exists: - **Sign Up / ``**: Signs the user up. Fails if the user already exists. - **Sign Up or In / ``**: Signs the user in if they exist, and automatically signs them up if they don't. This is the most commonly used action for login/signup flows. - **Sign In / ``**: Signs the user in. Fails if the user doesn't exist. - **Update User / ``**: Merges the method (for example a verified email or phone number) into an existing, already-authenticated user. ![Adding a Sign Up or In action to a flow](/assets/sign-up-or-in-otp.webp) To add any of these, click the blue **+** in the flow builder, select **Action**, then search for the method by name. See [Adding Actions to Flows](/flows/actions#adding-actions-to-flows) for the general steps. ## Available Methods - **[OTP](/auth-methods/otp)**: One-time codes sent via email or SMS. - **[Magic Link](/auth-methods/magic-link)**: A clickable link sent via email or SMS that verifies the user. - **[Enchanted Link](/auth-methods/enchanted-link)**: A cross-device link flow where the user approves the request from another device or tab. - **[Passkeys (WebAuthn)](/auth-methods/passkeys)**: Biometric or device-based authentication using the WebAuthn standard. - **[Social Login (OAuth)](/auth-methods/oauth)**: Sign in via third-party identity providers like Google, Facebook, Apple, and more. - **[Passwords](/auth-methods/passwords)**: Traditional password-based authentication, including reset flows. - **[Authenticator Apps (TOTP)](/auth-methods/auth-apps)**: Time-based one-time codes from apps like Google Authenticator. - **[SSO](/auth-methods/sso/with-flows)**: Redirects the user to their tenant's configured identity provider (SAML or OIDC). - **[nOTP](/auth-methods/notp)**: Number matching-based one-time authentication. - **[Device Authentication](/auth-methods/device-auth)**: Authenticates a device directly, commonly used for headless or IoT scenarios. - **[Embedded Link](/auth-methods/embedded-link)**: A short-lived, single-use link your backend generates and delivers outside of Descope's messaging (for example your own email templates). - **[Recovery Codes](/auth-methods/recovery-codes)**: Backup codes users can redeem if they lose access to their primary method. - **[Security Questions](/auth-methods/security-questions)**: Knowledge-based verification as a secondary or fallback method. - **[External Authentication](/flows/actions/external-authentication)**: Delegates authentication entirely to your own existing login system, then resumes the Descope flow. SSO only exposes a single **SSO** action (there's no separate Sign Up/Sign In/Update User variants), since the identity provider handles authentication. ## Combining Methods Descope doesn't distinguish between "first factor" and "MFA" authentication methods, and any auth method can serve as either. This means you can mix and match any combination of methods to build the exact multi-factor experience you want. See [Multi-Factor Authentication (MFA)](/mfa-and-step-up/mfa), for more details. Because each method is its own action, you can offer multiple authentication options on a single screen (for example, buttons for **Continue with Google**, **Continue with Passkey**, and a **Sign Up or In / OTP** action triggered by an email form) and route users to different next steps based on which action completes. ## Error Handling Like any flow action, authentication method actions can fail (such as an incorrect OTP code). You can configure how the flow responds under each action's error handling settings. See [Handling Action Errors in Flows](/flows/actions#handling-action-errors-in-flows) for the available options. # Custom Claims (/flows/actions/custom-claims) Use the Custom Claims flow action to add claims to a user's JWT during a Descope flow. # Custom Claims in Flows Use the **Custom Claims** flow action when claim values depend on flow context, such as conditional logic, connector responses, step-up authentication, or user inputs collected during the flow. For project-wide claim shaping, use [JWT Templates](/management/token/jwt-templates). For the full conceptual guide (including limits, nested JSON claims, and method selection), see [Custom Claims](/management/token). If a claim key does not exist in the JWT, this action adds it. If the key already exists (including from a JWT Template), the flow action value overrides it for that flow run. ## Add the Custom Claims Action 1. Open the flow where you want to set claims. 2. Click the add button in the lower-left corner. 3. Search for **Custom Claims** and add the action. ![Descope custom claims management add to flow](/assets/custom-claims-action.webp) ## Configure the Custom Claims Action When implementing custom claims, avoid exposing sensitive data. See [JWT Claims security best practices](/security-best-practices/custom-claims). In **Simple** mode, you can set string, boolean, numerical, and dynamic values. In **Advanced** mode, you can define claims as a JSON object for more complex structures. ![Descope custom claims management add to flow](/assets/custom-claims-action-advanced.webp) You can also enable **Set Custom Refresh Duration** in this action. When set, it overrides the [project-level refresh token timeout](/management/project-settings#refresh-token-timeout) for sessions issued by this flow. ![Set custom refresh duration in custom claims action](/assets/custom-refresh-duration.webp) ## Save and Attach the Action After configuration, click **Done** and place the action after user verification (typically near the end of the flow). ![Descope custom claims management add to flow](/assets/custom-claims-action-attach.webp) ## Test the Flow Run the flow and inspect the returned JWT to confirm your claims are present. ![Descope custom claims management add to flow](/assets/custom-claims-action-test.webp) ## Example: Include IdP Groups in the JWT When a user authenticates with SSO, you may want to include the groups received from the external identity provider in the issued JWT in order to map IdP groups to internal roles, permissions, organizations, or other authorization entities. To include IdP groups, add a **Custom Claims** action after the SSO authentication step and set a custom claim value from the `idpGroups` flow context key, as shown below: | Key | Type | Claim value | |-------------|---------|-------------| | `idp_groups`| Dynamic | `idpGroups` | Because `idpGroups` is populated from the IdP response during SSO authentication, updated group membership from the IdP may not appear in the JWT until the user authenticates through the relevant SSO flow again. Example JWT with this custom claim: ```json { "amr": ["oauth"], "aud": ["P2OkfVnJi5Ht7mpdfFjx17nV5epH"], "idp_groups": ["Engineering", "Admin"], "drn": "DS", "exp": 1740591584, "iat": 1740591404, "iss": "P2OkfVnJi5Ht7mpdfFjx17nV5epH", "rexp": "2025-03-26T17:36:44Z", "sub": "U2sH5htjJuR98vQZOOD0tI5D6PQF" } ``` ## Nested JSON Claims You can set nested claim objects in **Advanced** mode. For full examples and guidance, see [Nested JSON Claims](/management/token#nested-json-claims). # Using Messaging Templates (/flows/actions/email-sms-templates-in-flows) This article will show you how to customize email, voice, and sms templates for OTP, enchanted link, and magic link within Descope flows. # Using Messaging Templates This doc will show you how to use email, voice, and SMS templates for [OTP](/auth-methods/otp), [Enchanted Link](/auth-methods/enchanted-link), and [Magic Link](/auth-methods/magic-link) authentication. We will use OTP as an example, but the steps are the same for each of the authentication methods. ## Creating a Messaging Template Before utilizing a template for your authentication message, you must first create it following the steps in our [messaging templates guide](/management/messaging-templates) ## Using a Custom Template in Flows Within a flow, you can edit any authentication action that involves messaging to use a custom template instead of the default message we provide. 1. Open your flow in the [Flow Editor](https://app.descope.com/flows) 2. Select the messaging action 3. Choose your custom template from the dropdown ![Example of a customized template for email authentication methods in Descope](/assets/example-customized-template.webp) ## Using Template Options (Dynamic Keys) Template options allow you to pass dynamic data to your templates. To learn how to set and use these values, refer to our [Template Options Doc](/management/messaging-templates#dynamic-content-with-template-options). # End Action (/flows/actions/end-action) This article will show you how to use the End action in Descope flows. # End Action The End action terminates the flow and returns Descope tokens, successfully authenticating the user. ![End Action](/assets/end-action.webp) ## End without Session When enabled, the flow ends without issuing or returning a session — no JWT, user, cookies, login event, or trusted-device JWT. The user keeps any session they already had. Use this for flows that should complete without logging the user in (e.g. the user-profile widget, or a deliberate no-login outcome). ## Custom Cookie Names When you manage tokens in cookies, Descope uses `DSR` for the refresh token and `DS` for the session token by default. If you need different names — for example, because multiple Descope projects share the same [root domain](/how-to-deploy-to-production/custom-domain) — you can override them here on the End action. ### Refresh Cookie Name By default, Descope refresh tokens [managed with cookies](/security-best-practices/refresh-token-storage#handling-refresh-tokens-in-cookies) are returned in a cookie named `DSR`. The **Refresh Cookie Name** field lets you set a custom name (for example `refreshToken`) instead. ### Session Cookie Name By default, most Descopers don't manage session tokens as backend cookies and instead rely on the tokens returned in the response body. However, our client SDKs automatically set the session token as a `non-HttpOnly` cookie, and you can enable the same behavior by [turning on the `sessionTokenViaCookie` parameter in your AuthProvider](/client-sdk/descope-components#cookie-configuration-options). By default, Descope session tokens [managed with cookies](/security-best-practices/session-token-storage#managing-with-cookies) are returned in a cookie named `DS`. The **Session Cookie Name** field lets you set a custom name (for example `sessionToken`) instead. ## External Token Connector When a flow completes, Descope can return a provider token (`externalToken`) from a Firebase, Supabase, or custom connector. By default, flows use the connector you selected under External Token in [Session Management](https://app.descope.com/settings/project/session). If you want to use a different External Token connector than what's configured in Session Management for all flows within your project, you can set a flow-specific **External Token Connector** here. Configure connectors first under [External Token connectors](/connectors/connector-configuration-guides/token). See [External Token](/management/project-settings/external-token) for full setup. ## Return Token after Flow By default, a JWT is only issued when a flow involves user authentication. The **Return token after flow (without user authentication)** option allows a JWT to be returned on flow completion even if no user login occurs. This is useful for triggering connector logic, running risk or fraud assessments, or supporting flows that require a signed token for secure communication with downstream services without authenticating a user. # External Authentication (/flows/actions/external-authentication) Use the External Authentication flow action to redirect users to your existing login page and complete authentication. # External Authentication The **External Authentication** flow action lets you delegate authentication to your existing login system, then continue the Descope flow after your external auth completes. This is useful when you want to keep your current login UX and backend, while still issuing Descope tokens for downstream use cases. This action behaves as a **sign up or in** action: - If `loginId` does not exist, Descope creates a new user. - If `loginId` already exists, Descope signs in that existing user. ## How it Works 1. In your Descope flow, add the **External Authentication** action and configure your `External Auth URL`. ![External Authentication flow action](/assets/external-authentication-flow-action.webp) 2. During runtime, the flow redirects the user to that login URL, preserving your authentication user experience. 3. Descope appends an external auth request identifier to the login URL as a query parameter (`external_auth_req_id`). 4. Your external login app authenticates the user. 5. Your backend completes the flow by calling the management API: - `POST /v1/mgmt/flow/externalauth/complete` - Include the request ID as `externalAuthReqId` in the body 6. Descope responds with a redirect URL back into the flow, and the user continues from there. ![External Authentication flow action configuration](/assets/external-authentication-flow-action-configuration.webp) ### Bring Your Own Auth for MCP For the end-to-end MCP pattern using External Authentication in a user consent flow, see the [Bring Your Own Auth](/mcp/bring-your-own-auth) docs. ## Completing External Authentication via Management API Use your Project ID and Management Key in the authorization header: ```bash curl -X POST "__BaseURL__/v1/mgmt/flow/externalauth/complete" \ -H "Authorization: Bearer :" \ -H "Content-Type: application/json" \ -d '{ "externalAuthReqId": "be83dd51761c34372db888becadf8ebf", "loginId": "", "emailVerified": true, "phoneVerified": false, "customClaims": { "source": "external-auth" }, "selectedTenantId": "tenant-id-123", "userTenants": [], "user": { "givenName": "Kevin", "familyName": "Gao" } }' ``` ### Request Fields The `externalAuthReqId` field is a one-time flow-bound identifier that cannot be re-used. It is tied to a unique authentication request for security purposes. - `externalAuthReqId`: Request ID received from the external auth redirect query parameter - `loginId`: Login identifier for the user (for example email or username) - `user`: Optional user profile fields (for example `givenName`, `familyName`) - `emailVerified`: Whether email is already verified in your source system - `phoneVerified`: Whether phone is already verified in your source system - `customClaims`: **Optional** claims to include in the resulting flow/token context - `selectedTenantId`: **Optional** tenant to set as active tenant context (`dct`) in the downstream Descope session token - `userTenants`: **Optional** array of user-tenant associations, allowing you to associate the user (new or existing) with multiple tenant IDs ### Response Fields - `redirectUrl`: URL to redirect the browser back into the Descope flow ### Tenant Association If you are using Connections with your MCP tools, review [Multi-Tenancy with Connections](/agentic-identity-hub/core-components/connections/multi-tenancy) for how to handle the multi-tenancy scenario with Connection tokens. If you want to associate the user with one or more tenants, pass `userTenants` as an array in the completion request. The values in the array should be the tenant IDs of the associated tenants. If you want to set the active tenant context for the resulting session, pass `selectedTenantId`. This populates the `dct` claim in the downstream Descope session token and is used as tenant context for downstream flow actions like [Outbound App / Connect](/agentic-identity-hub/core-components/connections/storing-connections). To use this `dct` tenant-context behavior, ensure your JWT template is enabled for your project or MCP server with authorization claims configured to include `dct`. See [JWT templates](/management/token/jwt-templates#authorization-claims-configuration). If the tenant does not exist yet in Descope, create it first using the Management SDK/API before calling external auth completion. # Generate Audit Event (/flows/actions/generate-audit-event) This doc will show you how to use the generate audit event action within a flow. # Generate Audit Event The `Generate Audit Event` action creates custom audit events that are logged and available in your project's [Audit page](https://app.descope.com/audits) and any connected services. This action allows you to track specific business events, user actions, or system activities that are important for compliance, monitoring, or debugging purposes. Custom audit events generated through this action will appear alongside standard [Descope audit events](/audit-trails-and-integrations/audit-trail-streaming), providing a comprehensive audit trail for your application. ## Configuration The `Generate Audit Event` action allows you to specify: - **Step Name**: The name of the step within the flow - **Action**: A descriptive name for the event - **Type**: Choose from three severity levels: - **Information**: General events for tracking normal operations - **Warning**: Events that indicate potential issues or unusual behavior - **Error**: Events that represent failures or security concerns - **Event Data**: Any relevant data from the flow context, including user information, timestamps, or custom parameters - **Actor ID** (optional): The actor to record on the event. Leave it empty to use the default actor for the flow's context ![Generate Audit Event Configuration](/assets/generate_audit_event_config.webp) ## Attributing the Actor The **Actor ID** field sets the [Actor ID](/audit-trails-and-integrations/audit-events#fields) on the generated event. This is the same field that identifies who performed the action on built-in Descope events. It accepts [dynamic values](/flows/dynamic-keys), so you can draw the actor from the flow context. In an interactive flow, use `{{user.userId}}` to attribute the event to the signed-in user. In a [Management Flow](/flows/management-flows) started by a Descope audit event, use `{{triggeringEvent.metadata.actorId}}` to attribute the event to whoever performed the action that triggered the flow. This resolves to the Descoper's user ID for actions taken in the console, or to the management key ID for actions taken through the SDK or API. Some events carry no actor, such as changes made by an automated process, and the value is empty in those cases. You can also enter a static value, such as the ID of the service account a flow runs on behalf of. If you leave **Actor ID** empty, Descope falls back to the actor on the request that ran the flow. In a Management Flow, that is not always the actor of the event that started the flow, so set the field when your audit pipeline needs to record who performed the original action. ## Flow Implementation To generate a custom audit event within a flow, simply add the action at the relevant point of the flow. ![Generate Audit Event in Flow](/assets/generate-audit-event-flow.webp) ## Viewing Audit Events Generated audit events will appear in your project's [Audit page](https://app.descope.com/audits) with the following information: - Event timestamp - Action name - Event type (with appropriate severity indicators) - Event data (as JSON) - Actor ID (if set on the action) - User information (if available) - Flow context details You can filter and search these events using the audit page's built-in search and filtering capabilities. Additionally, these audit events will be [streamed alongside default events](/audit-trails-and-integrations/audit-events) to any configured [**Analytics** connectors](/connectors/connector-configuration-guides/analytics). Custom audit events are subject to the same retention policies as standard Descope audit events. # Generate JWT (/flows/actions/generate-jwt) Use the Generate JWT action to provision a user from a third-party IdP. # Generate JWT For flows where you can federate with OIDC or SAML, use [custom OAuth providers](/auth-methods/oauth/providers/custom-providers) or [tenant-based SSO](/auth-methods/sso) instead. The **Generate JWT** actions provision a user in Descope and return a session token as if the user had signed in with a standard Descope auth action. Use these actions when you have a **homegrown or third-party IdP** that cannot use OIDC or SAML. In your flow, validate the user (e.g. via a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) or an existing cookie), then add a Generate JWT action to create or match the user and issue a Descope JWT for the rest of your app or downstream flows. Also, if you're trying to use this action to use an existing auth provider with Descope as your MCP auth provider, see our [bring your own auth for MCP](/mcp/bring-your-own-auth) doc. ## Available Flow Actions There are three Generate JWT actions available. Each action determines how the user is provisioned when no matching Descope user exists: | Action | Behavior | |--------|----------| | **Generate JWT / Sign In with 3rd Party IdP** | Provisions a user only if they already exist in Descope (e.g., matched by login ID). Use when the user must have been created beforehand. | | **Generate JWT / Sign Up with 3rd Party IdP** | Always creates a new user in Descope. Use when this path is exclusively for new sign-ups. | | **Generate JWT / Sign Up or In with 3rd Party IdP** | If a user exists (e.g., matched by email or external ID), signs them in; otherwise creates a new user. Use for a single path that handles both new and returning users. | ## How It Works 1. **Previous step must set identity** - A prior screen or step in the flow must set `form.email` or `form.externalId` so the action knows what login ID to create or match a user with. For example, collect email in a form, or set it from the response of a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) that validated the user with your auth API. 2. **Use the Generate JWT action** - The action creates or signs in an existing user in Descope based on that login ID provided by the previous step. 3. **Follow Up Steps (Optional)** - After the Generate JWT action runs, you can update the user or the token using: - **Update User / Attributes** - to set or change user profile and attributes. - [**Custom Claims**](/flows/actions/custom-claims) - to add or change claims on the issued JWT. 4. **Token returned** - The flow returns a Descope session token (JWT) just as if the user had completed a standard Descope authentication action. Downstream steps and your application can use this token for authorization and API calls. This is a simplified example flow, using the **Generate JWT / Sign Up or In with 3rd Party IdP** action: ![example flow with generate jwt action](/assets/generate-jwt-example-flow.webp) ## Step-Up Authentication If a Generate JWT action runs after a [Step Up action](/mfa-and-step-up/step-up#option-1-using-descope-flows) in the same flow, the token it issues includes the `su` claim, just like a native sign-in action would. This lets you satisfy a step-up requirement for a user you're authenticating through a homegrown or third-party IdP, instead of signing them in and then challenging them again. See [Step-Up Authentication for Third-Party IdP Sign-Ins](/flows/use-cases/step-up-with-generate-jwt) for a full walkthrough. # Actions (/flows/actions) Learn how to utilize actions within your Descope flows. # Actions Actions are one of the building blocks of Descope flows, enabling you to perform specific tasks during user authentication journeys. They can be combined with screens and conditions to create complex authentication workflows. An action step in a flow can perform various tasks, such as initiating and completing authentication (e.g., social login, passkeys, or magic link verification), updating user information, creating tenants, associating roles, and much more. ## Adding Actions to Flows To add an action within your flow, within the [console flow builder](https://app.descope.com/flows), click the blue `+` at the top left, and select `Action`. ![Add an action to your Descope flow](/assets/add-action-to-flow1.webp) You can then select the action you want to add or search for the action you're looking for. Once selected, you can drag the action into your flow. You can then move it around, configure it, and connect it to other actions, screens, and conditions. ![Selecting an action to add to your Descope flow](/assets/add-action-to-flow2.webp) ## Handling Action Errors in Flows Descope allows you to configure what to do in the event of an action error during flow execution. You can select any action within your flow, expand the error handling area, and configure the handling to be `Automatic`, `Ignore`, or `Custom`. ![Descope Error handling within flow action configuration](/assets/flows-error-handling.webp) When you select custom, you can change the flow handling behavior based on the error of an action. For example, you could show a screen that says that an error occurred and suggest other authentication method instead. ![Error handling within Descope flows example](/assets/flows-error-handling-flow.webp) # Load User (/flows/actions/load-user) This doc will show you how to use load user action within a flow. # Load User The `Load User` action retrieves the authenticated user's full session context using the refresh token. This action is particularly useful when other steps in the flow require an authenticated user context. For example, actions like [updating user consent](/identity-federation/inbound-apps/creating-inbound-apps#implementing-a-consent-flow) or [connecting an outbound app](/identity-federation/outbound-apps/connect#connecting-with-descope-flows) rely on verified user details, which the `Load User` action makes available to the flow. After running the `Load User` action, user related [dynamic values](/flows/dynamic-keys#user) will become available in context throughout the rest of the flow. ## Using with Flows To use the `Load User` action in a flow: 1. Make sure the user is authenticated with a condition that checks if the `user.loggedIn` key is `true` 2. Add the `Load User` action to load the user's session and details 3. Reference the user details and context in subsequent steps of the flow ![Load User Action](/assets/load-user-action.webp) ### With Management Flows When using [Management Flows](/flows/management-flows), the `Load User` action can load disabled or expired users for management. To perform this, select the `Load User` action and check the "Load Invalid Users" checkbox under the action. ![Load Invalid Users Action](/assets/load-user-disabled.png) # Link User Identities Across Different Auth Methods (/flows/actions/multiple-login-id) Learn how to associate multiple login IDs for different authentication methods in your Descope flows. # Link User Identities Across Different Auth Methods When you are building out your flows, you might have a user sign up with an email and verify their user account with an OTP text message to their phone. When this occurs, the user will still have to use their email address to sign up, as the phone number is not associated with their login ID by default (most commonly their email). If you would like to verify and include their phone number as a login ID in one simple sign-up process for new users, this guide will show you how to do that. That way, a user can use their phone number the next time they log in, instead of just their email. ## How to Implement with Flows 1. Navigate to your [flow](https://app.descope.com/flows), and open up the Action block list with the *blue +* in the top left corner ![Descope multiple loginIDs guide configuring within flows step 1](/assets/multiple-login-ids-guide-1.webp) 2. If you want to update a login ID with a phone number, select **Update User / OTP / SMS** and place it on the flow page. If you need to update a login ID with an email or something else, select **Update User / OTP / Email**, etc. This **Update** user block will give you the option to include whatever the user inputs as a verification method, in their Descope login ID. ![Descope multiple loginIDs guide configuring within flows step 2](/assets/multiple-login-ids-guide-2.webp) 3. Double-click on the first block (**Update User**) and select the checkbox **Add to login IDs**: ![Descope multiple loginIDs guide configuring within flows step 3](/assets/multiple-login-ids-guide-3.webp) You can choose to replace all the user attributes from either the previous user created, or the new user created. Only the login IDs will be preserved between either option. The two options in this block will allow you to choose which one you would like to merge attributes with. This merging will only be used in case of a conflict in attributes, otherwise, you can ignore this. 4. You can then include this as part of your main sign-up-or-in flow, with a user logging in via email, but then verifying themselves via SMS OTP and including both the phone number and the email address used to sign up as a valid login ID. ![Descope multiple loginIDs guide configuring within flows step 4](/assets/multiple-login-ids-guide-4.webp) As you can see in the picture, my flow prompts the user for an email, verifies the email with OTP, and then if it's a new user, it prompts the user to enter their phone number and updates their login ID to include their phone number as well (so that they can use it to log in afterward). 5. And that's pretty much it! Now, under your sign-in flow (***for only existing users***) you can sign in with either email or phone number because both are part of the new user's login ID. ![Descope multiple loginIDs guide configuring within flows step 5](/assets/multiple-login-ids-guide-5.webp) You can see the phone number included as well if you head to the [Users](https://app.descope.com/users) portal and check under the Login ID column: ![Descope multiple loginIDs guide example of user with multiple loginIds](/assets/multiple-login-ids-guide-multiple-ids.webp) Without the **Update** User block, it would just include the user's email address. As you can see, this is a really nice feature to include in your sign-up flow, to provide your users with a natural sign-up experience using both their email and phone number. If you have any other questions about Descope or user login IDs, feel reach to reach out to [us](/support)! # Check Rate Limit (/flows/actions/rate-limit-action) When you need to block certain sensitive flow parts and want to rate limit by ASN / IP / JA4 you can use this action to do so. # Check Rate Limit The Check Rate Limit Action is only available to Growth and Enterprise license holders. The `Check Rate Limit` action in the flow is used when you wish to rate filter and block various values to protect parts of your flow. The action form allows you to enter a step name, a rate limit key, a timeframe in minutes, a max attempts threshold within that timeframe, and custom error messages. The rate limit is checked every time the flow execution passes through this action. When the limit is reached, an error is thrown. This helps prevent: 1. Brute-force abuse (several quick login attempts) 2. Email spam (flooding legitimate users with emails) 3. User enumeration (detecting which emails are registered) 4. Bots (JA4/IP fingerprinting) Here's a guide to [rate limiting with Descope](https://docs.descope.com/rate-limiting). ![Check Rate Action](/assets/rate-limit-action.webp) The rate limit key can either be IP, ASN, or JA4. ![Rate Limit Key](/assets/rate-limit-key.webp) The timeframe defines the window, in minutes, over which the action counts attempts; max attempts caps how many a user gets within that window before the rate limit triggers. A timeframe of `5` and a max attempts value of `5` allow 5 attempts every 5 minutes. It is also possible to configure error handling for the action. Handling can either be automatic, custom, or ignored. ![Rate Limit Error Handling](/assets/rate-limit-error.webp) # Reset Form (/flows/actions/reset-form-action) When you're designing a flow and need to reset all of the previous user inputs you can use this action to do so. # Reset Form The `Reset Form` action in the flow is used when you wish to reset previous user inputs that were taken in the flow. An example of where you might use this would be the following: 1. You first sign up and login via `SMS / OTP` with your phone number. Your login ID is now your phone number. 2. You then apply this `Reset Form` action, so that the current login ID as your phone number is not automatically picked up and used by the next action. 2. You then login via email / password as the 2nd factor, and because of the `Reset Form` action, you can input a totally separate email that is not part of your current login ID(s) and it will allow you to sign in with it. ![Reset Form Action](/assets/reset-form-action.webp) Basically, anytime you need to reset your previous form inputs, you can use this action. # Scriptlets (/flows/actions/scriptlets) Run custom JavaScript in a Descope Flow with the Scriptlet action. # Scriptlets Flows are built from screens, actions, conditions, and connectors. Those steps are customizable, but their outputs are fixed shapes that Descope writes into the flow context. When you need something more flexible — hashing, date math, string transforms, or other small logic that does not fit a built-in action — use the **Scriptlet** action. It runs custom JavaScript in the flow and writes the values you return back into context. ![Scriptlet action in the flow editor](/assets/scriptlets-1.webp) ## Using Scriptlets Add a **Scriptlet** action from the flow editor, then configure arguments, code, the result context key, and optional overrides. ### Arguments Arguments pass values into the script. Each argument has a name (used as a variable in your code) and a source: | Type | Description | | ---- | ----------- | | **Dynamic** | Value read from flow context at runtime (for example `form.displayName`) | | **String / Boolean / Number / Time** | Static value you set in the action, validated by type | ![Scriptlet arguments](/assets/scriptlets-2.webp) In the example above, the Scriptlet builds a `greeting` string. If `form.displayName` is empty, it falls back to a static default such as `John`. ### Available Libraries The Scriptlet runtime includes: - [Lodash](https://lodash.com/) — iteration, object helpers, and similar utilities - [CryptoJS](https://cryptojs.gitbook.io/docs) — hashing and related crypto helpers Use them directly in your script (for example Lodash collection helpers or CryptoJS hash functions). Secure random number generation (for example `CryptoJS.lib.WordArray.random()`) is **not supported** in Scriptlets. For non-cryptographic random values, use `Math.random()` instead. ### Context Key The object your script `return`s is stored under the context key you configure (default paths look like `scripts.scriptletResult`). ![Scriptlet context key](/assets/scriptlets-3.webp) Example output shape: ```json { "scripts": { "scriptletResult": { "greeting": "Hello, John!" } } } ``` Later actions and conditions can read those keys from context (for example `scripts.scriptletResult.greeting`). ### Overrides Use **Overrides** to copy Scriptlet results onto existing flow context keys such as `form.displayName`, `form.email`, or `form.customAttributes.*`. ![Scriptlet overrides](/assets/scriptlets-override.webp) For example, strip `+` subaddresses from an email in the Scriptlet, return `{ email: result }`, then override `form.email` with `scripts.scriptletResult.email`: ```js let result = email; const domainIndex = email.lastIndexOf('@'); if (domainIndex > -1) { // Treat '+' after the first character as a subaddress separator const aliasIndex = email.substring(1).indexOf('+') + 1; if (aliasIndex > 0) { result = email.substring(0, aliasIndex) + email.substring(domainIndex); } } return { email: result, }; ``` ![Mapping a Scriptlet result onto form.email](/assets/scriptlets-override-2.webp) If you override a custom screen input, define that field's context key on the screen component first. ![Custom input component context key](/assets/custom-input-component-context-key.webp) #### Overriding the Tenant To switch tenants in an existing session without running another flow, use [`selectTenant`](/client-sdk/auth-helpers#available-functions) on the client SDKs. If a Scriptlet overrides tenant-related context keys, Descope includes the [`dct` (Descope Current Tenant) claim](/management/token#user-session-and-refresh-tokens) in the resulting JWT, updates JWT structure as needed, and applies tenant-specific roles and permissions. ### Logging and Debugging Scriptlets support the standard `console` methods - `console.log()`, `console.warn()`, `console.error()`, and `console.debug()`. Anything you log is written to the [Flow Runner](/handling-flow-errors/troubleshooting-flows#flow-runner) messages when you run the flow from the Descope console. This lets you inspect intermediate values while the flow executes, without having to return them into flow context just to see them. ```js console.log(`Incoming email: ${email}`); const domainIndex = email.lastIndexOf('@'); if (domainIndex === -1) { console.error(`Email is missing a domain: ${email}`); return {}; } const domain = email.substring(domainIndex + 1); console.debug(`Extracted domain: ${domain}`); return { domain: domain, }; ``` ![Scriptlet console output in the Flow Runner messages panel](/assets/flow-runner-logs.webp) To view the output, run your flow from [Flows](https://app.descope.com/flows) in the Descope console and check the Runner messages panel below your flows. ### Testing Scriptlets Use **Test** in the Scriptlet editor before you rely on the action in a live flow. The test panel lists the **Arguments** you defined above. For each **Dynamic** argument, you can override the value that would normally come from flow context — enter any sample input you want to try. Static arguments keep the values you already set on the action. Run the test to see the object the Scriptlet would return (and therefore what lands under your context key). That makes it easy to confirm hashing, date math, or string transforms without stepping through the full flow. ![Scriptlet test panel](/assets/scriptlets-4.webp) ## Examples Here are some examples of how to use Scriptlets in [Flows](/flows). ### Hash an Email Domain for a Tenant ID Create a tenant for a new user and derive a stable tenant ID from the user's email domain (for example with CryptoJS hashing), then pass that value into **Create Tenant**. ![Flow that creates a tenant for a new user](/assets/scriptlets-5.webp) ![Scriptlet that hashes the email domain](/assets/scriptlets-6.webp) ![Create Tenant action using the Scriptlet result](/assets/scriptlets-7.webp) ### Time-Based MFA Condition Prompt for MFA when the user has not authenticated in the last 30 days. ![Flow that branches into MFA](/assets/scriptlets-8.webp) Compare last authentication time to the current time in a Scriptlet: ![Scriptlet comparing last auth time to now](/assets/scriptlets-9.webp) Use the boolean (or age) output in a **Condition** step: ![Condition using the Scriptlet output](/assets/scriptlets-10.webp) # User Invite (/flows/actions/user-invite) This doc will show you how to invite users within a flow. # User Invite ## `User / Invite` Action The `User / Invite` action allows you to invite users via email within your Descope Flows. This is useful for creating onboarding experiences where authenticated users can invite others to join your application. ### Flow Setup To create a flow that prompts users to invite others: 1. Add an invitation screen after user authentication with the `Invite Users` input field to collect email addresses of users to be invited. 2. Add the `User / Invite` action to send the invitations ![Inviting Users Flow Screen](/assets/invite-users-flow-invite.webp) ![Inviting Users in Flow](/assets/invite-users-flow.webp) ### Configuring the `User / Invite` Action Within the `User / Invite` Action, you can configure the messaging connector and template used for the invitation. Refer to the [messaging templates doc](/management/messaging-templates#user-invitation-templates) for configuration steps. You can also define [tenant-level roles](/authorization/role-based-access-control#tenant-level-roles), [federated application](/identity-federation/applications) access, and attributes, including custom attributes, for the invited users. Additionally, you can configure whether only tenant admins are allowed to invite users, or if any authenticated user can send invitations. For instance, restricting invitations to tenant admins can streamline the onboarding of additional tenant admins, while allowing any authenticated user to invite others can facilitate referral programs. ![User Invite Flow Action Configuration](/assets/user-invite-flow-action.webp) # Validate Email Address (/flows/actions/validate-email-action) Use the Validate Email Address action in Descope Flows to check email format and verify the domain has MX records before sending. # Validate Email Address The `Validate Email Address` action checks an email address before your flow sends anything to it. It performs two checks: 1. **Format validation** - confirms the address is syntactically valid. Entries such as `invalidemail.com`, `user@`, or `@domain.com` fail this check. 2. **MX record lookup** - queries DNS for the domain's mail exchange (MX) records. A domain with no MX records cannot receive email, so the address is rejected. ## How It Works Place this action before any step that sends an email, so it can confirm the address is valid first. This avoids sending unnecessary emails to addresses that don't exist and protects your sending reputation. An MX lookup confirms that the domain can receive email. It does not confirm that the specific mailbox exists. An address like `neverever@example.com` passes as long as `example.com` has valid MX records. ## Adding the Action to Your Flow 1. Open your flow in the [Flow Builder](https://app.descope.com/flows). 2. Click the `+` in the top left corner and select `Action`. 3. Search for `Validate Email` and select **Validate email address**. 4. Drag the action into your flow and connect it after the screen that collects the user's email. ![Searching for the Validate email address action in the Descope flow builder](/assets/validate-email-action-search.webp) ## Error Handling The action returns a single error, **Invalid email address**, when the address fails either check. You can set the handling behavior for that error and optionally provide a custom error message, which overrides the default system message. | Handling | Behavior | | --- | --- | | **Automatic** | Automatically return to the previous screen and display the error in its designated message component. This is the default. | | **Mitigate** | Send the flow to a step you choose and clear the error, treating it as resolved. | | **Continue** | Send the flow to a step you choose and keep the error so it can be displayed there. | | **Ignore** | Treat the step as successful and continue or exit the flow without further error handling. | | **Abort** | End the flow and send the error to your application instead of showing a screen. | For more detail on each option, see [Customizing Flow Errors](/handling-flow-errors/customizing-flow-errors). ![Configuring error handling for the Validate email address action](/assets/validate-email-action-config.webp) ## Testing the Action Run your flow and submit an address with a domain that has no MX records, such as `user@example.invalid`. The action fails and, with `Automatic` handling, returns you to the previous screen with the error message displayed. Submitting a valid address allows the flow to continue to the next step. ![Example flow using the Validate email address action](/assets/validate-email-action-flow.webp) # Verify Token (/flows/actions/verify-token) This doc will show you how to verify tokens within a flow. # Verify Token The `Verify Token` action is also available in [Management Flows](/flows/management-flows). You can use the `Verify Token` action in a flow, or management flow, to validate [Magic Link](/auth-methods/magic-link) and [Embedded Link](/auth-methods/embedded-link) tokens. For example, if you are adding a [Magic Link token to your invitation link](/management/project-settings#sign-ups-and-user-invitations), you need to verify the token within the flow that the invitation link opens. Additionally, you can use the `Verify Token` action when using Descope as an [OIDC provider](/getting-started/web-development-platforms#integration-as-an-oidc-provider). By passing the `t=` parameter through the OIDC flow, you can verify the token using this action as well. The same applies when using Descope as a [SAML Identity Provider](/identity-federation/applications/saml-apps#idp-initiated-sso): a `flow_token` passed on the IdP-initiated URL is forwarded into the flow as `t=`, and can be verified with this action the same way. ## How It Works To use the `Verify Token` action, start your flow with a condition that checks whether a `t=` token is present (this covers Magic Link, Embedded Link, OIDC, and SAML IdP-initiated `flow_token`; all of them arrive in the flow as `t=`). If a token is found, proceed with the `Verify Token` action. If no token is present, you can prompt the user to authenticate instead. ![Token present condition](/assets/external-token-condition.webp) ![Verify Token Flow](/assets/verify-token-flow.webp) # Flow Library (/flows/intro-to-flows/flow-library) Learn how to utilize Descope's flow library to explore flow templates to use within your application. # Flow Library This guide will cover utilizing Descope's flow library to explore flow templates for your application. These flow templates can be used as a baseline when building your flows. The flow library is an excellent tool for exploring use cases and gathering ideas for improving your authentication flow to add more auth methods, security features, etc. To view Descope's flow library, navigate to the [flows](https://app.descope.com/flows) section of the Descope console and click `Start from template`. ![Navigating to Descope's flow library](/assets/navigate-to-flow-library.webp) ## Navigating the Flow Library Once you've opened the Descope flow library, you will see an overview like the image below. You can navigate the methods along the left-hand navigation or even search for specific use cases. ![An overview view of Descope's flow library](/assets/flow-library-overview.webp) The search bar on the navigation panel can search for templates based on use case, description, connectors, methods along with tags of each template. ## Preview a Template You can preview a template from the flow library by hovering it and clicking the `Preview` option. ![How to preview a template within Descope's flow library](/assets/preview-flow-template.webp) Once in the template's preview mode, you can zoom in and navigate the flow to see the screens, actions, conditions, etc. If you want to work with this flow further and test it out, you can click the `Use Template` button at the bottom right as well. ![Previewing a template within Descope's flow library](/assets/preview-open-flow-template.webp) ## Using a Template You can use a template from the flow library by hovering it and clicking the `Use Template` option, or if in the preview mode, you can also click the `Use Template` button at the bottom right as well. ![How to use a template within Descope's flow library](/assets/preview-flow-template.webp) ### Customizing a Template Once you have selected a template, you can set the flow `Name`, `ID`, and `Description`. Then, select `Create` to add the flow to your project. ![Adding a flow template from Descope's flow library to your project](/assets/customizing-flow-template.webp) Now that the flow template has been added to your project as a flow, you can run it within the flow runner to test its functionality and further customize or add more flow screens, actions, and conditions to meet your use case. ![Viewing a flow created from Descope's flow library in the flow runner](/assets/customizing-flow-template1.webp) ## Requesting a New Template If you have a use case that isn't covered by an existing template, you can request a new one directly from the flow library by clicking the `Request a template` button in the bottom left corner. ![Requesting a new template within Descope's flow library](/assets/request-flow-template.webp) # Flow Notes (/flows/intro-to-flows/flow-notes) Learn how to annotate steps in the Descope flow editor with notes, so anyone opening the flow can understand what each part of it does. # Flow Notes When flows get complex, it can be difficult for maintainers and reviewers to understand the purpose of chained components at a glance. Flow notes address this by allowing you to attach brief, contextual explanations directly to any step. ## Working with Notes You can easily manage notes directly within the [flow editor](https://app.descope.com/flows). To get started, hover over a step, click the **note icon** in the pop-up toolbar, and type your message in the card below. ![Flow notes in the Descope flow editor](/assets/flow-note-anote.webp) - **Edit & Save:** Press `Enter` to add a new line (notes are plain text and limited to 500 characters). Save your note using the check button or by clicking anywhere outside of it. As with any other change in the editor, remember to save the flow itself to persist your notes. - **Visibility:** By default, notes stay hidden to keep your canvas clean. Hover over an annotated step to reveal its note, which closes again when you move away. Select the **pin button** to keep a note permanently visible on the canvas. - **Show notes:** To read all notes at once, use the **Show Notes** toggle at the bottom right of the canvas. - **Color:** Select the **palette button** to change a note's color. - **Delete:** Select the **trash button** on an open note to remove it. Alternatively, clearing all of the text and saving will automatically delete the note. Notes are saved as part of the flow itself, so [exporting a flow](/management/flows#exporting-and-importing-flows) includes all of its notes, and importing that flow restores them. # Flow Versioning (/flows/intro-to-flows/flow-versioning) Learn how to utilize Descope's flow versioning to manage changes, track history, and restore previous versions whenever needed. # Flow Versioning Descope supports comprehensive flow versioning, making it easy to manage changes, track history, and restore previous versions whenever needed. Every time you save changes to a flow, Descope automatically creates a new version, allowing you to maintain a complete audit trail of your flow's evolution. ## Saving Flow Versions When saving changes to a flow, you have the option to include a note describing what was modified. These version notes help you keep a clear record of how your flows have evolved over time and make it easier to identify which version contains specific changes or fixes. To save a version with a note: 1. Make your changes in the flow editor 2. Select the **Save & Note** option 3. In the save dialog, optionally add a descriptive note about the changes 4. Click **Save** to create the new version ### Viewing Version History To access the version history for a flow, head to the [Flows page](https://app.descope.com/flows) in the Descope Console and click the version history icon in the top-right corner of the flow editor. The version history interface displays all previous versions of your flow, including: - **Version number** - **Date and Timestamp** - **Version notes** (if provided when saving) - **Author** who made the changes - **Actions** available for each version ![Flow version history interface](/assets/flow-version-history.webp) ## Previewing Versions You can preview any past version directly from the version history interface. This allows you to: - Review the exact state of screens, actions, and configurations at any point in time - Understand what changes were made between versions Simply click on a version in the history to preview it. The preview shows the flow in read-only mode, so you can examine it without making any changes. ## Restoring Versions You can restore a previous version by clicking the **Restore** button on the version you want to revert to. 1. Open the version history for your flow 2. Select the version you want to restore 3. Click **Restore** on the version you want to revert to 4. Confirm the restoration This will immediately replace the current version of the flow with the selected previous version. The current version will be preserved in the history, so you can always restore it if needed. ![Restore flow version](/assets/restore-flow-version.webp) ## Comparing Versions If you have [AI features enabled for your Descope company](/management/company-settings#permissions), then you can generate an AI-powered analysis of how the current version of your flow compares to a specific past version. The generated report will list the differences in flow behavior and components between the given version and the current one, cumulative across all versions between them. 1. Open the version history for your flow 2. Select the version you want to compare against 3. Click **Compare Version** on the version you want to compare against ![Compare flow version](/assets/compare-flow-version.webp) ## Duplicating Versions as New Flows You can also duplicate any previous version as a completely new flow. This is useful when you want to: - Create a variant of a flow based on a previous version - Experiment with changes without affecting the current flow - Maintain multiple versions of a flow as separate entities To duplicate a version: 1. Open the version history 2. Select the version you want to duplicate 3. Click **Create as New Flow** 4. Give the new flow a name and ID The duplicated flow will be a completely independent flow, allowing you to modify it without affecting the original. # Migrate to Flows v2 (/flows/intro-to-flows/migrate-flows-v2) This article covers the update process for Descope flows. # Migrate to Flows v2 We're thrilled to announce a new version of our flow infrastructure (V2) designed to provide you with enhanced performance, scalability, and new features. In this article, we'll guide you through the simple process of updating your flows. You'll find step-by-step instructions and best practices to ensure a smooth transition. While the updates are under the hood and won't affect your day-to-day interactions within the Flow Builder or Styles configuration, they are critical for future compatibility. It is critical to note that V1 Flows will not be compatible with newer SDK releases. See [this matrix](#sdk-compatibility-matrix) for flow and SDK version compatibility details. ## Updating Descope Flows ### Before Updating Updating to the V2 infrastructure is a project-wide update, which means all of the flows and styles within the project will be updated. Updating your flows to V2 is a one-way activity with no revert possible. With this change across the project, doing some due diligence when deploying to your production environment is recommended. - You must ensure that you use a specific SDK version with your V1 Flows and not import the latest SDK versions. [This matrix](#sdk-compatibility-matrix) shows the SDK and flow version compatibility. - It is recommended to test V2 flows within your application before upgrading your production flows to V2. To test, we suggest utilizing a dev or sandbox project. If you do not have a dev or sandbox project, you can export and import your flows and styles within a new project. You'll then be prompted to update your flows within that project and test with your application. - [Exporting and Importing Flows Guide](/management/flows#exporting-and-importing-flows) - [Exporting and Importing Styles Guide](/management/styles/managing-themes-sdks) - If you have E2E tests running on your application, such as [Cypress Tests](/unit-testing/e2e-testing-guides/e2e-cypress), you will need to update your tests and validate that they are functioning as expected after updating your flows. It is recommended to make the applicable updates within your development environment, and then update your production E2E tests when you migrate your production project to flows V2. - If you are utilizing a Content Security Policy (CSP) on Flows V1, you will need to update your CSP to also include `static.descope.com` as a script src. You can see an example CSP within our [Security Best Practices guide](/security-best-practices/content-security-policy) which includes this configuration. ### Updating within Console Within the Descope Console, flows can be updated to V2 within the [Flows](https://app.descope.com/flows), [Styles](https://app.descope.com/styles), or in the Flow Builder screen. The below are examples of the prompt to update your flows within the console. To update, simply click the `Update` button. ![A prompt to update flows to V2 within the Flows page in Descope](/assets/flows-v2-update.webp) ![A prompt to update flows to V2 within the Styles page in Descope](/assets/flows-v2-update-styles.webp) ### Updated Confirmation Once the update to V2 has been completed, you will receive a confirmation. You can then update your application to utilize the correct SDK versions per [this matrix](#sdk-compatibility-matrix) ### SDK Compatibility Matrix You must ensure that you use a specific SDK version with your V1 Flows and not import the latest SDK versions. This matrix shows the SDK and flow version compatibility. | SDK | V1 Flow Support SDK Version (`<=`) | V2 Flow Support SDK Version (`>=`) | | --- | -------------------------------- | -------------------------------- | | React | v1.0.14 | v2.0.0 | | Web Component | v2.11.8 | v3.0.0 | | Core JS | v1.10.1 | v2.0.0 | | Vue | v1.0.4 | v2.0.0 | # Subflows (/flows/intro-to-flows/subflows) Learn about Descope Subflows, a way to utilize our visual no-code interface between one or more flows. # Subflows Descope Subflows are a way of reusing a flow within another flow. Subflows can be used in scenarios where a user needs to create a custom flow utilizing multiple flows. Any flow can be used within a subflow. This means that any flow ('subflow') can be added to another flow ('main flow') and be a part of its logic. This overview of Descope Subflows will cover creation, editing and managing subflows using our flow builder. Further details about flow builder shortcuts, custom flows, disabling flows, and exporting/importing flows can be found under [Managing Flows](/management/flows). ## Managing Descope Subflows ### View a Subflow When subflows are used, they are referenced in the "Used In" column of the main flow using it. ![Subflows view example](/assets/subflows-view.webp) ### Create a Subflow Subflows can be utilized in flows which are created from scratch as well as those derived from a flow template. Open the flow builder of the selected flow, click on the `+` button at the top right. You will then select `Flow` option from where you choose a flow from the library. ![Subflows create example](/assets/subflows-create.webp) All available flows from "Your Flows" appear in this list. The only flow that doesn't appear is the current flow itself. ![Subflows dropdown selection](/assets/subflows-selection.webp) A new subflow step type is added, having aggregated multi-steps view. By default, it will appear in "collapse" mode. You click on each step to expand them. ![Subflows component](/assets/subflows-component.webp) In order to access the subflow, just double click on the subflow component, a new tab opens right next to the main flow in use. ![Subflows tab](/assets/subflows-tab.webp) Once the subflow placement in the main flow is completed, save the flow and run it to test its working. ![Subflows included in a flow](/assets/subflows-completed-flow.webp) One thing to note is that you can add flows inside a subflow as well. All the flows will then appear as multiple tabs (as seen in the above image) in the flow builder for easy access. Additionally, any output of the subflow which is not handled, as well as any end step, becomes an output of the subflow's step in the main flow. This will further require handling (linking) in the main flow. Unlike a regular end that returns a jwt, when in subflow, the flow will just `continue` to the next step without generating a jwt token. Just like in a regular flow, context such as form inputs, connector responses, and scriptlet outputs is still passed along to these subflows, where it can be further utilized. ### Edit a Subflow A subflow step can be modified by clicking onto the pencil icon to edit. Once clicked, a new tab opens in the flow builder of the selected subflow. The user will edit this subflow like any other regular flow. ![Subflows editing](/assets/subflows-editing.webp) Additionally, when a flow has been edited, an asterisk appears right next to the subflow name on the tab. This indicates your unsaved work. Make sure to save your changes. Descoper can work on the main flow concurrently, even if the subflows are in progress. Since Descoper is editing the subflow, they will be able to open it in the same flow builder. This option is not limited to one subflow per main flow, but any subflow the Descoper wishes to view/edit. ### Pass inputs to a subflow When a subflow is added to a main flow, the subflow can be passed inputs from the main flow. Subflow inputs are defined in the same way as [flow outputs](https://docs.descope.com/flows/management-flows#flow-outputs-with-end-action) - by mapping a key to a value or context-derived data from the parent flow. Each key you define becomes available inside the subflow. To configure inputs for a subflow, click the **gear icon** on the subflow component in the main flow. In the subflow input settings, define the keys and map them to the desired values. You can reference the passed inputs using the `{{subflowInput.key}}` syntax: ![Subflows inputs](/assets/subflows-inputs.webp) ### Save a Subflow Saving operation is done per flow. While navigating outside the flow builder, all the flows will show an asterisk symbol as mentioned in the above section. These indicate all current open flows. ### Delete a Subflow Descoper gets an option to delete a flow being used in other flows. Simply click on the 3 dots and select `Delete` from the action menu. If this flow was reused in other flows, the Descoper sees a warning dialog box with list of flow names where the deleted flow is being used. Once confirmed by providing flow name, your selected flow is deleted. ![Subflows delete](/assets/subflows-delete.webp) ![Subflows delete confirmation](/assets/subflows-delete-confirmation.webp) # Management Flows (/flows/management-flows) In this article, you will learn about management flows in Descope. # Management Flows Management Flows are autonomous, backend operations that run without user interaction. Unlike [Interactive Flows](/flows) that handle user-facing authentication journeys, Management Flows provide automated responses to authentication events and user management tasks. These flows can be triggered in two ways: - **API calls**: Manual execution via API calls from your backend systems in response to your own system events, audit logs, or scheduled tasks - **[Audit Events](/audit-trails-and-integrations/audit-events)**: Automatic execution when configured to start based on specific Descope audit events ## Creating Management Flows You can create a new Management Flow from the [Flows page](https://app.descope.com/flows) of the Descope Console. Select **Create from Scratch**, set the flow **Name** and **ID**, and mark the Flow type as **Management**. ![Create Management Flow](/assets/create-management-flow.webp) ## Designing Management Flows Management Flows use a specialized set of backend-focused components designed for autonomous operations: ### Available Components - **Actions**: Handle backend operations like user and tenant updates, updating consent, and generating audit events - **Connectors**: Provide integration with external systems and databases - **Conditions**: Enable logic branching based on data, events, or flow context Interactive components like screens are not available in Management Flows, maintaining their focus on autonomous backend operations. ### User Management Actions Management Flows include dedicated actions for controlling user account state: **User / Disable** — Disable a user. The user ID is extracted from the triggering event if present. For API-triggered flows, pass the user ID via `options.input.userId` in the HTTP call, or directly set `client.userId`. **User / Enable** — Enable a user. The user ID is extracted from the triggering event if present. For API-triggered flows, pass the user ID via `options.input.userId` in the HTTP call, or directly set `client.userId`. ![User Enable and Disable Action](/assets/management-flow-user-enable-disable.webp) ### Flow Outputs with `End` Action Every Management Flow should conclude with an `End` action that defines what data gets returned in the JSON response. In the End action, you can configure flow outputs by specifying the **Key**, **Type**, and **Value**. These outputs will be included in the API response when the Management Flow completes, allowing you to retrieve processed data or confirmation of completed operations. ![Management Flow End](/assets/management-flow-end.webp) ## Triggering Management Flows Management Flows can be triggered in two ways: via API calls or automatically through audit events. ### Using SDKs / APIs You can also run Management Flows with the [Run Management Flow API](/api/management/flows/run-management-flow) if you prefer, or if you're using a backend that doesn't have a dedicated Descope SDK. Management Flows are typically executed with a [Management Key](/management#management-keys) for secure access to management operations. The recommended way to run them from your backend is with the Descope SDKs. You can see our [Management Flows with SDKs](/flows/management-flows/with-sdks) guide for proper examples. For Management Flows that require input parameters, pass them using the `options` field. In the management flow itself, the input is available in the flow context as `client.`. All of the outputs defined in the [`End` action](/flows/actions/end-action) are returned as JSON in the API response. ### Audit Events You can configure a **Management Flow** to automatically start when specific Descope [audit events](/audit-trails-and-integrations/audit-events) occur, rather than requiring them to be manually triggered via API calls. You can do this by modifying the **Start** action in your Management Flow to be triggered by a Descope audit event. When configuring the **Start** action, you can select **Descope audit event types** that will automatically trigger the flow execution. This enables real-time, event-driven automation without needing to make explicit API calls from your backend. ![Management Flow Start](/assets/management-flow-start.webp) #### Loading Users from Triggering Events When a Management Flow is triggered by an audit event, you can use the **Load User / From Triggering Event** action to load the user associated with that event. This action automatically retrieves the user based on the triggering event's user ID, making the user data available in your flow context for subsequent operations. This is particularly useful when you need to perform user-specific operations based on the audit event that triggered the flow, such as updating user attributes, sending notifications, or performing user management tasks. ![Load User / From Triggering Event](/assets/management-flow-load-user-from-triggering-event.webp) #### Filtering Events with Conditions You can use [Conditions](/flows/conditions) within your Management Flow to further filter which audit events should trigger the flow. By adding conditions with [`triggeringEvent`](/flows/dynamic-keys#event-triggers), you can create more granular control over when your Management Flow executes based on specific event properties or attributes. For example, you might want to trigger a Management Flow only for certain types of audit events or when specific conditions are met within the event data. Conditions allow you to evaluate event properties and create branching logic that determines whether the flow should proceed. ![Management Flow Conditions](/assets/management-flow-conditions.webp) ## Use Cases ### Audit Event Automation Use your own audit events to trigger Management Flows via API calls for real-time responses. For example, when your system detects a [SCIM event](/management/tenant-management/scim), your backend can call a Management Flow that uses a [Messaging Connector](/connectors/connector-configuration-guides/messaging) to send an alert to admins. ### SSO Configuration Change Notifications Trigger a Management Flow on the `SAMLSettingsModified` or `OAuthSettingsModified` [audit event](/audit-trails-and-integrations/audit-events) to react in real time when a tenant changes its SAML or OAuth SSO settings. Use it to alert your team or update a provisioning system the moment a customer finishes SSO setup. ### Automated User Lifecycle Management Use Management Flows to enforce inactivity policies and automate account lifecycle decisions without manual intervention. Configure your backend to track user activity thresholds and call a Management Flow when a threshold is crossed. Within the flow, use the **User / Disable** action to immediately deactivate the account — preventing login until re-enabled. When the user returns and passes additional verification, a separate flow can call **User / Enable** to restore access. This pattern supports: - **Inactivity detection**: Disable accounts after a configurable period of no activity, requiring step-up verification on the user's next login attempt - **Automatic deactivation**: Suspend users who exceed inactivity thresholds based on your own audit events or scheduled backend jobs - **Reactivation flows**: Enable accounts after the user completes a verification challenge, keeping the lifecycle fully automated **User / Disable** and **User / Enable** are only available inside Management Flows. For API-triggered flows, pass the target user ID via `options.input.userId` in the request body. ### User Engagement Automation Automate user lifecycle management through intelligent workflows, like sending follow-up emails to users who haven't accepted invites, or deleting users after a period of inactivity. ### [Client Registration Flow](/agentic-identity-hub/core-components/mcp-servers/settings#client-registration-flow) in the Agentic Identity Hub Clients registered with [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) do not use the Client Registration Flow. You can define a Management Flow that will be triggered when an MCP client registers using [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr). You can use a management flow and conditional logic to verify the OAuth client's attributes and request and set its `verified` status appropriately. # Management Flows with SDKs (/flows/management-flows/with-sdks) Learn how to easily implement management flows for your app with Descope using the Descope backend SDKs. # Management Flows with SDKs You can use the Descope management SDK to run Management Flows and retrieve their outputs. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). For an overview of Management Flows, see [Management Flows](/flows/management-flows). ### Run Management Flow You can also use our [Run Management Flow API](/api/management/flows/run-management-flow) to run a Management Flow. This operation runs a Management Flow by its flow ID. Outputs configured on the flow `End` action are returned in the response. The flow must be a Management Flow, not an interactive flow. ```javascript // Args: // flowId (str): The Management Flow ID to run. const flowId = "my-management-flow"; // options (object): Optional run options. // input (object): Optional key/value input available in the flow as client.. const options = { input: { email: "user@example.com", }, }; const resp = await descopeClient.management.flow.run(flowId, options); if (!resp.ok) { console.log("Failed to run management flow.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully ran management flow.") console.log(resp.data) } ``` ```python # Args: # flow_id (str): The Management Flow ID to run. # options (FlowRunOptions | dict): Optional run options. # input (dict): Optional key/value input available in the flow as client.. # preview (bool): Optional preview flag. # tenant (str): Optional tenant ID. from descope import FlowRunOptions try: resp = descope_client.mgmt.flow.run_flow( flow_id="my-management-flow", options=FlowRunOptions( input={"email": "user@example.com"}, ), ) print("Successfully ran management flow.") print(resp) except AuthException as error: print("Failed to run management flow.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. // In cases where context is absent, context.Background() is a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // flowID (str): The Management Flow ID to run. flowID := "my-management-flow" // options (*descope.MgmtFlowOptions): Optional run options. // Input (map[string]any): Optional key/value input available in the flow as client.. options := &descope.MgmtFlowOptions{ Input: map[string]any{ "email": "user@example.com", }, } res, err := descopeClient.Management.Flow().RunManagementFlow(ctx, flowID, options) if err != nil { fmt.Println("Failed to run management flow: ", err) } else { fmt.Println("Successfully ran management flow.") fmt.Println(res) } ``` ```csharp // Args: // request (RunManagementFlowRequest): Request containing the flow ID and optional options. // FlowId (string): The Management Flow ID to run. // Options (ManagementFlowOptions?): Optional run options. // Input (ManagementFlowOptions_input?): Optional input; keys go in AdditionalData and are available as client.. // Preview (bool?): Optional preview flag. // Tenant (string?): Optional tenant ID. var request = new RunManagementFlowRequest { FlowId = "my-management-flow", Options = new ManagementFlowOptions { Input = new ManagementFlowOptions_input { AdditionalData = new Dictionary { { "email", "user@example.com" } } } } }; try { var response = await descopeClient.Mgmt.V1.Flow.Run.PostWithJsonOutputAsync(request); // Access JSON properties directly using JsonElement var root = response.OutputJson!.Value; var email = root.GetProperty("email").GetString(); // Access nested objects using standard JsonElement methods var count = root.GetProperty("obj").GetProperty("count").GetInt32(); var enabled = root.GetProperty("obj").GetProperty("enabled").GetBoolean(); } catch (DescopeException ex) { // Handle the error } ``` ### Run Management Flow Asynchronously You can also use our [Run Management Flow Async API](/api/management/flows/run-management-flow-async) to run a Management Flow asynchronously. For long-running Management Flows, start an asynchronous run to receive an execution ID immediately. Use [Get Management Flow Async Result](#get-management-flow-async-result) with that ID to retrieve the output when the flow completes. ```python # Args: # flow_id (str): The Management Flow ID to run. # options (FlowRunOptions | dict): Optional run options. # input (dict): Optional key/value input available in the flow as client.. from descope import FlowRunOptions try: # Returns immediately with an execution ID async_result = descope_client.mgmt.flow.run_flow_async( flow_id="my-management-flow", options=FlowRunOptions( input={"email": "user@example.com"}, ), ) execution_id = async_result["executionId"] print("Successfully started async management flow.") print(execution_id) except AuthException as error: print("Failed to run management flow asynchronously.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() // flowID (str): The Management Flow ID to run. flowID := "my-management-flow" // options (*descope.MgmtFlowOptions): Optional run options. options := &descope.MgmtFlowOptions{ Input: map[string]any{ "email": "user@example.com", }, } executionID, err := descopeClient.Management.Flow().RunManagementFlowAsync(ctx, flowID, options) if err != nil { fmt.Println("Failed to start async management flow: ", err) } else { fmt.Println("Successfully started async management flow.") fmt.Println(executionID) } ``` ```csharp // Args: // request (RunManagementFlowRequest): Same request shape as the synchronous run. var request = new RunManagementFlowRequest { FlowId = "my-management-flow", Options = new ManagementFlowOptions { Input = new ManagementFlowOptions_input { AdditionalData = new Dictionary { { "email", "user@example.com" } } } } }; try { // Returns immediately with an execution ID var asyncResponse = await descopeClient.Mgmt.V1.Flow.Async.Run.PostAsync(request); var executionId = asyncResponse!.ExecutionId; } catch (DescopeException ex) { // Handle the error } ``` ### Get Management Flow Async Result You can also use our [Get Management Flow Async Result API](/api/management/flows/get-management-flow-async-result) to get the result of an asynchronous Management Flow. This operation fetches the result of an asynchronous Management Flow run using the execution ID returned by [Run Management Flow Asynchronously](#run-management-flow-asynchronously). Poll this endpoint until the flow completes. ```python # Args: # execution_id (str): The execution ID returned by run_flow_async. try: resp = descope_client.mgmt.flow.get_flow_async_result( execution_id="execution-id-from-async-run", ) print("Successfully retrieved async management flow result.") print(resp) except AuthException as error: print("Failed to get async management flow result.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() // executionID (str): The execution ID returned by RunManagementFlowAsync. executionID := "execution-id-from-async-run" res, err := descopeClient.Management.Flow().GetManagementFlowAsyncResult(ctx, executionID) if err != nil { fmt.Println("Failed to get async management flow result: ", err) } else { fmt.Println("Successfully retrieved async management flow result.") fmt.Println(res) } ``` ```csharp // Args: // resultRequest (GetManagementFlowAsyncResultRequest): Request containing the execution ID. // ExecutionId (string): The execution ID returned by the async run. var resultRequest = new GetManagementFlowAsyncResultRequest { ExecutionId = "execution-id-from-async-run" }; try { var result = await descopeClient.Mgmt.V1.Flow.Async.Result.PostAsync(resultRequest); } catch (DescopeException ex) { // Handle the error } ``` # Buttons (/flows/screens/buttons) This article will teach you how to configure buttons within Descope flows. # Buttons Button components allow users to interact with your flows through actions like submitting forms, navigating between screens, or initiating authentication processes. This guide covers how to configure and customize button components in your flow screens. ## Generic Button ### "Submit Upon Enter" Enable users to submit forms by pressing the Enter key instead of clicking a button. For example, in a login screen with multiple buttons, you can configure the **Sign In** button to respond to the Enter key while leaving other buttons (like **Sign Up** or **Forgot Password**) as click-only. To configure, simply select the button and toggle **Submit upon enter**. This can only be configured on one button on the screen. ![Configure which button will submit upon enter within Descope flow](/assets/button-submit-on-enter.webp) ### Adding Icons to Buttons You can enhance button components by adding icons. Icons can be displayed alongside text or as standalone elements without any text. To add an icon: 1. Select the button component in your flow screen 2. Upload an icon file 3. Choose whether to display the icon with text or as icon-only ![Button icon upload within flow](/assets/button-icon-upload.webp) ## Sign In Buttons Descope provides pre-configured sign-in buttons for common authentication methods. These include: - Social login options (Google, Facebook, Apple, etc.) - Biometric authentication and passkeys - Single Sign-On (SSO) providers These buttons are automatically styled and configured for their respective authentication methods. ### Last Used Badge You can display a **Last used** badge on a button to indicate the authentication method the user most recently signed in with. This helps returning users quickly identify their preferred login option. To enable or disable the badge, select the button, open the **Behavior** tab, and toggle **Show badge**. ![Last used badge on button](/assets/last-used-badge.webp) To customize the badge appearance, open the [Styles](https://app.descope.com/styles) page in the Descope Console and edit the **Last used badge** component under the **Components** tab. ## Input Validation Generic buttons, as well as most sign-in buttons, can be configured to validate required input fields before proceeding to the next step in the flow. This ensures that users provide valid information before submission. To enable or disable input validation, select the button, open the **Behavior** tab, and toggle **Validate inputs**. ![Input validation on button](/assets/button-input-validation.webp) ## Timer Button You can add a timer button that becomes temporarily disabled after it is clicked. This button is commonly connected to actions like `Resend Magic Link / OTP` to prevent repeated submissions in a short period. When the button is clicked, it enters a countdown state, and becomes active again once the timer expires. You can configure the timeout duration (in seconds) in the button's behavior settings. ![Timer button within Descope flow](/assets/timer-button.webp) ## Interaction IDs Each transition to the next step in a flow is assigned a unique **Interaction ID**. Interaction IDs are typically tied to a button click or a state change (such as a timer expiring), and they identify which action triggered the flow to advance. You can find the Interaction ID for a screen component in the screen editor. This value is editable, and can be changed to something more human-readable. ![Interaction ID within Flow editor](/assets/buttons-login.webp) Interaction IDs are useful when you need to distinguish between multiple exit paths from a single [screen](/flows/screens) — for example, a screen that has both multiple social login buttons. The last Interaction ID can be retrieved dynamically in the flow's context as `{{interactionId}}`, and can be referenced in subsequent flow screens and conditions. ![Example Flow](/assets/buttons-flow.webp) ![Buttons Continue Screen](/assets/buttons-continue.webp) ## Tooltip You can **enable** a tooltip on a button component so that extra text-based context appears on hover. This is useful for abbreviations, short definitions, or adding text-based detail to components without cluttering the screen. To configure a tooltip, start by selecting the text component in the Screen Builder and enabling the tooltip option in the component settings. Then, enter the Tooltip text, keeping it brief—ideally one or two short sentences. Next, choose the **Tooltip position** to control where the tooltip appears relative to the text. You can also optionally set the **Hover delay** and **Hide delay** to adjust how quickly the tooltip appears on hover and how long it takes to hide after the pointer leaves. ![Button with tooltip example](/assets/button-tooltip-example.webp) # Bring Your Own Screen (/flows/screens/byos) Learn how to use Descope flows with your own screens # Bring Your Own Screen When using [Flows](/flows), you can use Descope's **Bring Your Own Screen (BYOS)** functionality if you want to utilize your own custom screens. This allows you to maintain complete control over the UI while still leveraging the logic of Descope flows. Using custom screens (Bring Your Own Screens) means giving up the flexibility of Descope flows — including the ability to update authentication logic and UI without redeploying code. We only recommend BYOS in exceptional cases, such as strict compliance needs or deeply embedded native experiences. ## Overview **Bring Your Own Screen** works by: 1. Using the Descope web component 2. Implementing the `onScreenUpdate` callback to handle screen transitions 3. Rendering your custom components based on the current screen name 4. Managing the required inputs and outputs of each screen ## Critical Concepts When implementing BYOS, the most important aspects to get right are: 1. **Interaction IDs** - Each next step in your flow has a unique Interaction ID, often tied to a button or a state change - These IDs are required to proceed to the next step in the flow 2. **Inputs and Outputs** - Each screen expects specific inputs from the previous screen - Each screen produces specific outputs for the next screen - These must match exactly what the flow expects 3. **Screen Names** - Each screen has a name found and modified from the screen editor - Each screen in your flow must have a unique name To find the Interaction IDs, inputs, and outputs for each screen, you can expand the screen details within the flow builder. ![Screen Detail Expander](/assets/expand-screen-details.webp) The Interaction IDs can be seen on the screen widget, and the expected inputs and outputs of the screen can be seen in the expanded details on the right. To expose a custom attribute for a user, add text on the screen with the [dynamic value](/flows/dynamic-keys) of the custom attribute. ![Screen Detail Expander](/assets/expanded-screen-details.webp) Each step has an automatically generated unique Interaction ID. You can choose to modify it from the screen editor itself. It is very important to rename your Interaction IDs to something unique and easy to reuse. This will allow you to more easily make changes to your flow without having to redeploy your code every time. ![Modify Interaction ID](/assets/modify-interaction-id.webp) The screen name can also be seen and modified from the screen editor. It is extremely important to rename your screen names so that within a flow they are all unique. If your screen names are not unique, you will run into conflicts, and be unable to properly run your flow. ![Modify Screen Name](/assets/edit-screen-name.webp) ## Implementation Walkthrough We'll use a simple example to illustrate how this works. Instead of showing the original "Welcome Screen" from our "sign-up-or-in" flow, we will show a custom "Welcome Screen" component: ```jsx import { AuthProvider, Descope } from "@descope/react-sdk/flows"; import { useState } from 'react'; import CustomWelcomeScreen from './CustomWelcomeScreen'; export default function App() { const [state, setState] = useState({ error: {} }); const [form, setForm] = useState({}); return ( { // Handle screen updates and state management setState(prevState => ({ ...prevState, ...state, next, screenName })); // Return true to use custom screen, false to use Descope's screen return screenName === 'Welcome Screen'; })} onSuccess={() => { console.log('success') setState(prevState => ({ ...prevState })) }} > {state?.screenName === 'Welcome Screen' && { // Call the "next" function with the next step's Interaction ID and required inputs await state.next('', { ...form }) }} errorText={state?.error?.text} />} ); } ``` ### Key Components These are the key components to the example above: 1. **Descope Component**: - `flowId`: Specifies which flow to use - `onScreenUpdate`: Callback that determines when to use custom screens - `onSuccess`: Handles successful authentication 2. **Custom Screen Components**: - Can be any custom component - Receive props to: - Update and use form values - Handle user actions - Display errors - Contain event handlers to define when to proceed to the next step in the flow 3. **State Management**: - `state`: Contains the flow state, including the screen name, errors, and the next function - `form`: Manages form data - `next`: Function to proceed to the next screen Within the Descope component, we check if the current state's "Screen Name" matches the name of a screen we want to replace. If it does, we show our custom screen component instead of the default one. The custom screen component must implement all required inputs and outputs, as shown below. ### Custom Screen Implementation Here's an example of a custom screen component: ```jsx function CustomWelcomeScreen({ onFormUpdate, onClick, errorText }) { return (
onFormUpdate({ email: e.target.value })} /> {errorText &&
{errorText}
}
); } ``` The `form` object maintains state throughout your flow, collecting and passing data between screens and actions. Each screen receives the current form values and can update them using the `onFormUpdate` function. In this example, any change in the email input box on the custom screen updates the form object: ```jsx onFormUpdate({ email: e.target.value })} /> ``` Each screen must update the form with all values listed in the "Outputs" section of that screen in the Descope flow builder. These outputs are required for the next steps in your flow to function correctly. ![Screen outputs in flow builder](/assets/screen_outputs.webp) When the user clicks on the "Continue" button in the custom screen, the `onClick` handler is triggered. ``` jsx ``` The `onClick` handler calls the `state.next()` function, passing in the `interaction-id`, in this case "sign-up-or-in", corresponding to that button in the flow. We also pass in the `form` object, so that the email and any other updated values are available in the next step of the flow. ``` jsx onClick={async () => { if (state.next) { // Call the "next" function with the next step's Interaction ID and required form values await state.next('sign-up-or-in', form) } }} ``` ## Error Handling When implementing custom screens, you can access error information through the `state` object passed to your component. The `state` object contains an `error` property that provides details about any errors that occur during the flow execution. The error object contains the following fields: | Field | Description | Example | |-------|-------------|---------| | `code` | A unique error code that identifies the specific error type | "E011003" | | `text` | A high-level error message that describes the general error category | "Failed to sign up or in" | | `description` | A more detailed explanation of what went wrong | "Request is invalid" | | `message` | The specific reason for the error | "The loginId field is required" | You can handle errors by either showing all errors in your screen, like in the [example screen above](/flows/screens/byos#custom-screen-implementation), or by handling errors with more granularity. For example, if an OTP sent to the user has expired, within your custom screen you can call a function to handle the resend of the OTP: ``` jsx // Resend the OTP when the OTP has expired useEffect(() => { if (state?.error?.code === "E061104") { handleResend(); } }, [state?.error?.code]); // Handle resend click const handleResend = () => { if (canResend) { setIsResending(true) onResendClick() } }; ``` Within the flow component, we handle the onResendClick() event by proceeding to the step where the `resend` Interaction Id points ``` jsx {state?.screenName === verifyScreenName && { if (state.next) { await state.next('resend', form) } }} state={state} /> } ``` For a full list of common errors that are included in the error object, see our [Common Errors](/common-errors) doc. Input validation must be handled within your custom screen component. The error object passed through the state only contains flow-level errors and does not include input validation errors. You'll need to implement your own validation logic for form inputs. ## Screen Data In addition to the `state` object described in [Error Handling](/flows/screens/byos#error-handling), some screens make additional structured data available through `context.data`. This is populated only for screens that produce it — for example, a recovery-codes screen exposes the generated codes, while most other screens leave `context.data` empty. ``` jsx function onScreenUpdate(screenName, context, next, ref) { if (screenName === 'Display recovery codes') { const codes = context.data?.recoveryCodes; // string[] return true; } return false; } ``` ### Available Fields | Field | Description | |-------|-------------| | `recoveryCodes` | Generated MFA recovery codes (`string[]`) | | `totp` | TOTP enrollment data (QR/key) | | `notp` | NOTP challenge data | | `sentTo` | Destination (email/phone) an OTP or magic link was sent to | | `sso` | SSO configuration/redirect data for the current step | | `selfProvisionDomains` | Domains eligible for self-provisioning | | `inboundAppApproveScopes` | Scopes pending approval for an inbound app consent screen | | `outboundApp` | Outbound app connection data | | `passwordPolicy` | Active password policy rules for the tenant | | `securityQuestionsSetup` | Data needed to render security-question setup | | `securityQuestionsVerify` | Data needed to render security-question verification | | `userTenants` | Tenants associated with the user | | `userRoles` | Roles associated with the user | | `ssoApplications` | SSO applications available to the user | | `ssoConfigurations` | SSO configuration details | | `samlAttributeMappings` | SAML attribute mapping configuration | | `oidcAttributeMappings` | OIDC attribute mapping configuration | | `samlGroupMappings` | SAML group mapping configuration | ### Migrating from Top-Level Context Fields `context.totp`, `context.notp`, `context.sentTo`, `context.sso`, `context.selfProvisionDomains`, and `context.inboundAppApproveScopes` have moved under `context.data.*`. Update any custom-screen code that reads these fields at the top level of `context`. This requires a minimum SDK version: | Package | Minimum Version | |---------|-----------------| | `@descope/web-component` | 4.0.0 | | `@descope/react-sdk` | 3.0.0 | | `@descope/vue-sdk` | 3.0.0 | | `@descope/nextjs-sdk` | 0.15.63 | | `@descope/angular-sdk` | 0.27.11 | ## Best Practices - Keep your custom components focused and reusable - Handle errors appropriately using the provided error state - Maintain consistent styling with your application - Use the `next` function to pass all required data to the next screen - Test thoroughly to ensure proper flow progression The most common issues when implementing BYOS are: - Using incorrect interaction IDs - Missing required inputs - Providing inputs in the wrong format - Not handling all required outputs Always verify these in the Descope Console before implementing your custom screens. ## Security Considerations - Never expose sensitive data in the UI - Validate all user inputs ## Sample App For a complete working example of Bring Your Own Screen, check out the [BYOS Sample App](https://github.com/descope-sample-apps/byos-sample-app). # Containers (/flows/screens/containers) This article will teach you how to configure containers within Descope flow screens, including layout, responsive sizing with Auto-Fit Items, and styling. # Containers Containers are layout components that organize and position other elements on your Descope flow screens. Use them to group related components, control how those components are arranged, and apply background, border, and shadow styling. ## Container Types Descope offers two container components: - **Container**: A standard container that lays out its children in a row or a column. - **Collapsible Container**: A container with a title that users can expand and collapse, which is useful for keeping longer forms manageable. ![Collapsible Container](/assets/collapsible-container.webp) Components placed inside a container are its **children**. Select a container in the Screen Builder to configure it. A standard container has a **Design** tab holding a **Layout** and a **Style** section. A collapsible container adds a **Content** section for its title, along with a **Behavior** tab. The outermost container of a screen also has an **Actions** section for attaching a [fraud prevention connector](/connectors/connector-configuration-guides/fraud). ## Layout The **Layout** section controls the size of the container and the arrangement of its children. - **Width**: The width of the container as a percentage of its parent. - **Direction**: Lay children out in a row (horizontal) or a column (vertical). - **Auto-Fit Items**: Available for row containers. Children share the container width and resize together. See [Auto-Fit Items](#auto-fit-items). - **Spacing Type**: Shown for row containers. Choose **Manual** to set the gap yourself, or **Auto** to spread children across the full width of the container. This control is hidden while Auto-Fit Items is on. - **Space Between**: The gap between children. Hidden when Spacing Type is set to Auto, since Descope calculates the gap. - **Alignment**: Where children sit inside the container, as start, center, or end. A row container shows two rows of buttons here, the first for horizontal placement and the second for vertical. - **Horizontal Padding** and **Vertical Padding**: The space between the edges of the container and its children. ![Container Alignment](/assets/container-alignment.webp) When you edit a [widget](/widgets), the widget's outermost container also has a **Height** setting: **Auto** adapts the height to the content, and **Stretch** fills the available height of the host element. This setting does not appear on the flow screens inside a widget. ## Auto-Fit Items By default, each component in a row keeps its own width, so a row of fixed-width components can wrap onto a second line or leave uneven gaps as the screen narrows. Turning on **Auto-Fit Items** makes the children of a row container divide the available width between them and resize together as that width changes. Use it when you want two fields side by side, such as a first name and a last name input. With Auto-Fit Items on, the two inputs split the row evenly on a desktop browser and shrink together on a phone, instead of wrapping or leaving one field cramped. To turn it on, select a container, set its **Direction** to row, and toggle **Auto-Fit Items** in the **Layout** section. ![Three sign-in buttons sharing a row with Auto-Fit Items enabled](/assets/container-auto-fit-items.webp) Auto-Fit Items is off by default, so existing screens keep the layout you already built. ### Interactions with Other Settings - **Fill Container takes precedence.** A child with [Fill Container](/flows/screens#fill-container) enabled still occupies the full width of the container, and Auto-Fit Items does not resize it to fit alongside its siblings. Turn Fill Container off on the components you want to share the row. - **Spacing Type is hidden.** Turning on Auto-Fit Items switches the container to manual spacing and hides the Spacing Type control, because no leftover space remains to distribute. **Space Between** still applies and sets the gap between the children. - **Row containers only.** The toggle appears for row containers, and collapsible containers do not have it. Turn Auto-Fit Items off before you switch a container to column direction, since the toggle disappears while the setting stays on. ## Style The **Style** section controls the appearance of the container itself. - **Fill**: The container background. Choose a fill color based on your project [Styles](https://app.descope.com/styles), a custom fill color, or a background image. The custom color picker includes an opacity value, so set that to 0% for a transparent container. - **Shadow**: The drop shadow around the container, from none through several sizes. - **Border Radius**: The corner rounding. - **Border**: Choose a border based on your project [Styles](https://app.descope.com/styles), a custom border, or no border. The custom option reveals **Border Color** and **Border Width**. ![Container styling options in the Screen Builder](/assets/container-styling.webp) ## Variants Containers other than the root container of the screen have a **Variant** selector above the Layout section. Pick a container variant defined in your project [Styles](https://app.descope.com/styles) to apply that shared look across every container using it. Selecting a variant hides the Style section and most of the Layout section, since the variant supplies those values, and leaves **Width** for you to set. Pick **Custom** to configure this one container by hand. ## Collapsible Containers A collapsible container adds a clickable title row that expands and collapses its contents. The **Content** section of the **Design** tab configures that title row: - **Text**: The title shown on the container. - **Alignment and Direction**: The alignment of the title text, and its text direction for right-to-left languages. - **Icon Position**: Whether the expand and collapse icon sits to the left or the right of the title. - **Attach icon to text**: Keeps the icon next to the title text rather than at the edge of the container. - **Text Fill**: Stretches the title text to fill the width of the title row. The **Style** section starts with two selectors for the title text: **Style** picks a typography level from H1 through Body2, and **Color** picks Primary or Secondary. Below those sit the container **Fill**, **Shadow**, **Border**, and **Border Radius**. Here **Border** is a simple on/off toggle, unlike the three-way control found on a standard container. The **Layout** section has **Width**, **Space Between**, **Horizontal Padding**, and **Vertical Padding**. A collapsible container always stacks its children in a column. The **Behavior** tab holds one setting, **Open by default**, which controls whether the container is expanded the first time a user sees the screen. # Images and Logos (/flows/screens/images-and-logos) This article will show how to use images and logos in a Descope flow. # Images and Logos This guide covers the design configurations for image and logo components in Descope flow screens. These components let you display your brand's logo, a connected app's logo, or any other custom image. Add them by dragging a component from the **Images** category in the left panel into your flow screen. The Images category includes three components: **Image**, **Logo**, and **Inbound App Logo**. ## Image The **Image** component displays a static image that you upload directly to the component. ### Content Selecting an Image component shows a light mode and dark mode preview side by side. Click the pencil icon to open the upload dialog. - The first image you upload is applied to **both** light and dark mode. - If you then upload a second image, it's assigned only to the mode you uploaded it to, letting the image differ between light and dark themes. You can also set the **Alt Text** for the image using the `Alt Text` field, which is used for accessibility and screen readers. ![Image component content settings in the Screen Builder](/assets/image-component-content.webp) ### Style - **Width**: The width of the image, in pixels. - **Height**: The height of the image, in pixels. ### Tooltip You can enable a tooltip on an Image component so that extra text-based context appears on hover. Toggle **Tooltip** on, then enter the tooltip text, position, and optional hover/hide delays. For more on configuring tooltips, see the [Tooltip section](/flows/screens/buttons#tooltip) in the Buttons guide. ## Logo The **Logo** component displays your project's configured brand logo rather than an image you upload per-component. The logo itself is set once for the whole project in the Descope Console under **Styles > Theme > Logo**, where you can upload separate light and dark theme versions (SVG recommended). Every Logo component you add to a screen automatically reflects that project-wide logo. ![Project logo upload in Styles](/assets/styles-logo-upload.webp) Like the Image component, a Logo component exposes: - **Alt Text**: Accessibility text for the logo. - **Style**: **Width** and **Height**, in pixels. - **Tooltip**: Same tooltip options as described above. To change the actual logo image shown by every Logo component across your project, update it in the [Styles section](/management/styles), rather than in the individual component. You can also override the logo via code mode using the `--descope-logo-url` and `--descope-logo-fallback-url` variables. ## Inbound App Logo If you're using a common agent, such as Claude or ChatGPT, the logo should automatically appear when authenticating a user. The **Inbound App Logo** component is used within OAuth consent screens involving [Inbound Apps](/identity-federation/inbound-apps) and [Agentic Client](/agentic-identity-hub/core-components/clients). It displays both logos side by side—the logo of the Agentic Client or Inbound App requesting access and your project's logo—connected by a bidirectional arrow icon that visually represents the link being made between the two. ![Inbound app logo and project logo shown side by side with a connecting arrow](/assets/inbound-app-logo.webp) The Inbound App Logo component exposes a **Style** section with a **Size** slider that lets you adjust the size of both logos simultaneously. The component automatically scales the logos to fit within the available space while maintaining their aspect ratios. # Screens (/flows/screens) Learn how to customize and utilize components within Descope screen. # Screens This document covers the configurations of Descope screens. This guide covers the screen builder as well as the basic components and how you can customize their behavior to fit your desired flow results. The sidebar for this section also contains detailed articles on some of the more complex components. Translations of configured component items, such as labels, placeholders, etc, are configurable within the Localization section of the Descope Console. ## Screen Builder The *Screen Builder* is a widget-based interface that enables you to build user-facing login and sign-up screens from the Descope UI. Whenever you add a Screen Step while creating Flows, you can design the associated screen using the Screen Builder. You can easily create screens with widgets such as: - Inputs that capture user details like email IDs and phone numbers - Buttons for users to select actions such as login methods (SSO, Social Logins, etc.) - Text boxes that can display login errors and success messages - Links that can point to your privacy policy, terms of service, or other destinations - [Images and logos](/flows/screens/images-and-logos) that can display your brand's logo or other custom images - [Containers](/flows/screens/containers) that help you layer and position other components ![Descope screen builder example](/assets/flows-screen-builder.webp) ### Conditional Components You can control component visibility and behavior based on specific conditions. For example, if the device does not support WebAuthn, you can hide the Passkey component. To add a condition in the Screen Builder, click the **Conditions +** button in the top left corner. Then define the condition and specify the component ID. When the condition is met, you can either: - **Hide the component** - The component will not be displayed to the user. - **Disable the input field** - The component remains visible but the input field is disabled and cannot be interacted with. - **Set the input to read-only** - The component stays visible and its value can still be copied, but users can't edit it. Use this instead of Disable to let users copy a value, like a TOTP seed key, without risking accidental changes. ![Descope conditional components](/assets/conditional-components.webp) Components can update live as the user interacts with the screen, not only when the screen first loads. If a condition depends on another field's value, its target component can update as the user types or makes a selection, without requiring a page reload. #### Combining Rules in One Condition A condition can hold more than one rule block. Descope labels the first block `If` and every block you add after it `Or`. Any single matching block fires the `Then` action. No block takes priority over another. Inside a block, Descope joins the rules with `And`, so all of them must hold for it to match. ### Alignment And Direction Buttons, texts, icons, and other inputs support alignment and direction. Alignment can re-organize elements in three screen positions - left, center, and right. Configuring direction is used for input elements to support RTL (Right To Left) languages such as Hebrew, Arabic, Farsi, etc. Clicking on the component will reveal the options available. #### Available Configurations - In Text, Link, and error messages, you can control text alignment and direction. - In all other components, text alignment is defined according to text direction and alignment available in the component configuration. - The button's text is always aligned to the middle, so only the Oauth logo changes direction. - Text in a divider is always aligned to the middle. - Inputs and boolean have only left or right alignment to make it easier for development. ### Bot Trap The **Bot Trap** is a simple bot mitigation technique that adds an invisible field to your screen. Real users never see or interact with this field, but many basic bots will attempt to fill every available input, causing them to be automatically flagged. The Bot Trap only protects against roughly ~60% of bots, particularly less sophisticated ones. More sophisticated bots can typically get around this simple trap. To enable this protection, turn on the **Bot Trap** toggle in the screen configuration. When enabled, Descope will silently validate the hidden field and can block or fail the step if an automated script attempts to fill it. The Bot Trap is lightweight and user-friendly (no extra friction for legitimate users) and can be used in addition to other [bot prevention connectors](/connectors/connector-configuration-guides/fraud) for additional protection. ![Bot Trap](/assets/bot-trap.webp) ## General Component Configurations Some Descope screen component configurations apply to most if not all, components. This section will cover these generic configurations. ### Context Key The context key relates to Descope input and selection components; this is the key that the item is stored in for later use within the flow. ### Alignment and Direction Buttons, texts, icons, and other inputs support alignment and direction. Alignment can re-organize elements in three screen positions - left, center, and right. Configuring direction is used for input elements to support RTL (Right To Left) languages such as Hebrew, Arabic, Farsi, etc. Clicking on the component will reveal the options available. For more details, see the [Customize Flows Documentation](/flows/screens#alignment-and-direction). ### Fill Container The `Fill Container` option will stretch the component to fill the parent container with the element. Fill Container takes precedence over the container's [Auto-Fit Items](/flows/screens/containers#auto-fit-items) setting. If you want several components to share the width of a row, turn Fill Container off on each of them. ### Size You can configure a component's size to one of four sizes; the slider works from left to right, with the left being the smallest. ### Mandatory Many Descope components, like input fields and selection components, can be marked as mandatory. When marked as mandatory, the flow will only allow a user to continue past that screen once the field has been filled or selected. # Text (/flows/screens/text) This article will show how to use the text component in a Descope flow. # Text This guide covers the design and behavior configurations for text components in Descope flow screens. Text components allow you to display content to users, including static text, links, and dynamic values. Add text components by dragging the `Text` component from the left panel into your flow screen. ## Design Text components offer extensive design configurations organized into three categories: Content, Alignment and Direction, and Style. ### Content The content is the text displayed to users as they progress through the flow. You can use simple text like "Welcome, please sign up" or include links and dynamic values for more advanced use cases. You can format text using keyboard shortcuts or the toolbar to apply bold, italic, and other formatting. You can also embed links and organize content into multiple paragraphs. ![Example of a configured Descope text field in a screen](/assets/text-field-example-config.webp) #### Dynamic Values You can enhance your flow screens by using dynamic keys to display user data or previously submitted form data. Our [dynamic values guide](/flows/dynamic-keys) explains available keys. The example below shows how to use dynamic keys with auto-population using double curly braces. ![Example dynamic key usage in flows](/assets/text-field-example-dynamic-key.webp) ### Alignment and Direction Text components support alignment and direction settings. These allow you to position text elements and configure RTL (right-to-left) support for various languages. For detailed configuration options, see the main [Screens guide](/flows/screens#alignment-and-direction). ### Tooltip You can **enable a tooltip** on a text component so that extra context appears on hover. This is useful for abbreviations, short definitions, or adding detail to linked text without cluttering the screen. To configure a tooltip, start by selecting the text component in the Screen Builder and enabling the tooltip option in the component settings. Then, enter the Tooltip text, keeping it brief—ideally one or two short sentences. Next, choose the **Tooltip position** to control where the tooltip appears relative to the text. You can also optionally set the **Hover delay** and **Hide delay** to adjust how quickly the tooltip appears on hover and how long it takes to hide after the pointer leaves. ### Style Like other screen components, you can configure the style of text components. Available options include: - **Style**: Uses the configured [typography](/management/styles#typography) settings for the project - **Color**: Uses the configured color schemes from [styling components](/management/styles#styling-components) - **Fill Container**: Stretches the component to fill the parent container. This takes precedence over the container's [Auto-Fit Items](/flows/screens/containers#auto-fit-items) setting ## Behavior The **Hide when empty** setting controls whether the text component takes up space when it has no content. When enabled, empty text components won't occupy space on the screen, allowing other elements to adjust accordingly. # Authenticated Flows (/flows/use-cases/authenticated-flows) In this article, you will learn how to start authenticated flows in Descope mobile sdks. # Authenticated Flows Descope supports running initial and post-authentication flows on mobile devices. Post-authentication or authenticated flows enable functionality like `step up` authentication and `update user`. This article explains how to implement them. ## Starting an Authenticated Flow Authenticated flows work like unauthenticated or initial authentication flows. The difference is that the user must already be signed in when the flow starts. The SDK picks up the active session automatically - React Native through `AuthProvider`, Swift and Kotlin through `DescopeSessionManager` - so there is nothing extra to pass. ```jsx import { FlowView, useDescope, useHostedFlowUrl, useSession } from '@descope/react-native-sdk' const sdk = useDescope() const { manageSession, updateUser } = useSession() const flowUrl = useHostedFlowUrl('') { await manageSession(jwtResponse) // Pick up any user details the flow changed const meResponse = await sdk.me(jwtResponse.refreshJwt) if (meResponse.data) { await updateUser(meResponse.data) } }} onError={(error) => { // handle flow errors }} /> ``` `FlowView` resolves the active session through `AuthProvider`, so it must be rendered inside one. If a session exists, the flow runs as that user. `FlowView` renders nothing while `isSessionLoading` is `true`. This prevents the flow from starting with an empty refresh JWT on a cold start, but it means the view mounts slightly later than the rest of your screen. Show your own loading indicator until `onReady` fires: ```jsx const [ready, setReady] = useState(false) setReady(true)} onSuccess={handleSuccess} onError={handleError} /> {!ready && } ``` Some authenticated flows finish without signing the user in again, such as a profile update. The response still carries the active session's tokens, so `onSuccess` never receives an empty JWT and `manageSession` is always safe to call. In that case the native layer returns a placeholder user, which the SDK replaces with the user from the active session before onSuccess runs. Changes the flow made to the user are not reflected in it - call `sdk.me` and `updateUser` as shown above to pick them up. ```swift // If DescopeSessionManager holds a valid session, the flow runs as that user let flow = DescopeFlow(url: "") let flowViewController = DescopeFlowViewController() flowViewController.delegate = self flowViewController.start(flow: flow) // The delegate receives the response when the flow completes func flowViewControllerDidFinish(_ controller: DescopeFlowViewController, response: AuthenticationResponse) { let session = DescopeSession(from: response) Descope.sessionManager.manageSession(session) } func flowViewControllerDidFail(_ controller: DescopeFlowViewController, error: DescopeError) { // handle flow errors } func flowViewControllerDidCancel(_ controller: DescopeFlowViewController) { // the user dismissed the flow } ``` ```kotlin descopeFlowView.listener = object : DescopeFlowView.Listener { override fun onSuccess(response: AuthenticationResponse) { Descope.sessionManager.manageSession(DescopeSession(response)) } override fun onError(exception: DescopeException) { // handle flow errors } } // If DescopeSessionManager holds a valid session, the flow runs as that user val descopeFlow = DescopeFlow(Uri.parse("")) descopeFlowView.run(descopeFlow) ``` # Backend Webhooks from Flows (/flows/use-cases/backend-webhooks) Send flow context and user data to your backend in real time using the Generic HTTP connector, with a request body formatted exactly how you need it. # Backend Webhooks from Flows You can call your backend directly from a flow by using a **Generic HTTP connector** as a "webhook" step. The request is triggered at that point in the flow execution and sends whatever payload you define—flow context, user info, form data—formatted exactly how your API expects it. This is different from audit streaming: the call happens immediately when the flow hits that step, not as part of a batched audit export. Use flow webhooks when you need to notify or sync with your backend at a precise moment in the flow, with a payload that matches your system. ## Using a Webhook in Your Flow ### 1. Create a Generic HTTP connector Create a Generic HTTP connector to your backend API that will consume the information sent from the flow. Full configuration options are in the [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) guide. ### 2. Add the Webhook Action to Your Flow 1. In the [Flow Editor](https://app.descope.com/flows), open your flow. 2. Click the **+** button and choose **Connector** → `Generic HTTP / Post` Action. 3. Select the connector you created and set the **path** (e.g. `/webhooks/auth-complete`). 4. In the request **Body**, build the JSON you want your backend to receive. You can use [dynamic values](/flows/dynamic-keys) so each request is filled with current flow and user data. For example: ```json { "event": "user_signed_in", "userId": "{{user.userId}}", "email": "{{user.email}}", "tenantId": "{{user.tenantIds[0]}}", "loginId": "{{form.email}}", "authMethod": "{{lastAuth.authMethod}}" } ``` Your backend receives exactly this structure on each run, so you can map it directly to your DB or internal APIs. ### 3. Use the Action in Your Flow Add the `Generic HTTP / Post` action at the point in the flow where you want the webhook to fire—for example right after a successful sign-in or after collecting consent. The request runs when that step executes. ## Alternate Method: Using the Audit Trail Flow webhooks are for **calling your backend** with a custom payload. They do not, by themselves, create audit events in Descope’s audit trail or in audit streaming. If you want the same moment to appear in your audit log and in any audit-based integrations, add a [`Generate Audit Event`](/flows/actions/generate-audit-event) action in the flow (e.g. next to or right after the webhook step). That action creates a custom audit event with a step name, action, type, and event data you choose. Those events show up on the [Audit page](https://app.descope.com/audits) and are streamed to configured [Audit Connectors](/connectors/connector-configuration-guides/audit-and-troubleshooting). So you can have both: - **Generic HTTP (flow webhook):** real-time, custom request to your backend. - **Generate Audit Event:** same moment recorded in Descope’s audit trail and audit streaming. # Backup Custom Schemes (/flows/use-cases/backup-custom-schemes) In this article, you will learn how to handle Custom Schemes in Android for running flows in Descope mobile sdks. # Backup Custom Schemes On mobile devices, Descope supports running flows for authentication. However, Android [App Links](https://developer.android.com/training/app-links), which Descope uses to handle redirection and token exchange, are blocked by default in certain browsers, like Opera. Developers will need to set up a custom scheme and include it in the flow parameters. ## Setting up a Custom Scheme To set a custom scheme, add the below in your Android Manifest file: ```xml title="AndroidManifest.xml" ``` Then, when starting the flow, include the "backupCustomScheme" field. This will only apply to Android users whos default browser won't open App Links automatically. ```javascript import { useFlow } from '@descope/react-native-sdk' const flow = useFlow() const { session, manageSession } = useSession() try { // When starting a flow for an authenticated user, provide the authentication info let flowAuthentication = undefined if (session) { const flowAuthentication = { /** The flow ID about to be run. */ flowId: 'flow-id', /** The refresh JWT from an active descope session */ refreshJwt: session.refreshJwt, } } const backupCustomScheme = "myapp://auth"; const resp = await flow.start('', '', backupCustomScheme, flowAuthentication) await manageSession(resp.data) } catch (e) { // handle errors } ``` # Block Users by IP Address (/flows/use-cases/block-users-by-ip-address) Learn how to use Descope Lists to block users by IP address, preventing abusive or fraudulent traffic from accessing your authentication flows. # Block Users by IP Address A common use case is blocking abusive or fraudulent traffic by preventing users from logging in or signing up if their IP address appears on a ban list. [Lists](/management/project-settings#lists) provide a native way to manage reusable sets of values, such as IP addresses, CIDR ranges, email domains, or countries and reference them directly in flow conditions. Follow the steps below to implement this use case: ## 1. Create a List of Banned IP Addresses Create a centralized list of banned IP addresses to manage all blocked IPs in one place in the [Project Settings page](https://app.descope.com/settings/project/lists), making it easy to add or remove entries without modifying your authentication flows. - **List name**: Enter the list name (e.g., `IP_ban_list`) - **List type**: `IPs/CIDR` - **Description**: Enter the list description - **Items**: Enter the IP addresses or CIDR ranges to ban ![Create IP ban list](/assets/create-list.webp) ## 2. Add a Condition to Your Flow Adding the condition at the start of your flow ensures that IP blocking happens before any authentication steps, preventing unnecessary processing, OTP costs, or account creation attempts from banned IPs. Add a **Condition** at the start of the flow, and configure the condition as follows: - **Key**: `lists.` - **Operator**: `Lists Contains` - **Value**: `{{ipAddress}}` ![Add condition to flow](/assets/banned-ip-condition.webp) ## 3. Handle the Banned User If the IP address is banned, configure the condition's routing to determine how the banned user should be handled. You can choose to: - **Display a Screen**: Redirect users to a **Screen** that displays a banned message, providing clear feedback about why access is denied. - **Redirect to External URL**: Redirect banned users to an external page or an error page on your application. - **End the Flow**: Terminate the authentication flow immediately without allowing further progress. Since this is part of a flow, you have full flexibility in how you want to handle banned users. In this example, we display a screen that shows a banned message. However, you can configure any action that fits your use case. ![Add screen to flow](/assets/banned-ip-list.webp) Providing clear feedback to users with banned IPs improves the user experience and helps legitimate users understand why they're being blocked. ![Add screen to display banned message](/assets/banned-screen.webp) # Checking for Email Scanners (/flows/use-cases/email-scanner) Condition to check if the email is being scanned and prevent magic link invalidation # Checking for Email Scanners When using magic links, email services like Outlook's "Safe Links" may click links to check for malicious content, potentially invalidating the one-time authentication. The `isEmailScanner` condition allows detecting such scanners and adding a screen to prevent token invalidation. ## Templates with Email Scanner Detection The following flow templates include built-in email scanner detection: - [Enchanted Link with email scanner protection](https://app.descope.com/flows?template=enterprise-b-2-b-enchanted-link) - [Magic link with email scanner protection](https://app.descope.com/flows?template=enterprise-b-2-b-magic-link) ## Using Built-in Flow Actions The simplest way to add email scanner protection is to use Descope's built-in flow actions that include automatic email scanner detection. These actions handle the detection and protection logic for you, so you don't need to manually configure conditions or screens. ### Available Actions The following actions include built-in email scanner protection: - **Sign In / Magic Link / Email with Email Scanner Protection** - A magic link flow that handles cases where email scanners might click the link before the real user. - **Sign Up / Magic Link / Email with Email Scanner Protection** - A magic link flow that handles cases where email scanners might click the link before the real user. - **Sign Up or In / Magic Link / Email with Email Scanner Protection** - A magic link flow that handles cases where email scanners might click the link before the real user. - **Update user / Magic Link / Email with Email Scanner Protection** - A magic link flow that handles cases where email scanners might click the link before the real user. ### Example: Using a Built-in Action To use a built-in action with email scanner protection: 1. Add one of the email scanner protection actions to your flow (e.g., **Sign In / Magic Link / Email with Email Scanner Protection**) 2. Configure the action with your desired settings (email field, redirect URL, etc.) 3. The action automatically handles email scanner detection and protection - no additional configuration needed The built-in actions automatically: - Detect when a request comes from an email scanner - Show an intermediate screen with a button for scanners (which they typically won't interact with) - Allow genuine users to proceed directly to token verification - Handle token verification only after confirming the request is from a real user ![Actions with email scanner detection](/assets/email-scanner-example-flow.webp) ## Using the isEmailScanner Condition If you’ve already built a flow using a magic link, you don’t need to start from scratch. You can simply add an `isEmailScanner` condition and include a `Verify Token` action in your existing flow. This lets you keep your current setup while adding the necessary logic to properly handle email scanner detection. # Email Verification Outside of Sign Up/In (/flows/use-cases/email-verification) Learn how to utilize embedded links to verify a user's email outside of the standard sign up/in flow without magic link. # Email Verification Outside of Sign Up/In This article will teach you how to utilize embedded links to verify a user's email outside of the standard sign up/in flow. This use case is applicable when a user signs up with email and password but OTP is not added to the sign up flow to verify the email belongs to the user. This guide requires access to a messaging connector, like SendGrid, that allows for custom Send Email actions in the flow. Read more on our [Messaging Connectors Doc](/connectors/connector-configuration-guides/messaging). ## Create the SendGrid Connector SendGrid will be used as the connector on this guide but any custom email connector will work. [This guide](/connectors/connector-configuration-guides/messaging/sendgrid) is more in depth on how to use the SendGrid connector. From the [Connectors](https://app.descope.com/connectors) page in the Descope Console, select SendGrid and configure it like the image below: ![Descope example sendgrid connector configuration](/assets/sendgrid-connector-creation.webp) You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. Save your configuration by hitting `Create.` ## Create the Applicable Flow Once you've configured the SendGrid connector, you must make the applicable flow. The below is an example of a completed flow. The next sections will cover how to add the related embedded link actions and the SendGrid connector to the flow. ![An example of a Descope Flow sending an embedded link and verifying an email](/assets/embedded-link-flow.webp) This flow begins by authenticating the user with either an email or username and a password. This does not need to be done in this flow if the user is already authenticated in your project when the flow runs. If you use username for login instead of email, prompt the user for their email after authentication. ### Sending the Verification Email The next screen asks the user to verify their email by entering it and continuing. This hits a condition that checks whether their email is already verified or not. If it is, the flow ends, if it is not, the flow uses the `Update User / Embedded Link / Verify Email` action to generate a token for email verification. Make sure to check the `Mark provided email as verified` box. ![Configure embedded link update user via email action within Descope flows](/assets/configure-embedded-link-update.webp) This is then passed into the send action for the SendGrid connector and included in the email using the `{{embeddedTokenURI}}` context key: ![An example of a SendGrid connector being used to send an embedded link](/assets/embedded-link-send-email.webp) SendGrid then sends the following email to the user: ![An example of the email the connector would send to the user](/assets/embedded-link-email-example.webp) Once the link in the email is clicked, the email is verified and the flow ends. ![User page in the console after verification](/assets/user-email-verified.webp) For more information on how to use embedded links with Descope, visit the [Embedded Links](/auth-methods/embedded-link) documentation section. # Embedded OTP with Generic HTTP Connectors (/flows/use-cases/embedded-otp) Learn how to utilize embedded OTP codes for authenticating users when sending customized notifications with your messaging connectors within Descope. # Embedded OTP with Generic HTTP Connectors This article will teach you how to utilize embedded OTP codes for authenticating users when sending customized notifications with your messaging connectors within Descope. This use case is applicable when you want to utilize a messaging tool which Descope does not yet provide an out-of-the-box connector for. For instance, if you are in a region which our connectors may be slow due to networking or inspection during the travel, you could configure a generic HTTP connector to use for sending your OTP messages during the flow execution. ## Create the Messaging Connector [The network connectors guide](/connectors/connector-configuration-guides/network) covers the generic details about connectors and the use of Descope's generic HTTP connectors. You will need to ensure that you have the details necessary for the messaging tool you'll be using. Essentially, you will need the API routes as well as the necessary credentials. This is a generic example of an HTTP connector which is connected to an email messaging tool. ![An example of a generic messaging connector in Descope](/assets/generic-http-connector.webp) ## Create the Applicable Flow Once you've configured the generic HTTP connector, you must make the applicable flow. The below is an example of a completed flow. The next sections will cover how to add the related embedded OTP actions and the HTTP post action to the flow. ![An example of a Descope Flow sending an embedded OTP code and verifying it](/assets/embedded-otp-flow.webp) ### OTP Actions The flow will require the `Generate OTP Embedded Code` action; Descope supports `sign-up`, `sign-in`, or `sign-up-or-in` actions for generating this code. To add them click the blue `+` icon at the top left of the flow builder, and search for `embedded` then add the desired action based. You will then also need to add the `Verify Embedded OTP Code` action. Below is an example of the actions displayed when searching the actions. ![An example of embedded OTP actions within the Descope flow builder](/assets/embedded-otp-actions.webp) ### Verify OTP Screen You will also need to add a screen which captures the OTP code from the user. This screen would go between the `Generate OTP Embedded Code` and `Verify Embedded OTP Code` actions. After successful verification, the user's email or phone will be verified within the user's details. Below is an example of that screen. ![An example of capturing an OTP code within a Descope flow screen](/assets/otp-screen.webp) ### Generic HTTP / Post Action You will then click the blue `+` sign at the top left of the flow builder screen and click `Connector` and then select the `Generic HTTP / Post Action` to add to your flow. When configuring the payload, you will add the `embeddedCode` wrapped in curly braces `{{}}`. The below example is for sending an email with the `embeddedCode` and is sent to the email given during the `Login Screen`. ```json { "personalizations": [ { "to": [ { "email": "{{form.email}}" } ] } ], "from": { "email": "email@company.com" }, "subject": "Your Code for login is {{embeddedCode}}", "content": [ { "type": "text/plain", "value": "Your Code for login is {{embeddedCode}}" } ] } ``` You'll also need to configure which connector you wish to use as well as the endpoint for completing the HTTP post call. The below is an example of a configured `Generic HTTP / Post Action` for this flow. Once added, your flow actions should be in a similar order to [the above example](#create-the-applicable-flow). ![An example of configuring a generic HTTP post action to use the embeddedCode within a Descope flow screen](/assets/generic-http-post-action.webp) # Testing Authenticated Flows with JWT Input (/flows/use-cases/flow-runner-jwt-input) Learn how to provide a refresh JWT to the Descope flow runner to test post-auth flows, step-up authentication, impersonation, and MFA enrollment. # Testing Authenticated Flows with JWT Input Some flows are designed to run in the context of an already-authenticated user — for example, a step-up MFA challenge, a profile update flow, an impersonation flow, or any other post-authentication journey. When you click **Run** in the [Descope Console](https://app.descope.com/flows) to test these flows, the runner starts a fresh, unauthenticated session by default. This means `user.loggedIn` will be `false` and any flow logic that depends on an existing session (user context, `jwtClaims`, refresh token presence) will follow the unauthenticated path. To simulate an authenticated user in the flow runner, you can provide a **refresh JWT** as input before running the flow. The runner will use it to establish a user session, populating `user.loggedIn`, `user.*` dynamic values, and `jwtClaims` — exactly the same way the [mobile SDKs supply the active session when running authenticated flows](/flows/use-cases/authenticated-flows). Providing a JWT sets the **session context** for the flow run — it makes `user.loggedIn` true and populates `user.*` values from the token. It does **not** automatically skip authentication steps within the flow. Whether those steps are bypassed depends on how the flow is designed: a flow that checks `user.loggedIn` and branches around its email/OTP step will skip it; a flow that always presents that step regardless will still present it. The JWT is most useful when testing flows that are explicitly built to detect and act on an existing session. ## How to Provide a JWT to the Flow Runner 1. Open the flow you want to test in the [Descope Console](https://app.descope.com/flows). 2. Click the **Run** button at the top right of the flow editor. 3. In the runner panel, click the **Configure** button. 4. Add a new **Input** and set `jwt` as the key. 5. Paste a valid refresh JWT as the value for the user you want to test as. 6. Click **Run** to start the flow — the session will be initialized with that user's context. You can obtain a refresh JWT for a test user by completing a sign-in flow and copying the `refreshJwt` from the browser's session storage or by calling the appropriate Descope SDK method in your application. ## Common Use Cases **Post-authentication flows** Flows that update user profile data, add authentication methods, or branch based on [`user.loggedIn`](/flows/conditions/isloggedin) require an active session to take the correct path. Providing a JWT makes `user.loggedIn` true so the flow follows the authenticated branch — without it, the runner would always follow the unauthenticated path regardless of how the flow is designed. **Step-up authentication** Step-up flows prompt an already-authenticated user to re-verify their identity before accessing sensitive resources (e.g., re-entering a password or completing an MFA challenge). The JWT doesn't skip the step-up challenge itself — the user still needs to complete it — but it ensures the flow reaches the step-up screen rather than falling back to a full login screen. Without a JWT, the flow would treat the runner as an unauthenticated session and route to the login path instead. **Impersonation** Impersonation flows allow an admin to act on behalf of another user. Because these flows check the identity and roles of the initiating user via [`user.project.roles`](/flows/dynamic-keys#user) or similar dynamic values, providing a JWT for the admin account lets you confirm that role checks and impersonation actions behave correctly. **Multi-factor enrollment** Flows that enroll a user in TOTP, passkeys, or other additional factors typically check whether the user already has a factor set (e.g., `user.totp`, `user.webauthn`) before presenting the enrollment screen. Supplying a JWT for an existing user lets you test both the already-enrolled and not-yet-enrolled branches. ## Related Resources - [Authenticated Flows (mobile)](/flows/use-cases/authenticated-flows) — how mobile SDKs run flows in the context of the signed-in user - [Troubleshooting Flows](/handling-flow-errors/troubleshooting-flows#flow-runner) — full flow runner documentation - [`user.loggedIn` condition](/flows/conditions/isloggedin) — branching based on authentication status - [Dynamic Values](/flows/dynamic-keys) — full reference for `user`, `jwtClaims`, and other context keys available in flows # Device Fingerprinting (/flows/use-cases/implementing-fingerprinting) This guide shows Descopers how to configure and use device fingerprinting functionality in their Descope Flows. # Device Fingerprinting in Flows Looking for a high-level overview of what fingerprinting is and how it helps? See the [Fingerprinting Overview](/fingerprinting) page for key features, use cases, and connector capabilities. Descope provides built-in fingerprinting and risk-based features that you can use to build secure, adaptive authentication flows. This guide shows you how to configure and use these built-in capabilities inside your Descope Flows. For a general overview of fingerprinting capabilities and what default features are available, visit the [Fingerprinting overview](/fingerprinting) page. ### Using the Fingerprint Assess Action Certain risk detection features (`riskInfo.botDetected` and `riskInfo.riskScore`) require the **Fingerprint Assess** action to be used in your Descope flows. To use these features: 1. Add a **Screen** where the user interacts (e.g., login, signup, or MFA screen). 2. Insert a **Fingerprint Assess** action **immediately after** the Screen. 3. Use the collected fingerprinting and risk data in conditional logic to build your authentication flow (e.g., challenge users with high risk). ### Which Features Require the Fingerprint Assess Action? | Risk Signal | Requires Fingerprint Assess? | |-------------|------------------------------| | `riskInfo.botDetected` | ✅ Yes | | `riskInfo.riskScore` | ✅ Yes | | `riskInfo.impossibleTravel` | ❌ No | | `riskInfo.trustedDevice` | ❌ No | ## Risk Signals and Implementation ### Risk Score (`riskInfo.riskScore`) The risk score provides a unified measure of authentication risk (0-1) based on multiple signals. - **Detection Sources:** - Network-level analysis via Cloudflare - Enhanced by reCAPTCHA, Turnstile, Telesign, or other connectors when configured - **Scoring Logic:** Takes the maximum risk level from all sources for a conservative final score - **Behavior:** - With Fingerprint Assess: Both Cloudflare and connector signals are evaluated - Without Fingerprint Assess: Only connector signals contribute to the score **Direction:** Higher values mean **more** authentication risk (stronger signals that something may be off). This unified score is **not** the same as every vendor's native scale—for example, raw reCAPTCHA v3 uses a different convention (where a high score often means “likely human”). For strict bot yes/no logic, also consider `riskInfo.botDetected` and connector-specific outputs. #### Interpreting `riskInfo.riskScore` The table below is a practical way to read the unified score when tuning conditions. Exact boundaries depend on your traffic and tolerance for friction. | Approximate range | Typical meaning | | ----------------- | --------------- | | ~0.1-0.3 | Lower risk; often typical traffic or not strongly flagged | | ~0.5+ | Elevated risk; a common starting point for extra verification (for example, step-up authentication) | | ~0.7+ | Stronger risk; stricter step-up, MFA, or deny paths often use thresholds in this range | Treat these ranges as **guidelines**, not fixed rules. Adjust thresholds based on false positives, fraud losses, and user experience. The condition examples on this page (`> 0.5`, `> 0.7`) are illustrative patterns for adaptive flows. #### Implementation Use risk scoring to adapt your flow based on the perceived risk level of each login: 1. Add a **Screen** (e.g., login, signup) 2. Add the **Fingerprint / Assess** action immediately after the Screen 3. Create a **Conditional Step** checking if `riskInfo.riskScore` exceeds your risk threshold Example: Trigger step-up authentication if `riskInfo.riskScore > 0.5` ![Descope risk score condition](/assets/descope-risk-score-condition.webp) ### Bot Detection (`riskInfo.botDetected`) Detects bot-like behavior during authentication attempts. - **Detection Source:** Network-level analysis via Cloudflare - **Limitations:** Purely network-based, no browser or device fingerprinting - **Requirement:** Requires the Fingerprint Assess action after a screen #### Implementation 1. Insert the **Fingerprint / Assess** action after the Screen 2. Create a **Conditional Step** that checks if `riskInfo.botDetected == true` Example: Block login attempts or apply stricter authentication if a bot is detected ### Impossible Travel (`riskInfo.impossibleTravel`) Flags logins from geographically implausible locations. - **Detection Source:** Geolocation and timestamp analysis - **Requirement:** No special requirements or Fingerprint Assess needed #### Implementation 1. Create a **Conditional Step** that checks if `riskInfo.impossibleTravel == true` Example: Require re-authentication if impossible travel is detected ![Descope bot detected condition](/assets/descope-bot-detected-condition.webp) ### Trusted Device (`riskInfo.trustedDevice`) Recognizes previously verified devices to reduce authentication friction. - **Detection Source:** First-party cookie on your custom domain - **Requirements:** - Pro tier or higher - Configured [custom domain](/how-to-deploy-to-production/custom-domain) - No Fingerprint Assess needed #### Implementation 1. Configure your [custom domain](/how-to-deploy-to-production/custom-domain) 2. Choose one of these implementation options: - Add a **Trust This Device** button on a user-facing screen ![Descope trust this device button](/assets/descope-trust-this-device-button.webp) - Use the **Mark Device As Trusted** action step in your flow logic ![Descope trust this device action](/assets/descope-trust-this-device-action.webp) 3. Use `riskInfo.trustedDevice` in conditional logic to adjust authentication ![Descope trust this device condition](/assets/descope-trust-this-device-condition.webp) ## Connectors Once you have these built-in features set up, you can further **enhance your flows** by **combining them with Connector signals** from services like: - **[reCAPTCHA Enterprise](/connectors/connector-configuration-guides/fraud/recaptcha-enterprise)** (bot protection) - **Turnstile** (alternative CAPTCHA) - **[Telesign](/connectors/connector-configuration-guides/fraud/telesign)** (phone number and risk intelligence) - **[Fingerprint](/connectors/connector-configuration-guides/fraud/fingerprint)** (advanced device and browser fingerprinting) - **[Forter](/connectors/connector-configuration-guides/fraud/forter)** and **Sardine** (fraud and behavioral risk scoring) For a complete list of all of the available fraud/risk connectors, check out the [Fraud Connectors Doc](/connectors/connector-configuration-guides/fraud) page. ### Using Connector Risk vs. Unified Risk Score Third-party services like FingerprintJS, Turnstile, and Forter provide detailed risk signals optimized for their detection methods. Aggregating everything into a single risk score may oversimplify your risk analysis. By default, Descope provides a unified `riskScore` — but if you're using advanced connectors like Forter or Sardine, consider evaluating their individual responses directly for more control. | Approach | When to Use | |:---------|:------------| | **Use `riskInfo.riskScore`** | When you want simple risk evaluation for a basic flow (e.g., if `riskScore > 0.7`, trigger step-up authentication). | | **Use Connector-Specific Outputs** | When using advanced services like Fingerprint, Forter, or Sardine, where granular risk signals should be evaluated independently for better accuracy and control. | For more information about all available fingerprinting capabilities and connector options, visit the [Fingerprinting Overview](/fingerprinting) documentation page. # Manage User Consent and Preferences (/flows/use-cases/manage-user-preference) Learn how to manage consent and user preferences in your flow. # Manage User Preference Many apps need to capture user consent during sign-up — acknowledging a privacy statement, agreeing to terms of service, or opting in to marketing emails. Descope handles this using [flows](/flows) and [custom attributes](/management/user-management#custom-user-attributes). You add it to a flow screen to capture consent at sign-up, and optionally add it to the [User Profile Widget](/widgets/users#user-profile-widget) so users can change their mind for opting out of marketing emails afterward. This guide walks through a single example — privacy statement, terms of service, and marketing email consent — but the same pattern applies to any preference you want to track and let users manage. ## How to Manage User Preference ### Step 1: Create a Custom Attribute for Each Preference In the [Users](https://app.descope.com/users) page of the Descope Console, open the **Custom Attributes** tab and click **+ Create Attribute**. Create one boolean attribute per preference — for this example: - Read Privacy Statement - Terms of Service Consent - Marketing Consent ![Custom attributes in the Descope Console](/assets/custom-attributes-consent.webp) See [Custom User Attributes](/management/user-management#custom-user-attributes) for the full list of supported attribute types. ### Step 2: Add these Custom Attributes to Your Flow Open the [Flows](https://app.descope.com/flows) page, select the flow, and open the screen where you want to collect consent. Under the **Custom User Attributes** dropdown on the left, find each custom attribute you just created, and drag it onto the screen — this adds it as a checkbox bound to that attribute. ![Adding custom attributes to a flow screen](/assets/custom-attributes-flow-screen.webp) For each attribute, you can configure the following: - **Content**: Edit the content and link to the privacy statement or terms of service pages. - **Mandatory**: You can mark the attribute as mandatory, which will require the user to check the checkbox before they can continue. - **Selected by Default**: You can set the default state of the checkbox to checked or unchecked. ### Step 3: Let Users Manage Their Preference Later Consent for a privacy statement or terms of service is typically a one-time acknowledgment, but preferences like marketing emails should stay editable. Open the [Widgets](https://app.descope.com/widgets) page, edit your [User Profile Widget](/widgets/users#user-profile-widget), and drag the `Marketing Consent` attribute onto it the same way you did on the flow screen. ![Adding a custom attribute to the User Profile Widget](/assets/consent-user-profile.webp) ### Step 4: Add an Update User / Attributes Action Dragging a custom attribute onto the screen only captures each checkbox's value in the flow's context — it doesn't save it to the user yet. Add an `Update User / Attributes` action to the flow (via the blue `+` icon) after the screen with your consent checkboxes, then map each captured value to its corresponding custom user attribute. ![Configuring the Update User / Attributes action to persist consent attributes](/assets/consent-update-user-attributes.webp) # Enabling OAuth Sign-In for Pre-Created Users (/flows/use-cases/oauth-signin-pre-created-users) How to let pre-created users sign in with OAuth when sign-ups are disabled by verifying their email and linking the OAuth identity to their existing account. # Enabling OAuth Sign-In for Pre-Created Users Some Descope projects disable self-service sign-ups and instead pre-create users manually, for example, through the [Management API](/management/user-management) or a bulk import. This is common when a company wants full control over who gets an account, rather than letting anyone create one at sign-up time. The complication comes when you also want these pre-created users to sign in with OAuth (social login) rather than a password or OTP. The first time a pre-created user authenticates with an OAuth provider, Descope has no existing login ID that matches the identifier returned by that provider, since the user has never logged in with it before. Because sign-ups are disabled, Descope cannot fall back to creating a new user, and the `Sign In / OAuth` action resolves to its `User does not exist` outcome instead of `Successful authentication`. This guide covers how to handle that outcome in a flow: verify the email address returned by the OAuth provider with an OTP code, then use that verified email to associate the OAuth identity with the user's existing, pre-created account, instead of blocking them or creating a duplicate. The flow described below is available as a ready-to-use [template in the Flow Library](https://app.descope.com/flows?template=sign-in-allow-social-when-signups-not-allowed), so you can add it to your project and adjust it to fit your needs rather than building it from scratch. The email claim returned by an OAuth identity provider isn't always trustworthy for account linking (some providers allow the email claim to be changed by the account holder). Verifying it with an OTP before attaching the OAuth login ID to an existing user prevents account takeover. Read more in [Securely Merging OAuth Identities](https://www.descope.com/blog/post/descope-flows-securely-merging-oauth-identities). ## How the Flow Works ![Sign in - Allow social login when signups are not allowed flow template](/assets/flow-signin-with-oauth-no-signup.webp) 1. **Sign In Screen** - Presents the user with a social login button (`Socials`). 2. **Sign In / OAuth** - Runs the OAuth exchange with the provider. - `Successful authentication`: the returned identifier already matches an existing login ID (the user has signed in with this provider before), so the flow can go straight to `END`. - `User does not exist`: this is the first time this OAuth identity has been seen. Continue to the next step instead of ending the flow. 3. **Create login ID and get user's email** - Extracts the email address from the OAuth provider's claims and prepares it as the login ID to verify, without creating a new user (sign-ups remain disabled). 4. **Sign In / OTP / Email** - Sends a one-time passcode to that email address. 5. **Verify OTP** - Screen that captures the code from the user, with a `Send again` option, and routes to `Verify OTP code`. 6. **Verify Code / OTP / Email** - Confirms the OTP is correct. On `Successful authentication`, the flow now knows the person behind the OAuth login also owns the email address on the pre-created account. 7. **Update User / Attributes** - Attaches the OAuth login ID to the existing user record that matches the verified email, rather than creating a new one. This is the step that actually links the OAuth identity to the pre-created account. Enable **Add to login IDs** on this action so the OAuth identifier is saved against the existing user going forward. See [Link User Identities Across Different Auth Methods](/flows/actions/multiple-login-id) for more detail on this option and the merge behavior it controls. 8. **End** - On success, the user is now signed in, and future sign-ins with this same OAuth provider will resolve directly through the `Successful authentication` path in step 2, since the login ID has been saved. ## Key Points - Sign-ups stay disabled throughout: no new user is ever created by this flow. The OTP/email step only confirms the OAuth email belongs to an account that already exists. - If no existing user matches the verified email, decide how you want the flow to behave (for example, show an error screen and end the flow) rather than falling through to `Update User / Attributes`, which expects a matching user to update. - This pattern works for any OAuth provider configured in [Social Login (OAuth)](/auth-methods/oauth), since the email claim from the provider is what's used to find the matching pre-created user. - For providers that return unverified emails (which changes how `Sign In / OAuth` behaves), see [Handling OAuth Providers with Unverified Emails](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails). # Adding a Recovery Email (/flows/use-cases/recovery-email) Learn how to let users add, verify and use a recovery email in your flows. # Adding a Recovery Email A recovery email gives users a fallback way to access their account when their primary login method is unavailable. This guide explains the steps needed to collect, verify, and store a recovery email on a user's account. This guide uses a **recovery email** verified via magic link, but the same pattern applies to a recovery **phone number** — simply use the magic link over SMS instead of email. ## Add Recovery Email Start the flow with your normal authentication method — for example, **Sign Up or In / Magic Link / Email** or **Sign Up or In / OTP / Email**. This ensures the user is authenticated before they are prompted to add a recovery email. ![Add Recovery Email flow overview](/assets/recovery-flow.webp) Add a Flow Screen with an input field for the recovery email address and a skip button. If the user skips, the flow ends and they continue normally. If they enter a recovery email, the next step verifies it. For verifying the recovery email, add a **Verify Recovery Email / Magic Link** action. In the action, select your **[Recovery Verification Flow](/flows/use-cases/recovery-email#recovery-verification-flow)** under the **Verification Flow** setting. This is a separate flow that will be used to verify the recovery email. Once this action runs, a magic link is sent to the recovery email address the user entered, and a confirmation screen is shown telling them to check their inbox. ![Verify Recovery Email Magic Link action configuration](/assets/recovery-email-action.webp) ## Recovery Verification Flow This is a separate flow that must be created first, then selected inside the **Verify Recovery Email / Magic Link** action in your main flow. This flow will have the **Finish Verify Recovery / Magic Link** action to finish the verification process, and then have a screen to confirm the recovery email has been successfully verified. The **Finish Verify Recovery / Magic Link** action validates the token from the magic link and marks the recovery email as verified on the user's account. After that, a success screen is shown to the user confirming that their recovery email is now set up and ready to use. ![Recovery Verification Flow overview](/assets/recovery-verify-flow.webp) ## Recovery Email Usage Once verified, the recovery email can be used for sign-in. In the **Sign In / Magic Link / Email** action, enable the **Send to recovery email** checkbox to route the magic link to the recovery email instead of the primary one. ![Recovery Email Usage](/assets/recovery-email-usage.webp) # Remember User Flow (/flows/use-cases/remember-me-flow) Learn how to implement the "Remember User" functionality in your Descope flows to streamline sign-in experiences. # Implementing a "Remember User" Flow The "Remember User" functionality enhances the user sign-in experience by remembering details about the user from their previous session. This reduces friction by being able to suggest a login ID to use or the authentication method the user last used, eliminating the need for them to recall either of these. ## How "Remember User" Works By default, Descope stores information about the user's most recent sign-in (such as `loginId`, `authMethod`, and `name`) in the browser's `localStorage`. This data is, by default, automatically cleared upon user logout. To adjust this behavior, Descope's SDK provides two optional parameters as part of our client SDKs: - `storeLastAuthenticatedUser`: Set to `false` to disable storing the user's last authentication details. - `keepLastAuthenticatedUserAfterLogout`: Set to `true` to persist user details after logout. Example: ```jsx {/* Your app components here */} ``` The above parameters are part of a broader set of optional client-side settings. Visit the [Auth Helpers](/client-sdk/auth-helpers) guide to explore additional options like token storage. Refer to the [Descope SDK Documentation](/client-sdk) for further implementation details. ## Implementing "Remember User" in a Flow Once you've configured the SDK in the way you prefer, you can implement the "Remember User" functionality within your authentication flow. Here is an example of how to structure your flow with the `lastAuth` context keys: To learn more about the `lastAuth` key and other related subkeys, refer to the [Dynamic Values doc](/flows/dynamic-keys#lastauth). ### 1. **Check if the User is Remembered** Begin your flow by adding a condition to check if the `lastAuth.loginId` value is present: ![Conditional check based on existing user](/assets/remember-me-guide-conditional.webp) ### 2. **Display a Welcome Back Screen** If the user is remembered (`lastAuth.loginId` is not empty), add a welcome-back screen displaying the user's remembered details, along with a button to continue signing in: ![Configuring welcome screen](/assets/remember-me-guide-welcome-screen.webp) #### Utilizing the User's Last Authentication Name The `lastAuth.name` key can be used to provide a personalized welcome message when users revisit your log-in page. This key follows a fallback mechanism in case certain user details are unavailable, prioritizing fields in this order: 1. Display Name 2. Login ID 3. Email 4. Phone ### 3. **Identify the Last Authentication Method** For the "Sign In" button, create conditions based on `lastAuth.authMethod`. This allows you to automatically route the user to the authentication method they previously used: ![Conditional based on authentication methods](/assets/remember-me-guide-conditional-auth-method.webp) ### 4. **Connect Conditions to Authentication Methods** Finally, connect each condition to the corresponding authentication methods within the Flow Editor. The structure should resemble: ![Conditional location within flow](/assets/remember-me-guide-conditional-location.webp) # Step-Up Authentication for Third-Party IdP Sign-Ins (/flows/use-cases/step-up-with-generate-jwt) Issue a stepped-up JWT when signing a user in through a homegrown or third-party IdP, so a single sign-in can satisfy both authentication and step-up. # Step-Up Authentication for Third-Party IdP Sign-Ins If your app authenticates some users against a homegrown or third-party identity system rather than natively through Descope, you can still use those sign-ins to satisfy a step-up requirement. This matters when you're migrating users off a legacy auth system: you can sign a still-validated user into Descope for the first time and have that same sign-in count as step-up, so your app doesn't challenge them again right after. ## The Problem This Solves You can authenticate a user against your legacy system (for example, by validating an existing session cookie with a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http)), then use a [Generate JWT action](/flows/actions/generate-jwt) to issue a matching Descope session for them without asking them to sign in again. If that migration happens on a page that also requires step-up, like a sensitive account action, you want the resulting Descope JWT to carry step-up status from the start. Otherwise, your app's step-up check runs right after, finds no `su` claim on the token, and prompts the user to re-authenticate. You end up asking the same person to verify their identity twice in a row for one action. ## How the Flow Works 1. **Step Up action** - Add the [Step Up action](/mfa-and-step-up/step-up#option-1-using-descope-flows) near the start of the flow. This marks the flow as a step-up authentication, so any sign-in that completes later in the same flow issues a JWT with the `su` claim set to `true`. 2. **Validate the user against your legacy system** - Use a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) (or similar) to check the user's existing session or credentials against your old auth system, and set `form.email` or `form.externalId` from the result so Descope knows which login ID to match. 3. **Generate JWT action** - Add a [Generate JWT / Sign In, Sign Up, or Sign Up or In with 3rd Party IdP action](/flows/actions/generate-jwt), depending on whether the user is expected to already exist in Descope. Because this action runs after the Step Up action, the JWT it issues includes `su: true`. 4. **Token returned** - The flow returns a Descope session token that reflects both a successful sign-in and a completed step-up, in one pass. ## Key Points - The `su` claim is added because of the flow-level step-up marking from the Step Up action, not because of any setting on the Generate JWT action itself. The Generate JWT action must run after a Step Up action in the same flow for its issued token to be stepped up. - This works with any of the three Generate JWT actions (Sign In, Sign Up, or Sign Up or In with 3rd Party IdP). - Your app can validate the result the same way it validates any other step-up: check for `su: true` on the session token. See [Step-Up Validation](/mfa-and-step-up/step-up#step-up-validation). - If you don't add a Step Up action to the flow, the Generate JWT action still issues a valid session token, just without the `su` claim. # AI Assistants (/ai-assistants) Connect Descope to Cursor, Claude Code, VS Code, and other AI assistants. # AI Assistants Use Descope directly from AI-powered IDEs and chatbots — without constantly switching to the console or searching docs manually. Descope offers two complementary tools for agent-assisted development: | Tool | What it does | Best for | | ---- | ------------ | -------- | | **[Descope MCP Server](/mcp/mcp-server)** | Connects your assistant to Descope over MCP — search documentation, query your project, and call Management API operations from the chat. | Exploring your project, looking up docs, managing users/flows/tenants, and skills that need live API access (such as FGA schema apply). | | **[Descope Skills](/ai-assistants/skills)** | Installable instruction packs that teach your agent Descope-specific workflows — SDK integration, migrations, BYOS, Terraform, and security review. | Multi-step implementation tasks with framework detection, guardrails, and repeatable procedures. | ## How they work together **Skills** tell your agent *how* to do Descope work — which SDK calls to make, how Auth0 maps to Descope, or how to parse flow JSON for BYOS screens. The **MCP server** gives your agent *current context* — documentation search, your project's users and flows, and (for some operations) the ability to apply changes. You can use either one alone. Many teams install both: skills for structured builds and migrations, MCP for day-to-day project management and doc lookup. Migration skills and the FGA schema skill check for MCP when available so SDK and API details stay accurate. ## Compatible tools Both work with agents that support MCP or the [Agent Skills](https://github.com/descope/skills) format, including: - Cursor - Claude Code and Claude Desktop - VS Code (GitHub Copilot) - Windsurf - OpenCode - Codex - ChatGPT (MCP connectors) See [Descope MCP Server — Connecting](/mcp/mcp-server#connecting-to-the-mcp-server) for client-specific setup, and [Descope Skills — Installation](/ai-assistants/skills#installation) for the skills CLI and Claude Code plugin. ## Related - [Model Context Protocol (MCP)](/mcp) — secure MCP servers and authorization with Descope - [descope/skills on GitHub](https://github.com/descope/skills) — skill source and updates - [Getting Started](/getting-started) — first steps with Descope in your application # Descope Skills (/ai-assistants/skills) Install official Descope Skills for Cursor, Claude Code, and other AI agents. # Descope Skills **Descope Skills** are installable instruction packs for AI coding agents — Cursor, Claude Code, GitHub Copilot, Windsurf, OpenCode, and [36+ compatible tools](https://github.com/descope/skills#compatible-agents). Each skill teaches your assistant Descope-specific workflows: correct SDK patterns, console setup steps, migration mappings, and guardrails that reduce hallucinated APIs or insecure auth code. Most skills work on their own. Used together with the [Descope MCP Server](/mcp/mcp-server), they make it straightforward to build, migrate, and operate Descope from inside your IDE or chat interface. **Skills** encode *how* to do Descope work — multi-step workflows, framework detection, and safety checks. The **[Descope MCP Server](/mcp/mcp-server)** gives your agent *live access* — search docs, query your project, manage users and flows, and (for some skills) apply FGA schema changes. Migration and FGA skills call the MCP server when available to verify SDK method names and option shapes against current documentation. ## Installation Install the full collection from the official [descope/skills](https://github.com/descope/skills) repository: ```bash npx skills add descope/skills ``` **Claude Code** — add the marketplace plugin: ```text /plugin marketplace add descope/skills /plugin install descope-skills ``` Once installed, skills load automatically. Describe what you need in natural language — no slash commands required unless noted below. ## Available Skills ### Authentication & Custom UI | Skill | Use for | | ----- | ------- | | **[descope-auth](https://github.com/descope/skills/tree/main/skills/descope-auth)** | Add Descope login to your app — passwordless OTP, magic link, passkeys, OAuth, SSO, MFA, and passwords. Detects Next.js, React, Node.js, and Python and routes to the right integration guide. | | **[descope-byos-builder](https://github.com/descope/skills/tree/main/skills/descope-byos-builder)** | Build [Bring Your Own Screen (BYOS)](/flows/screens/byos) React UI on top of Descope Flows. Parses exported flow JSON for real interaction IDs and form keys — avoids silent BYOS failures. | **Example prompts** - *"Add Descope authentication to my Next.js app"* - *"Help me implement passkey login with Descope"* - *"My BYOS submit button does nothing — no errors in the console"* - *"Build custom login screens over my Descope sign-up-or-in flow"* ### Migration | Skill | Use for | | ----- | ------- | | **[auth0-to-descope](https://github.com/descope/skills/tree/main/skills/auth0-to-descope)** | Self-service migration from Auth0 — SDK replacement, Actions → Flows, Organizations → Tenants, Token Vault, CIBA, and SSO. Produces a `MIGRATION-PLAN.md` for review before any code changes. | | **[okta-cis-to-descope](https://github.com/descope/skills/tree/main/skills/okta-cis-to-descope)** | Self-service migration from Okta Customer Identity Service (CIS) — hosted OIDC and embedded widget paths, Sign-On Policies → Flows, Authorization Servers → Resources/Inbound Apps, and `scp` → `scope` claim updates. | | **[workos-to-descope](https://github.com/descope/skills/tree/main/skills/workos-to-descope)** | Self-service migration from WorkOS — AuthKit → Flows, Organizations → Tenants, Enterprise SSO → Tenant SSO, Directory Sync/SCIM → Descope SCIM, and Admin Portal → SSO Setup Suite / Widgets. Produces a `MIGRATION-PLAN.md` for review before any code changes. | | **[stytch-to-descope](https://github.com/descope/skills/tree/main/skills/stytch-to-descope)** | Self-service migration from Stytch — Consumer/B2B auth → Flows, Organizations/Members → Tenants/Users, Enterprise SSO → Tenant SSO, SCIM, Connected Apps → Federated/Inbound Apps, and M2M → Access Keys. Produces a `MIGRATION-PLAN.md` for review before any code changes. | | **[pingone-to-descope](https://github.com/descope/skills/tree/main/skills/pingone-to-descope)** | Self-service migration from PingOne CIAM — DaVinci flows → Descope Flows and connectors, Populations/Groups → Tenants/Roles, External IdPs -> tenant SSO, OIDC/SAML Web Apps → Federated Apps, and Worker Apps → Management Keys. Produces a `MIGRATION-PLAN.md` for review before any code changes. | **Example prompts** - *"Migrate my Next.js app from nextjs-auth0 to Descope"* - *"How do Auth0 Actions map to Descope?"* - *"Our Express app uses @okta/oidc-middleware — how do we switch to Descope?"* - *"How do Okta Authorization Servers map to Descope Resources?"* - *"Migrate my app from WorkOS to Descope — we use @workos-inc/authkit-nextjs"* - *"How do WorkOS Organizations, Enterprise SSO, and Directory Sync map to Descope?"* - *"Migrate my Next.js app from stytch/nextjs to Descope"* - *"How do Stytch Organizations, Connected Apps, and RBAC map to Descope?"* - *"How do PingOne Environments, Populations, and Groups map to Descope?"* - *"Migrate my iOS Swift app from using Ping SDKs to Descope."* ### Authorization & Infrastructure | Skill | Use for | | ----- | ------- | | **[descope-fga-schema](https://github.com/descope/skills/tree/main/skills/descope-fga-schema)** | Author and apply [FGA](/authorization/rebac) schemas using the ReBAC/ABAC DSL. Dry-run validation, data-loss warnings, and user confirmation before live changes. **Requires the [Descope MCP Server](/mcp/mcp-server).** | | **[descope-terraform](https://github.com/descope/skills/tree/main/skills/descope-terraform)** | Manage Descope projects as code with the [Terraform provider](/managing-environments/terraform) — auth methods, RBAC, connectors, flows, and project settings across environments. | **Example prompts** - *"Define an FGA schema with users, organizations, and resource-level permissions"* - *"Set up Terraform to manage my Descope project"* - *"Create roles and permissions with Terraform"* ### Security Review | Skill | Use for | | ----- | ------- | | **[auth-review](https://github.com/descope/skills/tree/main/skills/auth-review)** | Static identity security review — enumerates routes, builds an authorization matrix, and triages auth/authz findings (IDOR, JWT flaws, session issues, OAuth gaps). Outputs `./auth-review/report-YYYY-MM-DD.md`. Read-only; does not modify code. | **Example prompts** - *"/auth-review"* - *"Audit my app for authentication and authorization vulnerabilities"* - *"Find IDOR and broken access control bugs in this repo"* ## Recommended Setup For the best experience working with Descope in an AI-powered IDE: 1. **Connect the [Descope MCP Server](/mcp/mcp-server)** — project management, docs search, and live API access. 2. **Install [Descope Skills](https://github.com/descope/skills)** — structured workflows for integration, migration, FGA, Terraform, and security review. 3. **Create a Descope project** — get your Project ID from [Settings → Project](https://app.descope.com/settings/project). Skills handle implementation patterns; MCP fills in current docs and project state. Migration skills explicitly check for MCP availability before executing so SDK calls stay accurate. # Business to Business (B2B) (/b2b) How Descope models B2B apps with tenants, SSO, Admin Portal, SCIM, RBAC, and tenant-level branding. # Business to Business (B2B) Descope is built for B2B apps where each customer is its own organization. You integrate once; every customer becomes a [tenant](/management/tenant-management) with its own users, SSO, roles, branding, and admin self-service. To see the model in a running app, try the [B2B React sample app](https://github.com/descope-sample-apps/b2b-react-sample-app) (Gibber). ## What You Get Per Customer One Descope project can serve many customers. Each customer is a tenant with its own identity provider, users, roles, SCIM, and styling, while your app keeps a single integration. ## How Multi-tenancy Works Users live at the **project** level. A user can belong to zero, one, or many tenants, and roles are scoped per tenant. The same person can be an Admin in one customer org and a Member in another, without a second identity. - **Identity is project-scoped.** Login ID, credentials, and MFA are shared across tenants by default. If the same email must be a separate account in each tenant, enable [Tenant User Isolation](/management/tenant-management#tenant-user-isolation). - **Authorization is tenant-scoped.** Roles and permissions are assigned per tenant and show up that way in the [JWT](/management/token). - **Your app enforces the JWT.** Descope authenticates the user and puts tenant membership and roles in the token. Your backend decides what that allows. ## How B2B differs from B2C Login and sessions look familiar, but selling to companies changes the identity model: - **Orgs are the unit of identity.** Users act on behalf of a tenant. Your backend needs to answer “which organization is this request for?” as reliably as “who is this user?” - **Enterprise SSO is expected.** Customers authenticate through Okta, Entra ID, Google Workspace, and similar IdPs. You usually need SSO per tenant, sometimes [multiple IdPs on one tenant](/sso/multi-sso), and [self-service setup](/auth-methods/sso/sso-setup-suite) so their IT isn’t opening tickets with you. - **Access control is contextual.** Roles differ per tenant, often driven by [IdP group mapping](/sso/sso-mapping). When static roles aren’t enough, use [FGA](/authorization) (ReBAC / ABAC). - **Lifecycle and compliance bar is higher.** Plan for [JIT](/sso/jit-provisioning) on SSO login, [SCIM](/management/tenant-management/scim) for hire/fire, [MFA](/mfa-and-step-up/mfa) where required, and [audit trails](/audit-trails-and-integrations) for sign-ins and admin changes. ## Common B2B use cases Pick the rows that match what you're building: | Use case | What you do with Descope | | -------- | ------------------------ | | **Enterprise customer with their own IdP** | Create a tenant, connect SAML or OIDC ([SSO](/auth-methods/sso)), optionally let their IT admin finish setup in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). | | **Self-serve customer onboarding** | Create tenants from [Flows](/management/tenant-management/handling-tenants-in-flows/add-attributes-to-tenant#creating-tenants-in-flows), the [Console](https://app.descope.com/tenants), or [Management SDKs / APIs](/management/tenant-management/sdks). Invite users or provision via SSO/SCIM. Sync CRM or billing with [connectors](/connectors) in the onboarding flow if you need it. | | **Route users by email domain (or enforce SSO)** | Associate domains with the tenant, and use Flow conditions to send SSO-enabled users to the right IdP ([Enforcing SSO](/flows/conditions/sso-enforced)). | | **Customer admins manage their own org** | Embed the [Admin Portal](/widgets/admin-portal) (or [admin widgets](/widgets/admins)) so tenant admins manage users, SSO, access keys, and audit logs without your support team. | | **Directory sync from the customer's IdP** | Turn on [SCIM](/management/tenant-management/scim) per tenant so users and groups stay in sync. | | **Roles from IdP groups** | Map groups and attributes on SSO login with [SSO mapping](/sso/sso-mapping) so access follows the customer's org chart. | | **Different auth rules per customer** | Override [passwords, OTP, Magic Link, MFA, session policy, and more](/management/tenant-management/tenant) at the tenant level. | | **Customer-specific login look and feel** | Apply [tenant-level styles](/management/styles) so each org sees its own branding in Flows. | | **Departments or regions under one customer** | Model hierarchy with [sub-tenants](/management/tenant-management/sub-tenants). | | **Same person in multiple customer orgs** | Keep one project identity and assign them to multiple tenants with different roles (see the diagram above). | | **Fine-grained permissions beyond roles** | Use [FGA](/authorization) (ReBAC / ABAC) when access depends on ownership, membership, or attributes, not only role names. | | **Add SSO without replacing your auth** | Use Descope only for the SSO handshake while you keep your own sessions. Start with [Getting Started with SSO](/auth-methods/sso/getting-started). | ## Product Map Deeper how-to pages, grouped by topic: ### Tenants and Users Create orgs, attach users, and scope roles per tenant: | Topic | Docs | | ----- | ---- | | Create and manage tenants | [Tenant management](/management/tenant-management), [Configuring a tenant](/management/tenant-management/tenant) | | Sub-tenants and hierarchy | [Sub-tenants](/management/tenant-management/sub-tenants) | | Users, invites, custom attributes | [User management](/management/user-management) | | Roles and permissions (including Tenant Admin) | [RBAC](/authorization/role-based-access-control), [B2B RBAC example](/authorization/role-based-access-control/examples/b2b-rbac) | | Fine-grained authorization (ReBAC / ABAC) | [Authorization / FGA](/authorization) | | Tenants in Flows | [Handling tenants in Flows](/management/tenant-management/handling-tenants-in-flows) | ### SSO and Federation Wire each customer's IdP, map groups, and provision on login: | Topic | Docs | | ----- | ---- | | SSO overview (SAML / OIDC, IdP vs SP) | [SSO](/auth-methods/sso) | | Quickstart (configure + code) | [Getting Started with SSO](/auth-methods/sso/getting-started) | | Customer self-service SSO setup | [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) | | Configure SSO on a tenant (Console / SDK) | [Tenant SSO](/management/tenant-management/sso) | | Multiple IdPs on one tenant | [Multiple SSO providers](/sso/multi-sso) | | Domain routing and SSO enforcement | [SSO domains](/auth-methods/sso), [Enforcing SSO in Flows](/flows/conditions/sso-enforced) | | JIT provisioning and attribute / group mapping | [JIT](/sso/jit-provisioning), [SSO mapping](/sso/sso-mapping) | | Test without a real IdP | [Mock SAML](/management/tenant-management/sso/mock-saml-testing) | | Go-live checklist | [SSO launch checklist](/auth-methods/sso/launch-checklist) | ### Admin Self-service and Provisioning Hand day-to-day identity work to the customer's admins, and keep directories in sync: | Topic | Docs | | ----- | ---- | | Admin Portal for tenant admins | [Admin Portal](/widgets/admin-portal) | | Embed individual admin widgets (users, roles, access keys, audit) | [Admin widgets](/widgets/admins) | | SCIM provisioning | [SCIM](/management/tenant-management/scim) | | Audit trails and streaming | [Audit](/audit-trails-and-integrations) | ### Branding, Sessions, and Auth Methods Per-tenant login UX and security policy: | Topic | Docs | | ----- | ---- | | Tenant-level Flow styling | [Styles](/management/styles) | | Per-tenant auth, MFA, and session settings | [Configuring a tenant](/management/tenant-management/tenant) | | MFA and step-up | [MFA](/mfa-and-step-up/mfa) | | Project-level SSO settings | [SSO settings](/auth-methods/sso/settings) | | Connectors in onboarding Flows | [Connectors](/connectors) | ## Sub-tenants When one customer needs nested orgs (regions, business units, franchises), use [sub-tenants](/management/tenant-management/sub-tenants). Inheritance of users and roles from the parent is configurable when you create the child. ## Where to Start 1. Create a tenant in the [Console](https://app.descope.com/tenants) or via the [Management SDK](/management/tenant-management/sdks). 2. Decide how users join: invites, self-sign-up into a tenant, [SSO](/auth-methods/sso/getting-started), or [SCIM](/management/tenant-management/scim). 3. If customers bring their own IdP, plan for the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) and [Admin Portal](/widgets/admin-portal) so their admins can self-serve. 4. Assign [roles](/authorization/role-based-access-control) per tenant and validate the tenant claims in your [session JWT](/management/token). For a longer product overview, see the [B2B Authentication](https://www.descope.com/blog/post/b2b-authentication-overview) blog post. # Fingerprinting (/fingerprinting) This guide explains the fingerprinting capabilities available in Descope, including device fingerprinting, risk-based authentication, and bot detection. # Fingerprinting Descope provides built-in device fingerprinting and risk detection capabilities to help you strengthen your application's security. By analyzing device and session data, Descope can detect suspicious activity such as bot behavior, impossible travel, and unrecognized devices. You can use these built-in risk signals (`riskInfo`) directly in your authentication flows or enhance them with advanced fraud detection connectors. This guide provides a high-level overview of both options. ## Built-In Risk Signals (`riskInfo`) For implementation instructions, visit the [Implementing Fingerprinting](/flows/use-cases/implementing-fingerprinting) guide. Every Descope project includes several default risk signals available through the `riskInfo` context object. These signals can be used in flow logic to adapt the authentication flow based on the risk level of the user. | Signal | Description | |:-------|:------------| | **riskInfo.botDetected** | Detects bot-like behavior based on network-level signals from Cloudflare. | | **riskInfo.impossibleTravel** | Flags login attempts that are geographically implausible compared to previous logins, based on time and distance. | | **riskInfo.riskScore** | Risk score (0 - 1) derived from Cloudflare and optional connector signals. [Learn more](/flows/use-cases/implementing-fingerprinting) | | **riskInfo.trustedDevice** | Indicates whether the current device was previously marked as trusted via a cookie on your [custom domain](/how-to-deploy-to-production/custom-domain). | These signals are available by default and can be used without additional setup in most cases. As a free additional layer of protection beyond what the Fraud Connectors can do for you, consider enabling the **Bot Trap** feature in your screen configurations. The **Bot Trap** adds an invisible field that basic bots will attempt to fill, automatically flagging them. Learn more about [Bot Trap](/flows/screens#bot-trap) here. ## Example Use Cases Risk detection can be used throughout your authentication flows to reduce friction for low-risk users and enforce stronger checks for high-risk scenarios. | Use Case | Example | How Fingerprinting Helps | |:---------|:--------|:-------------------------| | **Bot Prevention at Signup** | Preventing automated account creation. | Use `riskInfo.botDetected` or `riskInfo.riskScore` to challenge suspicious signups with CAPTCHA or block them entirely. | | **High-Risk Login Detection** | Detecting compromised accounts or unfamiliar devices. | Use `riskInfo.riskScore` and `riskInfo.trustedDevice` to trigger MFA for high-risk logins. | | **Adaptive Authentication** | Adjusting authentication steps based on risk. | Branch your flow to add verification steps only when certain risk signals are present. | | **Trusted Device Recognition** | Reducing friction for returning users. | Use `riskInfo.trustedDevice` to skip MFA for previously verified devices. | ## Enhancing Fingerprinting with Connectors Most connector-based risk detection must be added after a **Screen** component in your flow. Descope also supports integration with third-party fraud detection providers for more advanced risk analysis and fingerprinting. These connectors can be used **in addition to** or **instead of** the default `riskInfo` signals. Some of the supported capabilities include: - Advanced device fingerprinting and browser profiling - VPN and proxy detection - AI operator and automation detection - IP threat reputation checks - Behavioral risk scoring - Breach monitoring and email reputation You can explore available connectors in the [Fraud Connectors Doc](/connectors/connector-configuration-guides/fraud). Examples include: Most fingerprinting related connectors require an external subscription. - **[reCAPTCHA Enterprise](/connectors/connector-configuration-guides/fraud/recaptcha-enterprise)** (bot protection) - **Turnstile** (alternative CAPTCHA) - **[Telesign](/connectors/connector-configuration-guides/fraud/telesign)** (phone number and risk intelligence) - **[Fingerprint](/connectors/connector-configuration-guides/fraud/fingerprint)** (advanced device and browser fingerprinting) - **[Forter](/connectors/connector-configuration-guides/fraud/forter)** and **Sardine** (fraud and behavioral risk scoring) ## Next Steps To configure and use fingerprinting in your flows, visit the [Implementing Fingerprinting](/flows/use-cases/implementing-fingerprinting) guide. The implementation guide includes: - How to set up fingerprinting in Descope Flows - When to use the Fingerprint Assess action - Best practices for balancing security and user experience # Backend EHR Integrations (/healthcare/ehr-integrations) How to integrate Descope with Epic and other EHR systems using SMART Backend Services # Backend EHR Integrations This guide explains how to integrate Descope with **[Epic](https://www.epic.com/)**, **[Meditech](https://ehr.meditech.com/)**, **[OpenEMR](https://github.com/openemr/openemr)**, and other EHR systems. This integration allows your backend service to securely authenticate with an EHR system using a signed JWT (client assertion) instead of a client secret. This is the standard backend-only / system-level SMART on FHIR flow, designed for cases where no end-user is present. By handling JWT construction, signing, and EHR-specific token endpoint requirements, Descope removes the complexity typically involved in connecting to EHR platforms such as Epic, Meditech, OpenEMR, and other [SMART on FHIR](https://hl7.org/fhir/)-compliant systems. ## Overview [SMART Backend Services](https://build.fhir.org/ig/HL7/smart-app-launch/backend-services.html) enable backend applications to authenticate to EHR systems without user interaction. Instead of using traditional client secrets, your service generates a **signed JWT assertion** using a private key. The EHR validates this assertion using your public key and issues an access token. This approach allows you to request system-level scopes like `system/*.read` and access FHIR resources from automated server processes, scheduled jobs, or background services. Descope supports both directions of this flow: ### [Outbound Flow](#outbound-flow-descope--ehr) Descope generates signed JWT assertions and obtains **EHR-issued access tokens** that your backend can use to call FHIR APIs. ### [Inbound Flow](#inbound-flow-external-jwt--descope-token) Descope can accept **EHR-issued JWTs or access tokens** and exchange them for Descope tokens. This enables you to: - Treat an EHR-issued token as an authenticated identity, - Map EHR users or service accounts into Descope identities, - Normalize and unify identity handling across multiple EHR systems. ## Outbound Flow (Descope → EHR) This flow lets your backend obtain an EHR access token to call FHIR APIs. 1. Register your backend app in the EHR. 2. Upload your Descope public key (JWKs) to the EHR. 3. Use Descope to generate a **client assertion JWT**. 4. Send the JWT to the EHR token endpoint with `grant_type=jwt-bearer`. 5. The EHR validates the assertion and returns an **access token**. 6. Use the access token to call FHIR endpoints. ### 1. Register Your Backend App in the EHR #### Epic 1. Log into **Epic App Orchard / Connection Hub**. 2. Register a **Backend Service / System App**. 3. Select required scopes (for example, `system/*.read`). 4. Note the **Client ID** and **Token URL**. 5. Upload your **Descope Public Key (JWKs)**. #### Other Supported EHR Systems Descope supports integration with these EHR systems out of the box: | EHR | Token Endpoint | Algorithm | Notes | |---------------------------|------------------------------------------|----------:|----------------------------------------| | Meditech | `https:///token` | RS256 | Standard SMART backend services | | Medplum | `https:///oauth2/token` | RS256 | Ideal for testing & development | | eClinicalWorks (eCW) | `https:///oauth2/token` | RS256 | Standard OAuth2 implementation | | OpenEMR | `https:///oauth2/default/token` | RS384 | Requires RS384; set `flattenAudience: true` for a string `aud` claim | Descope automatically handles the correct signing algorithm, JWT claim structure, and token exchange format for each EHR system. For any SMART on FHIR-compliant EHR not listed here, Descope can be configured with custom token endpoints and signing algorithms. ### 2. Get Your Descope Public Key Get your Descope public key from the project-level JWKs URL: Replace `api.descope.com` in the URL with your [custom domain](/how-to-deploy-to-production/custom-domain) if applicable. ``` __BaseURL__/__ProjectID__/.well-known/JWKs.json ``` Descope automatically manages the private key used for signing. The JWKs endpoint contains the public key that EHR systems need to validate your client assertions. Upload this public key (or provide the JWKs URL) to your EHR system during app registration. #### Additional Signing Algorithms Descope only supports `RS384` and `ES384`, as additional signing algorithms for client assertions. By default, client assertions are signed with **RS256**. Some EHRs (for example, [OpenEMR](#other-supported-ehr-systems)) require **RS384** or **ES384**. To use those algorithms, you must enable them in your Descope project via the Management API: ```bash curl -X POST "__BaseURL__/v1/mgmt/project/signkey/additional/create" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer __ProjectID__:" \ -d '{"algorithm": "RS384"}' ``` This is a one-time initialization per algorithm, you do not call this endpoint repeatedly for the same Descope project. The new key will then be added to your project, and the JWKs URL will include it so the EHR can validate assertions signed with this key. Afterwards, when [generating the client assertion](#3-generate-a-client-assertion-jwt), you can simply pass the same `algorithm` in the request body (see below). ### 3. Generate a Client Assertion JWT [JWT templates](/management/token/jwt-templates) configured in your Descope Project do not apply to this client assertion JWT. You can generate the client assertion JWT using the Descope Management API or the Descope Node SDK. It is recommended to use a short lifetime in the `expiresIn` parameter for client assertions (for example, 300 seconds) to ensure the client assertion is not reused. ```typescript import { DescopeClient } from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: process.env.DESCOPE_PROJECT_ID!, managementKey: process.env.DESCOPE_MANAGEMENT_KEY!, }); async function generateEpicClientAssertion() { const clientId = process.env.EPIC_CLIENT_ID!; const tokenUrl = process.env.EPIC_TOKEN_URL!; const response = await descopeClient.management.jwt.generateClientAssertionJwt( clientId, // issuer clientId, // subject [tokenUrl], // audience 300, // expiresIn (seconds) true, // flattenAudience — string aud when one audience value 'RS256', // algorithm — use 'RS384' for OpenEMR ); const clientAssertionJwt = response.data.jwt; return clientAssertionJwt; } ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/token/clientassertion" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer __ProjectID__:" \ -d '{ "issuer": "EPIC_CLIENT_ID", "subject": "EPIC_CLIENT_ID", "audience": ["https://epic.example.com/oauth2/token"], "expiresIn": 300, "flattenAudience": true, "algorithm": "RS256" // or "RS384" or "ES384" }' ``` #### Using the Node SDK Use [`@descope/node-sdk`](https://www.npmjs.com/package/@descope/node-sdk) with a [management key](https://app.descope.com/settings/company/managementkeys): ```typescript descopeClient.management.jwt.generateClientAssertionJwt( issuer, // string — typically your EHR client ID subject, // string — typically the same as issuer audience, // string[] — token endpoint URL(s) expiresIn, // number — lifetime in seconds (e.g. 300) flattenAudience?, // boolean — optional; see below algorithm?, // 'RS256' | 'RS384' | 'ES384' — optional; default RS256 ) ``` | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `issuer` | `string` | Yes | JWT `iss` claim; must match the client ID registered with the EHR. | | `subject` | `string` | Yes | JWT `sub` claim; usually the same value as `issuer`. | | `audience` | `string[]` | Yes | JWT `aud` claim value(s); use the EHR token endpoint URL. | | `expiresIn` | `number` | Yes | Assertion lifetime in seconds. Use a short value (for example, `300`). | | `flattenAudience` | `boolean` | No | When `true` and a single audience is provided, emits `aud` as a string instead of a one-element array. Required by some EHRs (for example, OpenEMR). | | `algorithm` | `string` | No | Signing algorithm: `RS256` (default), `RS384`, or `ES384`. Create additional sign keys first if not using RS256. | #### Audience claim format (`flattenAudience`) By default, Descope sets `aud` as a JSON array even when you pass one audience value: ```json { "aud": ["https://localhost:8090/oauth2/default/token"], "iss": "YOUR_CLIENT_ID", "sub": "YOUR_CLIENT_ID" } ``` Some EHR validators (including OpenEMR when testing EPIC / SMART on FHIR) expect `aud` to be a **string**, as described in the [SMART App Launch client confidential asymmetric profile](https://build.fhir.org/ig/HL7/smart-app-launch/client-confidential-asymmetric.html): ```json { "aud": "https://localhost:8090/oauth2/default/token", "iss": "YOUR_CLIENT_ID", "sub": "YOUR_CLIENT_ID" } ``` Pass `flattenAudience: true` (Node SDK fifth argument, or `"flattenAudience": true` in the HTTP API body) when the EHR rejects an array `aud` claim. ### 4. Exchange the Assertion for an EHR Access Token Once you have the client assertion JWT from Descope, exchange it with the EHR: ```http POST Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer client_assertion= ``` Example Epic response: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 300 } ``` ### 5. Use the Access Token to Call FHIR Use the returned EHR access token in the `Authorization` header: ```http GET https:///api/FHIR/R4/Patient/123 Authorization: Bearer Accept: application/fhir+json ``` You can now perform FHIR operations allowed by the granted scopes (for example, `system/*.read`). ### Example: End-to-End Outbound Flow ```typescript import axios from 'axios'; import { DescopeClient } from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: process.env.DESCOPE_PROJECT_ID! }); async function getEhrAccessToken() { const clientId = process.env.EHR_CLIENT_ID!; const tokenUrl = process.env.EHR_TOKEN_URL!; const fhirBase = process.env.EHR_FHIR_BASE!; // 1. Generate client assertion with Descope const assertionResp = await descopeClient.management.jwt.generateClientAssertionJwt( clientId, clientId, [tokenUrl], 300, true, // flattenAudience ); const clientAssertionJwt = assertionResp.data.jwt; // 2. Exchange assertion for EHR access token const params = new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', client_assertion: clientAssertionJwt, }); const tokenResponse = await axios.post(tokenUrl, params.toString(), { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); const ehrAccessToken = tokenResponse.data.access_token as string; // 3. Use EHR access token to call FHIR const patientResp = await axios.get(`${fhirBase}/Patient/123`, { headers: { Authorization: `Bearer ${ehrAccessToken}`, Accept: 'application/fhir+json', }, }); return patientResp.data; } getEhrAccessToken().then(console.log).catch(console.error); ``` ```bash # 1. Generate client assertion with Descope Management API # Use "algorithm": "RS384" or "ES384" if your EHR requires it (create additional sign key first). curl -X POST "__BaseURL__/v1/mgmt/token/clientassertion" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer __ProjectID__:" \ -d '{ "issuer": "EHR_CLIENT_ID", "subject": "EHR_CLIENT_ID", "audience": ["https://ehr.example.com/oauth2/token"], "expiresIn": 300, "flattenAudience": true, "algorithm": "RS256" }' # Response contains the client assertion JWT # {"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."} # 2. Exchange assertion for EHR access token curl -X POST "https://ehr.example.com/oauth2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=" # Response contains the EHR access token # {"access_token": "abc123", "token_type": "Bearer", "expires_in": 300} # 3. Use EHR access token to call FHIR curl -X GET "https://ehr.example.com/api/FHIR/R4/Patient/123" \ -H "Authorization: Bearer " \ -H "Accept: application/fhir+json" ``` ### Multi-EHR / Multi-Tenant Patterns For Descope projects that involve multiple [tenants](/management/tenant-management) integrating with different EHR vendors: - Maintain a configuration map per tenant or per EHR. You can use tenant custom attributes to store the configuration, including: - EHR type (Epic, Meditech, OpenEMR, etc.) - Client ID - Token URL - FHIR base URL Example config shape: ```json { "tenantA": { "ehrType": "epic", "clientId": "EPIC_CLIENT_ID", "tokenUrl": "https://epic.example.com/oauth2/token", "fhirBase": "https://epic.example.com/api/FHIR/R4" }, "tenantB": { "ehrType": "openemr", "clientId": "OPENEMR_CLIENT_ID", "tokenUrl": "https://openemr.example.com/oauth2/default/token", "fhirBase": "https://openemr.example.com/apis/default/fhir/R4" } } ``` Your backend can then look up the tenant attribute based on the user who is logged in, and then use the respective values to [generate a client assertion JWT](#3-generate-a-client-assertion-jwt), as described above. ## Inbound Flow (External JWT → Descope Token) This Inbound Flow requires a specific license. Please contact [Descope Support](/support) to enable this feature for your project. In the **inbound flow**, you receive a token from the EHR (or another SMART-compliant identity provider) and want to: - Accept it as user identity. - Map the subject to a Descope user. - Issue Descope tokens (session, access, refresh). - Normalize identity across multiple EHRs. ### 1. Configure External Token Validation You can configure external token validation under the External Token Validation section of your [Inbound App](/identity-federation/inbound-apps) settings. If you do not already have an inbound application, you must [create one first](/identity-federation/inbound-apps/creating-inbound-apps). ![Add Inbound App -> JWT Bearer](/assets/add-inbound-app-jwt-bearer.webp) #### Issuer URL Enter the issuer URL for your EHR system. Some examples are: - Epic: `https://fhir.epic.com/interconnect-fhir-oauth/oauth2` - Meditech: `https:///oauth2` - OpenEMR: `https:///oauth2/default` #### JWKs URL You can manually override the **JWKs URL** if your EHR uses custom endpoints or if automatic discovery is not available. #### Signing Algorithm Select the signing algorithm used by your EHR system. The currently supported algorithms are: - RS256 - ES384 - ES256 - ES512 #### User Information Endpoint URL Optionally configure a `/me` or userinfo endpoint to pull additional user metadata after validating the JWT. #### User Information LoginID Field Name Choose which JWT claim identifies the user in Descope. Some examples are: - `sub` - `fhirUser` - Any nested claim (for example, `claims.user.id`) - A field from the `/me` response Currently, only the user identifier is supported. Other user attributes/information from the token cannot be mapped to user attributes in Descope. ### 2. Exchange the External Token with Descope EHR access tokens are typically short-lived tokens (5 - 15 minutes), and therefore must be exchanged quickly after generation. To exchange the EHR-issued token for a Descope token: ```http POST __BaseURL__/oauth2/v1/apps/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer assertion= client_id= ``` Descope validates the external token and returns: ```json { "access_token": "", "expires_in": 3600, "token_type": "Bearer" } ``` You can now use `access_token` as a standard Descope token in your APIs and frontends. ## Troubleshooting These are some common errors you might encounter when integrating with EHR systems: | Symptom / Error | Likely Cause | How to Fix | | ---------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------------------- | | `invalid_client` | `issuer` / `subject` don't match the registered client ID | Ensure both `issuer` and `subject` match the EHR client ID exactly. | | `invalid_grant` or `invalid_request` | `audience` does not match token URL; or assertion expired | Use the exact token endpoint URL as `audience`; ensure `expiresIn` is valid. | | JWT validation failed; `aud` is an array | EHR expects a string `aud` claim (common with OpenEMR) | Pass `flattenAudience: true` to `generateClientAssertionJwt`. | | Signature verification failed | Using the wrong algorithm or stale public key | Confirm RS256 vs RS384; re-upload JWKs; ensure you're using the right key. | | 401/403 when calling FHIR endpoints | Missing or insufficient scopes | Confirm the registered app has `system/*.read` or required resource scopes. | | Works in one environment but not another (sandbox vs prod) | Different token URLs, issuers, or keys per environment | Update config for each environment and ensure correct issuer/audience. | ## Testing & Sandbox Guidance For development and testing: - Use EHR sandbox environments (Epic Sandbox, Medplum, etc.). - Start with read-only scopes such as `system/*.read`. - Create a simple health check in your backend that: 1. Calls Descope to generate a client assertion. 2. Exchanges it with the EHR for an access token. 3. Calls a lightweight endpoint (for example, `/metadata` or `/Patient`). 4. Returns success/failure and any relevant error messages. This gives you a single endpoint to verify that Descope configuration and EHR configuration are all working end-to-end. # SMART on FHIR (/healthcare/smart-fhir) How to integrate Descope as an OAuth provider for SMART on FHIR applications, supporting both EHR Launch and Standalone Launch flows. # SMART on FHIR This guide explains how to use Descope as an OAuth 2.0 provider for [SMART on FHIR](https://hl7.org/fhir/smart-app-launch/) applications. SMART on FHIR is a healthcare industry standard that allows third-party applications to securely access Electronic Health Record (EHR) data. Descope's [Inbound Apps](/identity-federation/inbound-apps) feature enables you to act as an OAuth provider for SMART apps, handling user authentication, consent, and token issuance while your application manages the SMART-specific launch context and FHIR API calls. ## Overview SMART on FHIR provides a standardized way for healthcare applications to: - Launch from within an EHR system (EHR Launch) or as a standalone application (Standalone Launch) - Request access to patient data with granular scopes - Handle user authentication and authorization - Access FHIR resources on behalf of authenticated users Descope supports both **EHR Launch** and **Standalone Launch** flows through Inbound Apps, which provide: - `/authorize` endpoint for user authentication and consent - `/token` endpoint for token exchange - JWT templates for customizing access tokens with SMART-specific claims - Scope management and validation ## Architecture In a SMART on FHIR integration with Descope: 1. Your application receives launch requests from the EHR (for EHR Launch) or initiates authentication (for Standalone Launch) 2. Descope acts as the OAuth authorization server, handling user authentication and consent 3. Your application receives the access token and uses it to call the EHR's FHIR API The access token issued by Descope includes SMART-specific claims (like `patient`, `encounter`) that your application forwards to the EHR's FHIR server. ## Launch Types ### EHR Launch The app is launched from within the EHR. The EHR redirects to your app with a `launch` parameter containing an encrypted launch context token. **Flow:** 1. User clicks your app in the EHR 2. EHR redirects to your app with `iss` (EHR issuer URL) and `launch` parameters 3. Your app extracts launch context and redirects to Descope for authorization 4. User authenticates and consents via Descope 5. Descope redirects back with authorization code 6. Your app exchanges code for access token 7. Access token includes launch context (patient ID, encounter ID, etc.) ### Standalone Launch The app is launched independently, without being embedded in the EHR. Users authenticate directly. **Flow:** 1. User navigates directly to your app 2. Your app redirects to Descope for authorization 3. User authenticates and consents via Descope 4. Descope redirects back with authorization code 5. Your app exchanges code for access token 6. Access token can be used to access FHIR resources (patient selection may happen later) ## Integration Steps ### 1. Configure Your Inbound App in Descope 1. In the Descope Console, navigate to [Inbound Apps](https://app.descope.com/apps/inbound). Click **+ Inbound App**. 2. Configure the required **scopes** for your SMART app. Common SMART scopes include: - `patient/*.rs` - Read and search any resource for the current patient - `patient/*.read` - Read any resource for the current patient - `user/*.rs` - Read and search any resource for the current user - `launch` - Required for EHR Launch to receive launch context - `openid` - Standard OIDC scope - `fhirUser` - Retrieve information about the current logged-in user - `offline_access` - Request a refresh token For a complete list of SMART scopes, refer to the [SMART App Launch documentation](https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#quick-start). 3. Set the **redirect URI** to your application's callback URL (e.g., `https://yourapp.com/oauth/callback`) 4. Optionally customize your [consent flow](/identity-federation/inbound-apps/creating-inbound-apps) to match your branding ### 2. Configure JWT Template for SMART Claims Create a JWT template that includes SMART-specific claims in the access token: 1. In the Descope Console, go to Project Settings -> [JWT Templates](https://app.descope.com/settings/project/jwt), and create a new template. 2. Configure the template with SMART-specific claims: ```json { "aud": "{{fhir_server_url}}", "scope": "{{scopes}}", "patient": "{{patient_id}}", "encounter": "{{encounter_id}}", "need_patient_banner": true, "smart_style_url": "{{smart_style_url}}" } ``` Some of the key claims that you will need to configure are: - `aud` - The FHIR server URL (audience). This should match the EHR's FHIR endpoint - `scope` - The authorized scopes (automatically included) - `patient` - Patient ID from launch context (for EHR Launch) - `encounter` - Encounter ID from launch context (if available) - `need_patient_banner` - Indicates if the app should display patient context - `smart_style_url` - URL for SMART styling resources (optional) 3. Under Project Settings -> [Session Management](https://app.descope.com/settings/project/session), assign this JWT template as the **User JWT** The `aud` claim must match the FHIR server URL where your app will make API calls. For multi-EHR scenarios, you may need to use dynamic values or create separate templates per EHR. ### 3. Handle Launch Parameters (EHR Launch) When your app receives a launch request from the EHR, extract the launch context: ```typescript // Example: Express.js route handler app.get('/launch', (req, res) => { const { iss, launch } = req.query; // Store launch context temporarily (e.g., in session or encrypted cookie) // You'll need to decode the launch parameter to extract patient/encounter IDs const launchContext = { iss, // EHR issuer URL launch, // Launch context token state: generateRandomState() }; // Store in session for later use req.session.launchContext = launchContext; // Redirect to Descope authorization const authUrl = buildAuthorizationUrl(launchContext); res.redirect(authUrl); }); ``` ### 4. Build Authorization Request Construct the authorization request to Descope's `/authorize` endpoint: ```typescript function buildAuthorizationUrl(launchContext: LaunchContext): string { const params = new URLSearchParams({ client_id: process.env.DESCOPE_CLIENT_ID!, redirect_uri: 'https://yourapp.com/oauth/callback', response_type: 'code', scope: 'openid fhirUser patient/*.read launch', state: launchContext.state, // Include launch parameter for EHR Launch ...(launchContext.launch && { launch: launchContext.launch }), // Include iss parameter ...(launchContext.iss && { iss: launchContext.iss }) }); return `__BaseURL__/oauth2/v1/apps/authorize?${params.toString()}`; } ``` ### 5. Handle OAuth Callback After user authentication and consent, Descope redirects back to your callback URL: ```typescript app.get('/oauth/callback', async (req, res) => { const { code, state } = req.query; // Verify state matches if (state !== req.session.launchContext?.state) { return res.status(400).send('Invalid state parameter'); } // Exchange authorization code for access token const tokenResponse = await exchangeCodeForToken(code as string); // Extract launch context from token (if EHR Launch) const launchContext = extractLaunchContext(tokenResponse); // Store token and launch context req.session.accessToken = tokenResponse.access_token; req.session.patientId = launchContext.patient; req.session.encounterId = launchContext.encounter; // Redirect to your app's main interface res.redirect('/app'); }); ``` ### 6. Exchange Authorization Code for Token The `client_secret` comes from the [Inbound App](/identity-federation/inbound-apps) configuration. Exchange the authorization code for an access token: ```typescript async function exchangeCodeForToken(code: string) { const response = await fetch('__BaseURL__/oauth2/v1/apps/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'authorization_code', client_id: process.env.DESCOPE_CLIENT_ID!, client_secret: process.env.DESCOPE_CLIENT_SECRET!, code: code, redirect_uri: 'https://yourapp.com/oauth/callback', }), }); return await response.json(); } ``` The token response includes: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "abc123...", "scope": "openid fhirUser patient/*.read launch", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." } ``` ### 7. Extract Launch Context from Token Decode the access token to extract SMART-specific claims: ```typescript import jwt from 'jsonwebtoken'; function extractLaunchContext(tokenResponse: TokenResponse) { const decoded = jwt.decode(tokenResponse.access_token, { complete: true }); const payload = decoded?.payload as any; return { patient: payload.patient, encounter: payload.encounter, need_patient_banner: payload.need_patient_banner, smart_style_url: payload.smart_style_url, }; } ``` ### 8. Use Access Token with FHIR API Use the access token to call the EHR's FHIR API: ```typescript async function fetchPatientData(accessToken: string, patientId: string) { const fhirServerUrl = process.env.FHIR_SERVER_URL!; const response = await fetch( `${fhirServerUrl}/Patient/${patientId}`, { headers: { 'Authorization': `Bearer ${accessToken}`, 'Accept': 'application/fhir+json', }, } ); return await response.json(); } ``` ## Standalone Launch Example For Standalone Launch, the flow is simpler since there's no launch context: ```typescript // User navigates directly to your app app.get('/login', (req, res) => { const authUrl = new URL('__BaseURL__/oauth2/v1/apps/authorize'); authUrl.searchParams.set('client_id', process.env.DESCOPE_CLIENT_ID!); authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/oauth/callback'); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid fhirUser patient/*.read'); authUrl.searchParams.set('state', generateRandomState()); res.redirect(authUrl.toString()); }); // After authentication, user may need to select a patient // This is typically done through a patient picker interface ``` ## Handling Launch Context For EHR Launch, you need to decode the `launch` parameter to extract patient and encounter IDs. The launch parameter is typically a JWT or encrypted token provided by the EHR. The exact format of the `launch` parameter varies by EHR. Some EHRs provide it as a simple identifier that you exchange for launch context via their API, while others provide it as a JWT. Consult your EHR's SMART on FHIR documentation for specifics. ## Token Refresh SMART on FHIR access tokens are typically short-lived. Use the refresh token to obtain a new access token: ```typescript async function refreshAccessToken(refreshToken: string) { const response = await fetch('__BaseURL__/oauth2/v1/apps/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: process.env.DESCOPE_CLIENT_ID!, client_secret: process.env.DESCOPE_CLIENT_SECRET!, refresh_token: refreshToken, }), }); return await response.json(); } ``` ## Troubleshooting These are some common issues you might encounter when integrating with SMART on FHIR: | Issue | Likely Cause | Solution | |-------|--------------|----------| | Invalid `launch` parameter | Launch context expired or malformed | Ensure launch context is used immediately after receipt | | Missing `patient` claim in token | Launch context not properly extracted | Verify launch parameter decoding and JWT template configuration | | FHIR API returns 401 | `aud` claim doesn't match FHIR server URL | Ensure JWT template `aud` claim matches the EHR's FHIR endpoint | | Scope not granted | Scope not configured in Inbound App | Add required scope to Inbound App configuration | | State mismatch | CSRF protection or session issue | Verify state parameter is properly stored and validated | # Bring Your Own Auth (/mcp/bring-your-own-auth) Use your existing auth system with Descope to protect an MCP server. # Bring Your Own Auth If you're looking to migrate off of your existing auth system, see our [migration guides](/migrate). If your organization uses its own identity system (whether homegrown or from another vendor), you can bring that into Descope and wire it to an MCP server so your product APIs and features are exposed to LLMs with proper OAuth tokens, scopes, and [DCR/CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods) client registration. ## Why Bring Your Own Auth? Enterprises often have: - A homegrown auth system or an existing IdP (e.g., Okta, Azure AD, Auth0) - Product UIs and APIs already protected by that system - A desire to expose the same product to MCP clients (IDEs, agents, chat) without maintaining a second auth stack Descope lets you keep your existing auth as the source of truth and use it to drive MCP server authorization: users sign in with your system, and Descope issues MCP-ready OAuth tokens with the right scopes and audience so your MCP server can enforce access consistently. ## How MCP Server Auth Works with Descope MCP server authentication is typically done via the **OAuth 2.1 authorization code flow**. Descope acts as the authorization server for your MCP server: - You [configure an MCP server](/agentic-identity-hub/core-components/mcp-servers) in Descope with a well-known endpoint, [scopes](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes), and [client registration](/agentic-identity-hub/core-components/mcp-servers/registration-methods) (DCR and/or CIMD). - MCP clients discover your server, register (via [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) or [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr)), and send users through the authorization code flow. - Your [user consent flow](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) in Descope is where you plug in your own auth: instead of (or in addition to) Descope's native methods, you can validate the user against your existing system and then issue a Descope session/JWT so the MCP client receives a valid OAuth access token for your MCP server. ## Three Ways to Bring Existing Authentication You can use your existing auth with Descope as the authorization server for your MCP server in three main ways: ### 1. Use Identity Federation (OIDC or SAML) With this method, Descope acts as the OAuth authorization server for your MCP server. Then, when a user connects an MCP client: 1. The client redirects the user to your Descope **user consent flow**. 2. In that flow, you authenticate the user with **your** system (e.g., redirect to your IdP as either a [custom OAuth provider](/auth-methods/oauth/providers/custom-providers) or [tenant-level SSO provider](/auth-methods/sso)). 3. After successful auth, the flow continues in Descope (e.g., consent screen, [custom claims](/flows/actions/custom-claims)), and the flow ends with the [End action](/flows/actions/end-action), which issues a Descope session and JWT. 4. The MCP client exchanges the authorization code for an access token; that token is valid for your MCP server and carries the scopes and claims you configured. You can use the connector response value(s) in [conditions](/flows/conditions), [scriptlets](/flows/actions/scriptlets), and [custom claims](/flows/actions/custom-claims). See [Connectors in Flows](/connectors/connectors-in-flows) for details. ### 2. Homegrown Auth (no OIDC/SAML) You have two options when your auth system is **homegrown** or a third-party that doesn't support OIDC or SAML: 1. Using External Authentication 2. Collecting user credentials in the flow and issuing a Descope JWT #### Using External Authentication If you already have a standalone login page and want Descope to hand off authentication to it, use the [External Authentication Flow action](/flows/actions/external-authentication). This works as follows: 1. The MCP client redirects the user to your Descope **user consent flow**. The Descope flow runs the **External Authentication** action. 2. The Descope Flow action redirects to your login page URL, preserving your existing authentication UX. 3. As a part of this redirect, Descope appends an external auth request ID as a query parameter (`external_auth_req_id`). 4. After your login completes, your backend should call Descope's `externalauth/complete` endpoint: - `POST __BaseURL__/v1/mgmt/flow/externalauth/complete` 5. The body includes `externalAuthReqId` plus identity/context fields such as `loginId`, verification flags, optional `customClaims`, and optional tenant association (`selectedTenantId`, `userTenants`). 6. Descope returns a redirect URL so the user can continue and complete the flow, after which the MCP OAuth flow proceeds normally according to the spec. For details on the request body, see the [External Authentication](/flows/actions/external-authentication) flow action doc. If you're using MCP authentication with [Connections](/agentic-identity-hub/core-components/connections) to manage credentials downstream of your MCP server, review [Multi-Tenancy with Connections](/agentic-identity-hub/core-components/connections/multi-tenancy) to make sure you are using the right token type with External Authentication. If the user is already logged in to your system, you can simply trigger a backend management API call after validating the session is active. ##### Using External Authentication with Connections When using [External Authentication](/flows/actions/external-authentication), you can pass `selectedTenantId` during completion. This helps establish tenant context early so your MCP and Connections flows can store and fetch the right token scope model. If the tenant does not yet exist, create it first with the Management SDK/API before completing external authentication. When your auth system is **homegrown** or a third-party that doesn't support OIDC or SAML, you can still use it in the consent flow by collecting credentials or a pre-existing token, validating them against your API, and then issuing a Descope JWT with a [Generate JWT](/flows/actions/generate-jwt) action. #### Collecting User Credentials in the Flow The [Generate JWT](/flows/actions/generate-jwt) action creates or finds a user in Descope using either `form.email` or `form.externalId` from the flow context. One of these values must be set before the Generate JWT step runs. To do this: 1. **Add a [Screen](/flows/screens)** where the user enters their credentials (for example, email/password or username/password) 2. **Validate the credentials** by sending them to your authentication API using a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) 3. **Map the response** (such as user ID or email) into the flow context 4. **Add a [Generate JWT](/flows/actions/generate-jwt) action** to provision the user in Descope and issue a session token You can use the connector response value(s) in [conditions](/flows/conditions), [scriptlets](/flows/actions/scriptlets), and [custom claims](/flows/actions/custom-claims). See [Connectors in Flows](/connectors/connectors-in-flows) for details. ##### Using an Existing Session Token If the user already has a session token (such as a custom JWT or opaque token stored in a cookie), the flow can reuse it to verify the user and issue a Descope JWT. 1. **Read the token** in the flow, for example via [`dynamic_val`](/getting-started/oidc-endpoints#passing-dynamic-values-to-a-flow) or automatically from a cookie when the flow runs on the same custom domain 2. **Validate the token** by calling your API with a [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) (e.g., `GET /userinfo` or `POST /introspect`) 3. **Map the response** (such as user ID or email) into the flow context 4. **Issue a Descope JWT** using the [Generate JWT](/flows/actions/generate-jwt) action The MCP client can then continue the [authorization code flow](https://oauth.net/2/grant-types/authorization-code/) using the returned Descope JWT. You can use the connector response value(s) in [conditions](/flows/conditions), [scriptlets](/flows/actions/scriptlets), and [custom claims](/flows/actions/custom-claims). See [Connectors in Flows](/connectors/connectors-in-flows) for details. # Calling External APIs from MCP Tools (/mcp/calling-apis-from-mcp) Learn how to configure your MCP server to exchange inbound tokens for downstream credentials using Descope STS. # Calling External APIs from MCP Tools When an MCP tool needs to call an authenticated downstream API (a third-party service like Google or Salesforce, or an internal API not directly exposed as a Descope Resource), your MCP server needs credentials scoped to that service, not the inbound token it received from the MCP client. This page explains how to set that up. ## Your MCP Server Is Both a Resource and a Client To broker downstream access, your MCP server plays two Descope roles at the same time. It is both a **[Resource](/resources)** and a **[Client](/agentic-identity-hub/core-components/clients)**: - **As a Resource** (the inbound side), it receives the MCP client's access token, whose `aud` claim is your MCP server's URL, and validates it. - **As a Client** (the outbound side), it authenticates to the [Descope STS](/api/third-party-apps/token-endpoint) with its own `client_id` and `client_secret` to exchange that token for a credential scoped to the downstream service. This is not redundant. The client ID serves a specific purpose: it tells the STS which system is making the exchange, so that: - Policy can be evaluated against the full context: the original user, the MCP client, and your MCP server - The audit log records the complete delegation chain: user → MCP client → MCP server → downstream service - Policies can target the MCP server specifically using `client.tags` or `client.name` Without it, the STS has no way to distinguish a legitimate server-side exchange from a client trying to fetch credentials directly. ## Setup ### 1. Register a Client for Your MCP Server In the Descope Console, go to [Clients](https://app.descope.com/agentic-hub/clients) and create a new client to represent your MCP server. - Enable the **Client Credentials** grant type - Disable any grant types the MCP server will not use - Copy the generated **Client ID** and **Client Secret** This client is separate from the MCP clients (Claude, Cursor, your users' agents) that connect to your server. It represents the server itself acting as an OAuth client toward the STS. ### 2. Configure Your MCP Server with the Credentials Store the client ID and secret securely in your MCP server's environment. These are the credentials it will use to authenticate token exchange requests. ### 3. Define What the MCP Server Can Reach Depending on which downstream services your tools call, configure either: - A **[Descope Resource](/resources)** for internal APIs that can validate Descope tokens directly; the STS will issue a resource-scoped token - A **[Connection](/agentic-identity-hub/core-components/connections)** for third-party services (Google, Slack, GitHub, etc.) or any service that requires its own OAuth token or API key; the STS will look up and return the stored credential ### 4. Create a Policy (if needed) If you want to restrict which users or MCP clients can trigger downstream credential access, create a [Policy](/agentic-identity-hub/policies) targeting the relevant [Connection](/agentic-identity-hub/core-components/connections) or [Resource](/resources). The policy is evaluated at exchange time against the user associated with the original inbound token and the client (MCP Server) context. ## Making the Exchange At tool execution time, your MCP server calls the token endpoint with: ```http POST /oauth2/v1/token Authorization: Basic {base64(client_id:client_secret)} Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token={inbound_access_token} &subject_token_type=urn:ietf:params:oauth:token-type:access_token &resource={resource_url_or_connection_id} ``` Descope validates the server's credentials, evaluates policy against the inbound token's claims, and returns either a resource-scoped Descope JWT or a stored credential from Connections. The Descope [Python MCP SDK](/mcp/sdks/python) handles this exchange automatically when you use the connection token retrieval helpers. ## Which Pattern to Use | Downstream service | Pattern | What the STS returns | | ------------------ | ------- | -------------------- | | Internal API defined as a Descope Resource | STS → Resource | A Descope JWT scoped to that resource | | Third-party OAuth service (Google, Slack, GitHub) | STS → Connection | The stored OAuth access token for that user or tenant | | API key-based service | STS → Connection | The stored API key for that user or tenant | Both patterns use the same token exchange request. The `resource` parameter determines which path the STS takes. ## What Shows Up in Audit Logs Every exchange produces an audit event that records: - The original user identity from the inbound token - The MCP client that initiated the tool call - The MCP server client ID that made the exchange request - The downstream resource or connection accessed - The policy decision applied This gives you a complete, traceable record of every downstream credential access: from the user who authorized it, through the agent that requested it, to the server that fetched it. # Model Context Protocol (MCP) (/mcp) Learn how to use Descope to secure and authorize Model Context Protocol (MCP) servers with inbound, outbound, and SDK-based flows. # Model Context Protocol (MCP) + Descope The Model Context Protocol (MCP) is an open standard that defines a secure, consistent, and interoperable way for AI models and agents to communicate with external systems (APIs, tools, files, and services). Descope integrates with MCP to provide a fully managed authentication and authorization layer for securing MCP servers and governing access to MCP tools. This page is for developers building an MCP **server** that needs authentication. If you are wiring an agent to *call* MCP servers, see the [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk) instead. ## About MCP MCP is the standard interface layer between AI systems and external resources. A good mental model: **MCP is like USB-C for AI**: a universal, plug-and-play protocol that replaces custom, one-off integrations with a consistent connection model. Instead of bespoke integrations for every AI system, MCP introduces a standardized plug-and-play model that works across: - AI agents and chat interfaces - Developer tools like IDEs and CLIs - Local and remote APIs, SaaS apps, and data services - Secure, multi-tenant environments with structured access control To learn more about MCP, refer to the official [MCP documentation](https://modelcontextprotocol.io/docs/getting-started/intro). ## How It Fits Together The MCP authorization spec splits auth across two entities: - **Resource server: your MCP server.** Built with the official MCP SDKs or a framework like [FastMCP](https://gofastmcp.com/integrations/descope). It serves tools and validates access tokens. - **Authorization server: Descope.** Descope publishes the OAuth discovery metadata, registers MCP clients (CIMD/DCR), runs login and consent, and issues tokens. With that split, your server implements only two concerns: 1. **Validate Descope-issued tokens** on every request: signature against your project JWKs, plus the `aud` and `scope` claims. Our [MCP server guide](/mcp/mcp-server) and [MCP SDKs](/mcp/sdks) handle this for you. 2. **Point clients at Descope**: host a `/.well-known/oauth-protected-resource` document that lists Descope as your authorization server, so MCP clients discover where to send users after a `401`. See [OAuth Protected Resource Metadata](/agentic-identity-hub/core-components/mcp-servers#oauth-protected-metadata-resource). Everything else (client registration, consent screens, token issuance, refresh) happens inside Descope. ## Why Use Descope with MCP Descope provides the identity and access control foundation required to deploy MCP securely at scale: ### 1. Keep up with the MCP authorization spec MCP authorization is a moving target. The spec keeps adding requirements that are non-trivial to build and maintain yourself. Descope keeps your MCP server current by configuration rather than a rebuild, including: - **[Dynamic Client Registration (DCR)](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr)** and **[Client ID Metadata Documents (CIMD)](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)** so MCP clients like Claude, Cursor, and VS Code register with your server automatically, with **custom logic on registration** (e.g., assign tags like `sales-agent` for policy decisions). - **[Enterprise-Managed Authorization](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags)** (Cross App Access / XAA): let customers manage their agents with their own workforce IdP (Okta, Entra) using **XAA tokens**, with no extra consent. - New authorization requirements as the spec evolves, adopted through settings instead of code changes. ### 2. OAuth 2.1 Authorization for MCP Servers - MCP authorization is built on **OAuth 2.1** - Descope handles authentication, user consent, scope validation, SSO, social OAuth login, and other modern auth methods - You can configure your MCP server inside Descope and declare **OAuth scopes per tool** ### 3. Secure Token & Secret Management for MCP Tools - Store OAuth tokens, refresh tokens, static API keys, and external credentials using [Connections](/agentic-identity-hub/core-components/connections) - MCP tools retrieve credentials securely at runtime, avoiding hardcoded secrets ### 4. Granular OAuth Permissions and Policy Enforcement - Define [access policies](/agentic-identity-hub/policies) that map: - Client or agent attributes (e.g., tags, roles, metadata) - OAuth token claims to specific tool permissions and allowed scopes - Create granular OAuth [scopes](/resources) for agents to hold and end users to consent to, whether tool-level or broader. These can be **finer-grained than the permissions the downstream services behind your MCP server actually expose**, so you can grant, consent to, and audit exactly what an agent may do even when the underlying API only offers coarse access. - Ensures only approved clients or agents can call sensitive MCP tools ## B2C and B2B companies Descope supports MCP servers for both consumer and enterprise audiences from the same project. The auth problems differ, so here is what each typically needs. ### B2C You expose an MCP server to individual consumers, often to **get listed on the Claude or ChatGPT marketplaces**, where proper auth on your MCP server is a requirement. E-commerce platforms adopting agentic commerce protocols such as UCP and ACP fall in the same bucket: an agent is transacting on a real customer's behalf, so the server has to know who that customer is. - **Enable MCP auth quickly**: Descope handles OAuth 2.1, [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) and [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd), and proper user management and consent out of the box. If you already have an auth system, use [Bring Your Own Auth](/mcp/bring-your-own-auth) instead of migrating your users. - **Tool-level scopes with real user consent**: define [scopes per tool](#scoping-with-mcp-servers) and a [consent flow](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) so users approve exactly what an MCP client may do. - **Identity linking instead of an unauthenticated server**: when tools need user context (their cart, their orders, their data), link the MCP session to the user who already exists in your product, so every tool call carries a real identity rather than serving anonymous traffic. Start with the [MCP server guide](/mcp/mcp-server), or [Bring Your Own Auth](/mcp/bring-your-own-auth) if you already have an auth system. ### B2B You are building an MCP server to enable other enterprises. Everything above still applies. In this scenario, each customer is usually a [Tenant](/management/tenant-management), typically with its own SSO and its own access rules. - **[Cross App Access](#cross-app-access-xaa)**: let each customer's users and agents reach your server through the workforce IdP they already run (Okta, Entra), with no second login or consent screen. - **SSO groups to roles to policies**: [map each customer's SSO groups to Descope roles](/sso/sso-mapping), then use those roles in [policies](/policies) to decide which scopes each user's agents receive, per tenant. - **Multi-tenant isolation**: see the [multi-tenant calendar MCP server example](/mcp/examples/multi-tenant-calendar) for tenant-scoped SSO, roles, and tool scopes end to end. #### Cross App Access (XAA) This is specifically referring to the [Cross App Access (XAA)](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works) feature, where you sell an MCP server and accept customers' ID-JAG tokens. An enterprise buying your product already has an identity provider, and their IT team already decides who may use what. Cross App Access (XAA) lets them extend that same decision to the agents reaching your MCP server, instead of every employee approving a separate consent screen on your product. The customer registers their workforce IdP (Okta, Ping, etc.) as a trusted issuer on their tenant. When one of their agents calls your server, that IdP mints a short-lived assertion (an **ID-JAG**) saying this user, through this client, may reach your resource. Descope validates it against the issuer registered for that tenant and returns an ordinary access token for your server. Two things follow from that, and they are usually what closes the enterprise deal: - **Your MCP server does not change.** It keeps validating a normal Descope access token. Nothing in your tool code needs to understand ID-JAG. - **The customer configures it themselves.** The [Cross App Access section of the SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration) sits alongside SSO and SCIM, so their admin sets it up in the same session without a support ticket. Trust is scoped per tenant, so an assertion minted by one customer's IdP can never reach another customer's data. [Policies](/agentic-identity-hub/policies) then decide which scopes the resulting token carries. See [our guide](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags) for the full setup. ## Basic Authentication and Authorization with MCP You can use Descope as the authentication and authorization layer for **any MCP server**, including custom implementations or frameworks like [FastMCP](https://gofastmcp.com/integrations/descope), by leveraging Descope's OAuth 2.1 discovery endpoint. ### MCP SDKs Descope ships [server-side MCP SDKs](/mcp/sdks) that handle token validation (signature, `aud`, `scope`), the required metadata endpoints, and per-tool scope enforcement: - **[Express SDK](/mcp/sdks/express)** (`@descope/mcp-express`): drop-in middleware that ships an authenticated `/mcp` endpoint and tool registration with scopes. - **[Python SDK](/mcp/sdks/python)** (`descope-mcp`): token validation, Connection token retrieval, and FastMCP integration. You can read more about how to configure your MCP server with Descope in our [MCP Server doc](/agentic-identity-hub/core-components/mcp-servers). ### Scoping with MCP Servers Scopes are **not mandatory** for use with your MCP server. You can define basic authentication, without authorization via scopes. MCP authorization follows [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1), where the MCP server acts as a [Resource](/resources): a resource server that enforces permissions in the access token. Scopes are the standard way to express those permissions. Adding scopes to your MCP server enables: - Granular tool access (e.g., read-only vs. write actions) - Protection of sensitive MCP tools - Least-privilege enforcement across users, agents, and tenants - Auditable and revocable tool permissions Without scopes, access becomes **all-or-nothing**, making fine-grained tool authorization impossible. For granular authorization, it is recommended to define **one scope per MCP tool or tool group**. These scopes are issued in MCP client access tokens and let your MCP server verify which tools an AI agent or MCP client is allowed to invoke. Example tool-level scopes: - `mcp:invoice.create` → Permission to run invoice-generation tools - `mcp:calendar.write` → Permission to create or modify calendar events - `mcp:calendar.read` → Read-only access to calendar queries Scopes also require a human-friendly description when configured, which helps end users understand exactly what access an MCP client or agent is requesting. ### Calling External APIs from Tools Use this pattern only when an MCP tool needs credentials to call an external API, whether **OAuth tokens** or **API keys**. If your MCP tools **do not** call external APIs that require authentication, you can skip this section and rely on MCP [tool scopes](#scoping-with-mcp-servers) alone. When your MCP tools **do** call authenticated external APIs, Descope stores those credentials in [Connections](/agentic-identity-hub/core-components/connections): one vault shared across all your MCP servers and APIs, so nothing is hardcoded and you never write refresh logic. It holds: - **OAuth tokens per user or tenant**, each with its own scopes and automatic refresh, including multiple tokens for the same third-party service. - **API keys for non-OAuth services**, fetched at runtime. This works through token exchange. The MCP client connects with a Descope access token, and when a tool runs, your server exchanges that token with Descope for whatever the tool needs: an API key or OAuth token from the Connections vault, or **another Resource token** if the target is an API you have defined as a [Resource](/resources) in Descope. Your MCP server performs the exchange using Descope SDKs at runtime; the MCP client never holds the downstream credential. ### Policies for MCP Scope Authorization Descope [Access Control Policies](/agentic-identity-hub/policies) let you define which MCP clients or AI agents are allowed to request or use specific MCP server scopes (tools). - You can create **multiple policies** to govern different MCP client types and agent groups. - Policies evaluate requests using attributes such as: - **MCP client identity** (e.g., Claude, Cursor, etc. ) - **Agent tags** (e.g., `sales-agent`, `support-agent`) - **User roles/attributes** (e.g. associated user roles, permissions, tenants) - **Custom claims** (claims in client access token) - This allows you to grant **tool-level OAuth scopes** based on who the agent/client is, what group or tag they belong to, and where they're operating. Policies and [user consent](#scoping-with-mcp-servers) are two levels of control: policies are what an IT admin allows in the Descope Console, and consent is what the end user approves. Policy wins; scopes that no policy permits **never appear on the consent screen**, so users can only consent within what admins have allowed. See [Policies and User Consent](/policies#policies-and-user-consent). ## Example MCP Servers ### Multi-Tenant Calendar MCP Server See our detailed example of building a [Calendar MCP Server with SSO and tool-level scoping](/mcp/examples/multi-tenant-calendar) that demonstrates: - Restricting access to registered MCP clients - SSO authentication with Okta or Azure AD - Tool-level scope authorization ### Multi-Tenant MCP Server with Tenant Switching See our example of building a [B2B MCP Server](/mcp/examples/b2b-mcp-server) for users who belong to many tenants, that demonstrates: - Signing in once with one MCP URL and selecting a tenant in the consent flow - Switching the active tenant mid-session with a `switch_tenant` tool (no reconnect) - Using [token introspection](/sessions/introspection) to read the live active tenant on every tool call - Dynamically updating `tools/list` based on the active tenant For a list of example MCP servers, please refer to our GitHub [repository](https://github.com/descope/ai/tree/main/examples). # Descope MCP Server (/mcp/mcp-server) Use the Descope MCP Server to manage your Descope project and search documentation from any MCP-compatible AI agent. # Descope MCP Server The Descope MCP Server lets you manage your Descope project from any MCP-compatible AI agent. Ask your agent to search users, configure flows, inspect audit logs, manage tenants, and more, without leaving your IDE or chat interface. Documentation search and Q&A are built in. ## Server URLs Use the MCP server URL that matches where your Descope company and projects are hosted: | Deployment | MCP server URL | | --- | --- | | US (default) | `https://mcp.descope.com` | | EU | `https://mcp.euc1.descope.com` | If you run a [private cloud deployment](/how-to-deploy-to-production/private-cloud) of Descope, the MCP server is not available at a public regional URL by default. Contact [Descope Support](/support) to enable the Descope MCP Server for your environment. The connection examples below use the US endpoint. If your projects are in the EU region, substitute `https://mcp.euc1.descope.com` wherever a URL is shown. See [Multi-Region Support](/management/project-settings/multi-regional) if you're unsure which region your projects use. ## Connecting to the MCP Server Or add the following to your `~/.cursor/mcp.json` manually: ```json { "mcpServers": { "descope": { "url": "https://mcp.descope.com" } } } ``` Restart Cursor if the server does not appear. Or add a `.vscode/mcp.json` in your project root: ```json { "servers": { "descope": { "type": "http", "url": "https://mcp.descope.com" } } } ``` 1. Open **Settings → Connectors** 2. Click **Add Connector** and enter: `https://mcp.descope.com` 3. Sign in with your Descope account when prompted. ```bash claude mcp add --transport http descope https://mcp.descope.com ``` ```bash codex mcp add descope --url https://mcp.descope.com ``` Verify it's configured: ```bash codex mcp list ``` Or add it directly to `~/.codex/config.toml`: ```toml [mcp_servers.descope] url = "https://mcp.descope.com" ``` Add the following to your `opencode.jsonc`: ```json { "$schema": "https://opencode.ai/config.json", "mcp": { "descope": { "type": "remote", "url": "https://mcp.descope.com", "enabled": true } } } ``` 1. Open **Windsurf Settings** and search for **MCP** 2. Click **View raw config** to open `mcp_config.json` 3. Add the Descope server: ```json { "mcpServers": { "descope": { "serverUrl": "https://mcp.descope.com" } } } ``` 4. Save and restart Windsurf. 1. Go to [Settings → Connectors](https://chatgpt.com/#settings/Connectors) 2. Click **Add Connector** and enter: `https://mcp.descope.com` 3. Sign in with your Descope account when prompted. 1. Open [Connectors](https://www.perplexity.ai/account/connectors) in your Perplexity account settings. 2. Click **Add connector** (or **+ Custom connector**) and choose **Remote**. 3. Enter a name (for example, `Descope`) and the MCP server URL: `https://mcp.descope.com` 4. Sign in with your Descope account when prompted, then save the connector. 5. In a thread, enable the connector under **Sources** so Perplexity can call Descope tools. Custom remote connectors require a Perplexity plan that includes them. For the full flow, see [Perplexity's MCP connector guide](https://www.perplexity.ai/help-center/en/articles/11502712-local-and-remote-mcps-for-perplexity). Once connected, the server discovers your available Descope projects and prompts you to select one. You can switch projects at any time; just ask your agent to select a different project. ## Security Model Sessions start in **read-only mode**. Searching users, listing tenants, inspecting flows, and querying audit logs are all available immediately. Write operations require explicit elevation. Ask your agent to elevate the session when you want to make changes. The write window is time-bounded and closes automatically, returning the session to read-only mode. ![write operations example mcp server](/assets/write-operations-example-mcp-server.webp) This means an AI agent browsing your project cannot make changes unless you explicitly grant it the ability to do so for that session. ## Cross-App Access (XAA) The Descope MCP Server can also accept [Cross-App Access](/agentic-identity-hub/enterprise-managed-authorization) assertions, so your agents reach it through the identity provider your organization already runs instead of signing in to Descope separately. This is the same arrangement a B2B customer sets up for their own MCP server, with the roles reversed. Descope operates the MCP server and acts as the **validator**, and your IdP is the **issuer** whose ID-JAGs Descope accepts for your company. Your IdP can be Okta, Entra, another workforce IdP that supports ID-JAG, or your own Descope project. ### Enable Cross-App Access Our Descope MCP Server supports Cross App Access (XAA), so you can enable this as an MCP server for specific users within your organization, in accordance with your centralized XAA policies. To enable XAA for the Descope MCP Server: 1. Open [Company Settings](https://app.descope.com/settings/company/settings) and go to [Console Access](/management/company-settings#console-access). 2. Click **SSO Configuration for Console Access** to open the SSO Setup Suite. 3. Follow the steps under [Cross App Access (XAA) Configuration](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration). ## Tools The MCP server exposes the following tools. Each tool groups a set of related operations. Use `list_operations` to discover the full operation catalog at any time. | Tool | Description | | --- | --- | | `list_operations` | Discover the full operation catalog; list all operations by bucket, or fetch the input/output schema | | `session` | Manage session context: switch projects, check current identity, generate onboarding plans, and elevate to write mode | | `project_read` | View project configuration including JWT templates, lists, snapshots, and messaging localization | | `project_write` | Update project settings, clone projects, manage JWT templates, lists, Descopers, and messaging localization | | `access_control_read` | Query FGA schemas, roles, permissions, ReBAC relations, and run authorization checks and dry-runs | | `access_control_write` | Create and update FGA schemas, roles, permissions, and relations; manage backups and resource details | | `agentic_read` | View MCP server definitions, clients, and client secrets | | `agentic_write` | Create, update, and delete MCP servers and clients; rotate client secrets | | `audits_read` | Search audit events and analytics | | `auth_keys_read` | View access key details and password settings | | `auth_keys_write` | Create, search, and manage access keys; configure password settings; update JWTs; and impersonate users | | `flows_read` | View flows, flow templates, themes, flow localization, and widgets | | `flows_write` | Import and manage flows, themes, and flow localization; apply project themes | | `tenants_read` | View tenants, tenant settings, and SSO admin link state | | `tenants_write` | Create and manage tenants, tenant settings, default roles, and SSO admin links | | `connect_read` | View SSO settings, IDP apps, inbound apps, outbound apps, and third-party apps | | `connect_write` | Create and manage SSO applications (OIDC, SAML, WS-Fed), inbound/outbound apps, and SSO tenant settings | | `tests_read` | Search test users | | `tests_write` | Create and delete test users; generate test OTPs, magic links, and enchanted links | | `users_read` | View user records, custom attributes, group membership, trusted devices, and auth history | | `users_write` | Create, update, and delete users; manage credentials (passwords, passkeys, TOTP), and custom attributes | | `docs_search` | Semantic search across Descope documentation and SDK references | | `docs_ask_question` | Ask natural-language questions about Descope and get answers grounded in official documentation | ## What You Can Do The server covers the full Descope Management API: users, tenants, flows, access control, connections, audit logs, auth keys, and more. Use natural language: you don't need to know specific operation names. ### Example Prompts **Users and tenants** - *"List all users in the engineering tenant"* - *"What SSO connections are configured for tenant acme-corp?"* - *"Show me all users who haven't logged in for 30 days"* **Flows and configuration** - *"What flows do I have configured and which one is the default sign-in flow?"* - *"Show me the current FGA schema"* - *"What access keys exist in this project and when do they expire?"* **Audit** - *"Show me the audit log for the last 24 hours filtered by login events"* - *"Were there any failed authentication attempts in the last hour?"* **Write operations** (require elevation) - *"Create a test user with email test@example.com"* - *"Add the 'admin' role to user alice@example.com in the acme-corp tenant"* - *"Update the default session duration for this project"* ## Documentation Tools The server includes two tools for Descope product knowledge: **`docs_search`**: semantic search over Descope documentation and SDK references. Good for finding reference content, configuration details, and code examples. **`docs_ask_question`**: ask a natural language question about Descope and receive a grounded answer. Good for troubleshooting, conceptual questions, and understanding what Descope supports. Your agent picks the right tool based on what you ask. You can mix project management and documentation questions in the same conversation. ### What You Can Ask **Troubleshooting** - "Why am I getting 'Invalid session token' when validating a JWT in my API?" - "Magic link sign-in works in dev but fails in production, what should I check?" - "How do I fix CORS errors when embedding the Descope flow?" **Setup and configuration** - "How do I add Google OAuth to my Descope flow?" - "How do I map SSO groups to Descope roles?" - "What are the steps to configure an MCP server in Descope?" **SDK and API** - "How do I get the session token in the React SDK?" - "How do I validate a Descope JWT in a Lambda authorizer?" - "What's the Management API call to create a user?" **Architecture** - "Design my backend: session validation on every API route plus role checks so only admins can access /api/settings. I'm using Next.js App Router and the Node SDK." - "I'm building a multi-tenant app. Walk me through tenant resolution, attaching tenant to the session, and enforcing tenant-scoped role checks in my API." - "How do I implement step-up authentication for a sensitive action?" ### Tips for Better Results - **Be specific**: "How do I validate a JWT in AWS API Gateway?" is better than "How do I validate JWTs?" - **Include context**: mention your framework (Next.js, React, Python), auth method (magic link, OAuth, SSO), or environment (serverless, edge) when relevant - **Paste exact errors**: for troubleshooting, include the error message or code snippet - **One topic per question**: ask one clear question at a time; follow-ups work well ## Descope Skills For multi-step workflows (integrating auth, migrating from Auth0 or Okta, building BYOS screens, managing FGA schemas, or running security reviews), install [Descope Skills](/ai-assistants/skills). Skills complement this MCP server with structured agent instructions and guardrails. # Step-up Authentication (/mfa-and-step-up/step-up) Add layered security to your app utilizing Step-up authentication. # Step-up Authentication Step-up authentication is a security mechanism that allows you to add additional authentication requirements for sensitive operations in your application. It works by requiring users to re-authenticate when accessing high-risk features, even if they're already logged in. ## How Step-up Authentication Works When a user needs to perform a sensitive operation (like making a purchase or accessing personal data), you can trigger step-up authentication. This requires the user to verify their identity again, typically using a stronger authentication method than their initial login. After successful step-up authentication, the user's session token is updated with a `su` (step-up) claim set to `true`. This allows your application to verify that the user has completed the additional authentication step. This stepped-up token will be valid for the duration of the [Step Up Token Timeout](/management/project-settings#step-up-token-timeout) defined in Project Settings. ```json { "amr": ["xxxx"], "drn": "xx", "exp": xxxxx, "iat": xxxxx, "iss": "xxxxx", "rexp": "xxxxx", "su": true, "sub": "xxxxx" } ``` ## Use Cases Step-up authentication is particularly useful for: - Financial transactions (e.g., making purchases, transferring money) - Accessing sensitive personal information - Administrative operations - Changing account settings - Any high-risk operation that requires additional verification ## Implementation Options You can implement step-up authentication using our Flows, Client SDKs, or Backend SDKs. ### Option 1: Using Descope Flows Descope provides a pre-built `step-up` flow that you can easily integrate into your application. This flow: 1. Loads the user from their refresh token 2. Marks the flow as a step-up authentication 3. Presents authentication options (magic link, passkeys, social login) 4. Updates the session token upon successful authentication ![Default step-up flow](/assets/step-up-flow-overview.webp) You can integrate a step-up flow in your application in the same way you would [integrate a regular authentication flow](/getting-started). After the step-up flow succeeds, the user's JWT will be updated with the [`su` claim.](/mfa-and-step-up/step-up#how-step-up-authentication-works) The authentication options in a step-up flow aren't limited to native Descope methods. If a user is authenticated through a homegrown or third-party IdP instead, a [Generate JWT action](/flows/actions/generate-jwt) placed after the Step Up action also issues a token with the `su` claim. See [Step-Up Authentication for Third-Party IdP Sign-Ins](/flows/use-cases/step-up-with-generate-jwt) for details. ### Option 2: Using Client SDKs To perform step-up authentication using our Client SDKs, you use the same "Sign In" or "Sign Up Or In" functions as you would for regular authentication. You just have to specify that you are performing step-up using the Login Options parameter. The below example implements step-up authentication via [OTP Sign-In](/auth-methods/otp/with-sdks/client#user-sign-in) after the user has already authenticated using another authentication method. In `loginOptions`, `stepup` is set to true, indicating that this is a step-up authentication action. On success of the sign in function, the user's JWT will include the `su` claim. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions: login options for MFA, stepup, or custom claims. Ex: {stepup: true, mfa: false, customClaims: {}} const loginOptions = {stepup: true} // token: refresh token from the successful sign-in of the user const token = "xxxxxx" const resp = await descopeClient.otp.signIn[deliveryMethod](loginId, loginOptions, token); if (!resp.ok) { console.log("Failed to initialize step-up flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized step-up flow") } ``` ### Option 3: Using Backend SDKs To perform step-up authentication using our Backend SDKs, you use the same "Sign In" or "Sign Up Or In" functions as you would for regular authentication. You just have to specify that you are performing step-up using the Login Options parameter. The below example implements step-up authentication via [OTP Sign In](/auth-methods/otp/with-sdks/backend#user-sign-in) after the user has already authenticated using another authentication method. In `loginOptions`, `stepup` is set to true, indicating that this is a step-up authentication action. On success of the sign in function, the user's JWT will include the `su` claim. ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginId: email or phone - email or phone - the loginId for the user const loginId = "email@company.com" // loginOptions: login options for MFA, stepup, or custom claims. Ex: {stepup: true, mfa: false, customClaims: {}} const loginOptions = {stepup: true} // token: refresh token from the successful sign-in of the user const token = "xxxx" var resp = await descopeClient.otp.signIn[delivery_method](loginId, loginOptions, token); if (!resp.ok) { console.log("Failed to initialize step-up flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized step-up flow") console.log(resp.data) } ``` ```python # Args: # delivery_method: Delivery method to use to send OTP. Supported values include DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # login_id: email or phone - email or phone - the loginId for the user login_id = "email@company.com" # login_options (LoginOptions): login options for MFA, stepup, or custom claims. Ex: LoginOptions(stepup: True, mfa: False, customClaims: {}) login_options = LoginOptions(stepup=True) # refresh_token: refresh token from the successful sign-in of the user refresh_token = "xxxxx" try: resp = descope_client.otp.sign_in(method=delivery_method, login_id=login_id, login_options=login_options, refresh_token=refresh_token) print ("Successfully initialized Step-up flow") except AuthException as error: print ("Failed to initialize Step-up flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: Optional login options for MFA, stepup, or custom claims. loginOptions := &descope.LoginOptions{Stepup: true} err := descopeClient.Auth.OTP().SignIn(ctx, deliveryMethod, loginID, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize step-up flow: ", err) } else { fmt.Println("Successfully initialized step-up flow") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var loginOptions = LoginOptions.builder() .stepUp(true) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String maskedAddress = otps.signIn(DeliveryMethod.EMAIL, loginId, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ## Step-Up Validation To validate that the session was successfully stepped up, you can utilize the Backend SDKs to validate the session and check that the `su` claim is `true`. ```javascript // Args: // sessionToken (str): The session token, which contains the signature that will be validated const sessionToken="xxxx" try { const authInfo = await descopeClient.validateSession(sessionToken); console.log("Successfully validated user session:"); console.log(authInfo); if ("su" in authInfo.token) { console.log("Session is stepped up."); } } catch (error) { console.log ("Could not validate user session " + error); } ``` ```python # Args: # session_token (str): The session token, which contains the signature that will be validated session_token="xxxx" try: jwt_response = descope_client.validate_session(session_token=session_token) print ("Successfully validated user session:") print (jwt_response) if "su" in resp: if resp["su"] == True: print("Session is stepped up.") else: print("Session is not stepped up.") except Exception as error: print ("Could not validate user session. Error:") print (error) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // sessionToken (str): The session token, which contains the signature that will be validated sessionToken = "xxxx" authorized, userToken, err := descopeClient.Auth.ValidateSessionWithToken(ctx, sessionToken) if (err != nil){ fmt.Println("Could not validate user session: ", err) } else { fmt.Println("Successfully validated user session: ", userToken, authorized) val, ok := userToken.Claims["su"] if (ok == true && val == true) { fmt.Println("Session is stepped up.") } } ``` ```java // Validate the session. Will return an error if expired AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { Token t = as.validateSessionWithToken(sessionToken); Boolean su = (Boolean) t.getClaims().get("su"); if (su != null && su) { System.out.println("Session is stepped up."); } } catch (DescopeException de) { // Handle the unauthorized error } ``` ## Resources The B2C Retail Sample App, [Peek-a-Box](https://www.peek-a-box.shop/) demonstrates a practical implementation of step-up authentication. Users can browse products and add items to their cart with basic authentication. When the user proceeds to checkout, step-up authentication is required. Check out our [learning center article](https://www.descope.com/learn/post/step-up-authentication) for more examples, use cases, and guidelines on implementing step-up authentication. # SSO Login Flows (/sso/idp-initiated) SP-initiated and IdP-initiated SSO login flows in Descope, and how to configure IdP-initiated SSO. # SSO Login Flows An SSO login can start in two places: on your app (**SP-initiated**) or at the customer's identity provider (**IdP-initiated**). Most B2B apps need both. The diagram below compares them. ## SP-Initiated The user starts on your sign-in page, gets redirected to their IdP, authenticates, and comes back to your app. This is the flow you'll test first, and what [Getting Started with SSO](/auth-methods/sso/getting-started) sets up. ## IdP-Initiated The user starts in their IdP (Okta app tile, Entra My Apps, and so on), picks your app, and lands in your product already authenticated. Developers often ship SP-initiated only and discover IdP-initiated the hard way when a customer IT admin "just clicks the tile." When Descope receives an IdP-initiated SAML assertion, it validates it and then runs an internal SP-initiated code exchange, so your callback / SDK exchange path stays the same as normal SSO. You can't sign a user out of Descope using Single Logout (SLO) from a tenant's SSO provider. See [SAML Security](/security-best-practices/saml-security#single-logout-slo) for how to handle logout. If you use `enforceInitiatedEmail` (SDKs) or the flow action's **Verify initiated email matches IdP response** toggle (see [SSO with Flows](/auth-methods/sso/with-flows#verify-initiated-email-matches-idp-response)), it has no effect on IdP-initiated logins. These logins never go through your `sso.start`, so there's no email on your side to compare against the IdP's response. ### What You Must Configure IdP-initiated needs a **Post Authentication Redirect URL**, the page Descope sends the user to after it finishes the internal exchange. Without it, login fails with `E061206` (missing redirect URL for IdP-initiated login). Set it at: - **Project level:** [SSO Auth Method settings](https://app.descope.com/settings/authentication/sso), which supports dynamic URLs if you use different domains per tenant - **Tenant level**: the tenant's SSO configuration (or the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite)) That URL should be an HTTPS page in production that either runs a Descope Flow or exchanges the `code` with your SDK. ![Post Authentication Redirect URL for IdP-initiated SSO](/assets/idp-initiated-post-auth-url.webp) ### With Flows 1. Add a **condition** that detects IdP-initiated. If true, complete login automatically. If false, show your normal sign-in screen for SP-initiated SSO. ![IdP-initiated condition in a Descope flow](/assets/sso-idp-initiated-flow.webp) ![Example flow that handles IdP-initiated SSO](/assets/sso-idp-initiated-flow-2.webp) 2. Point the Post Authentication Redirect URL at a page that hosts that flow. ### With SDKs Point the Post Authentication Redirect URL at a page that reads the `code` query parameter, then exchange it the same way as SP-initiated SSO: - [Backend SDK](/auth-methods/sso/with-sdks/backend#step-2-exchange-the-code) - [Client SDK](/auth-methods/sso/with-sdks/client#step-2-exchange-the-code) - [Mobile SDK](/auth-methods/sso/with-sdks/mobile#sso-exchange-code) ### Test Both Paths 1. **SP-initiated**: sign in from your app with a test user. 2. **IdP-initiated**: from the IdP's app portal, launch your app (the customer assigns the app in Okta / Entra / etc.). 3. Confirm both land authenticated with the right tenant and roles ([SSO mapping](/sso/sso-mapping)). 4. Check [Audits](https://app.descope.com/audits) for `LoginSucceeded` (and failures if something's off). ### Troubleshooting IdP-Initiated | Symptom | Likely cause | | ------- | ------------ | | `E061206` | Post Authentication Redirect URL not set for the project or tenant | | User lands on wrong page / blank | Redirect URL doesn't host your flow or code-exchange page | | Works from your app, fails from IdP tile | IdP-initiated never configured; only SP-initiated was tested | | Assertion / cert errors | IdP cert or metadata out of date; [certificate rotation](/management/tenant-management/sso/cert-and-metadata-rotation) | More codes: [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting). ## Related - [Getting Started with SSO](/auth-methods/sso/getting-started) - [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) - [SAML Security](/security-best-practices/saml-security) - [SSO tutorials](/sso/tutorials) (IdP-initiated video) # Just-in-Time (JIT) Provisioning (/sso/jit-provisioning) Enable or disable SSO JIT provisioning in Descope, understand how it creates and updates users on login, and when to use SCIM instead. # Just-in-Time (JIT) Provisioning With **JIT provisioning** enabled on a tenant’s SSO connection, Descope creates the user on first SSO login and refreshes mapped attributes and groups on later logins. You don’t have to pre-create every account before someone signs in. JIT is configured **per SSO connection** on the tenant (SAML or OIDC), under **Authentication Methods → SSO**. ![How to enable or disable Just-in-Time (JIT) provisioning in Descope](/assets/jit-provisioning-tenant.webp) ## What Descope Does When JIT Is On On a successful SSO login: 1. If the user doesn’t exist yet, Descope **creates** them from assertion attributes (email, name, and anything you’ve mapped). 2. If the user already exists, Descope **updates** mapped attributes and group → role assignments from the latest assertion. 3. If no mapped roles apply, Descope can still assign **Default Roles** configured in the Roles & Groups tab for that tenant. That update-on-login behavior is why JIT is useful even after the first signup, as long as people keep logging in via SSO. ## JIT vs SCIM | | JIT | SCIM | | - | --- | ---- | | When users appear | On first (and later) SSO login | When the IdP pushes create/update/deactivate | | Attribute / group updates | On each SSO login | Continuously from the directory | | Deprovisioning | Not automatic; the user can remain until you remove them or use SCIM | IdP can deactivate / remove users | | Best for | Fast SSO rollouts, small orgs, or “login creates the account” | Enterprises that require hire/fire sync without waiting for login | You can run both: SCIM for lifecycle, JIT for attribute refresh on login (or turn JIT off if SCIM should be the only writer). See [SCIM](/management/tenant-management/scim) and [SCIM best practices](/management/tenant-management/scim/scim-best-practices). ## How to Enable or Disable JIT 1. Open the [Tenants](https://app.descope.com/tenants) page and select the tenant. 2. Go to **Authentication Methods → SSO** (and pick the right SSO profile if you use [multiple IdPs](/sso/multi-sso)). 3. Toggle **JIT Provisioning** on or off. Your customer’s IT admin can also change this in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). Manual field reference: [SAML](/auth-methods/sso/saml) and [OIDC](/auth-methods/sso/oidc) configuration. ## Mapping That JIT Relies On JIT only knows what you map from the IdP: - **User attribute mapping.** Email, name, phone, and custom attributes. - **Group and role mapping.** IdP groups to Descope roles (and optional FGA mappings). - **Default roles.** Applied when no group mapping matches. Configure those in the Console, Setup Suite, or Management API. Details: [SSO user and group mapping](/sso/sso-mapping). ## Security Notes - Restrict **SSO domains** so only intended emails can hit this connection. - Prefer strict group → role maps over broad default roles in production. - If users might already exist from password/OTP, read [Merging SSO identities](/sso/merging-sso-identities-risk) before go-live. - Confirm creates and updates in [Audits](https://app.descope.com/audits) (`LoginSucceeded` and related SSO fields). See [SSO audit fields](/audit-trails-and-integrations/audit-events#sso-related-fields). ## Related - [SSO mapping](/sso/sso-mapping) - [SCIM](/management/tenant-management/scim) - [Getting Started with SSO](/auth-methods/sso/getting-started) - [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting) # Risks in Merging SSO and Non-SSO Identities (/sso/merging-sso-identities-risk) Learn about the risks of merging SSO users with non-SSO identities, including security implications and best practices for identity management. # Risks in Merging SSO and Non-SSO Identities In Descope, a user identity can be associated with one or more authentication methods. All non-SSO methods, such as password, magic link, OTP, or passkey, can share the same login ID (typically an email or phone number), so a single user can authenticate through multiple non-SSO methods seamlessly. However, **SSO-based authentication** works differently. When a user logs in via SSO, their identity is tied to the unique tenant ID (associated with their external SSO IdP), which differs from their regular login ID (e.g., email). As a result, logging in with both SSO and non-SSO methods can result in **separate user records** unless explicitly linked. While it may seem convenient to let a user sign in through both SSO and non-SSO methods, doing so without proper identity linking can introduce significant **security**, **compliance**, and **identity management** challenges. ## Why Mixing SSO and Non-SSO Is Problematic SSO is designed to be a centralized, organization-controlled authentication mechanism. When a user signs in with SSO (e.g., Google Workspace, Microsoft Entra, Okta), their access is governed by your identity provider (IdP), often with additional policies like MFA, session control, group-based access, and more. Allowing that same user to bypass SSO using an alternative login method (like password, OTP, or magic link) introduces serious risks: #### 1. Bypassing Enterprise Controls: If an attacker gains access to the user's email or weak password, they could log in outside the organization's SSO and bypass: - Enforced MFA - Conditional access policies - Session or device restrictions #### 2. Loss of Central Revocation: Revoking access via the IdP (e.g., deactivating the employee in Google or Entra) becomes ineffective, as the user could still log in via the non-SSO path. #### 3. Audit and Compliance Confusion: Split identities and inconsistent authentication paths lead to: - Fragmented audit trails - Complications in identity governance or SCIM provisioning #### 4. Unintended Account Takeover: If the same user email is used for both SSO and non-SSO flows, automated login mechanisms may create unexpected outcomes. For example, a user expecting to log in via SSO may accidentally create a non-SSO account if fallback flows aren't tightly controlled. ## How Descope Handles SSO Users In Descope, SSO users are distinct identities linked to their SSO provider configuration. When a user logs in via SSO, Descope identifies them using the IdP metadata and assigns them a user ID specific to that SSO flow. By default, Descope does not merge SSO users with identities created through other authentication methods (e.g., email, phone, social logins). This separation ensures: - Consistent login behavior - Alignment with your IdP policies - Safer session management - Easier identity isolation for auditing and access reviews Even if a user signs in using the same email address via SSO and via a non-SSO method, they will be treated as separate users in Descope unless explicitly linked (which is not recommended). ## When It Might Make Sense to Allow a Backup Non-SSO Login In rare scenarios, you may want to enable fallback login methods for specific users such as tenant or organization administrators, in case the configured SSO (Single Sign-On) provider becomes temporarily unavailable or misconfigured. By default, if **SSO is enforced**, users are only permitted to authenticate through the configured SSO provider. However, when **SSO is not enforced**, users can authenticate using any supported method (e.g., OTP, magic link, password), including the fallback options. ![Condition for checking if SSO is enforced](/assets/sso-enforced.webp) ### Converting Existing User to SSO When the **Convert existing user to SSO** setting is enabled, Descope can automatically merge an incoming SSO or SCIM user with an existing user in the tenant. This process is guarded by multiple safeguards to prevent accidental or malicious account takeover. ![SSO Convert and Identity Merging Behavior](/assets/sso-convert-users.webp) An existing user is converted to an SSO user if and only if the following conditions are met: - The incoming SSO user's login ID or email matches the existing user's identifier (excluding the tenant ID suffix). - The existing user must belong to exactly one tenant. This safeguard exists to prevent unintended identity merges when a user account is shared across multiple tenants. If either of these conditions is not met, Descope will not automatically merge the identities, and a new user will be created instead. #### Post-Conversion Behavior for SSO Users - The Identity Provider (IdP) becomes the single source of truth for the user. - User attributes, groups, and roles are ingested from the IdP according to the tenant's SSO mapping configuration. - Any existing roles that were not assigned via IdP mapping are preserved. - Any existing user attributes are preserved unless explicitly overridden by the IdP. This ensures that enabling SSO does not unintentionally remove locally managed permissions or data. This additive role behavior is the project's default; if [Override roles](/auth-methods/sso/settings#role-mapping-add-vs-override) is enabled, the IdP's group → role mapping replaces the user's existing roles on the tenant instead of adding to them, including at conversion time. #### Disabling the Single-Tenant Safeguard By default, Descope prevents SSO conversion if the existing user belongs to multiple tenants. If you [understand the risks](/sso/merging-sso-identities-risk#why-mixing-sso-and-non-sso-is-problematic) and want to allow this behavior, you can disable the single-tenant safeguard by toggling the **Allow duplicate SSO domains across tenants** setting. #### Removing SSO Enforcement If a tenant admin later decides to remove [SSO enforcement](/flows/conditions/sso-enforced), the user can authenticate again using non-SSO login methods. Authentication flows can explicitly check whether a user is an SSO user and handle the transition accordingly. # Multiple SSO Providers Per Tenant (/sso/multi-sso) Configure more than one SAML or OIDC IdP on a single Descope tenant, route users by domain or ssoId, and manage Setup Suite and SCIM per connection. # Multiple SSO Providers Per Tenant Most tenants need one IdP. Some need several on the same tenant, for example Acme's corporate Okta plus a contractor Entra directory, or regional IdPs under one customer org. That is what this page covers: **multiple SSO configurations on one tenant**, each with its own `ssoId`. It is not the same as “each customer gets a tenant with one IdP” (that’s ordinary multi-tenant SSO). ## When You Need This Use additional SSO configs when: - One customer org authenticates through more than one IdP - Different email domains under the same tenant must hit different IdPs - You want a separate [SCIM](/management/tenant-management/scim#multi-tenant-and-multi-sso-architecture) pipeline per IdP - Admins should configure each connection in its own [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) link If every customer is a separate org with one IdP, create one tenant per customer instead. See the [B2B guide](/b2b) and [SSO overview](/auth-methods/sso). ## Default vs Additional Configurations Every tenant has a **default** SSO configuration. You can add more profiles in the Console or via the Management API/SDK. Each additional profile has an `ssoId` you use when starting SSO, loading settings, or generating a Setup Suite link. ![An example of how to configure multiple SSO within a tenant in Descope's console.](/assets/multi-sso-1.webp) You can also generate a dedicated [SSO Setup Suite link](/auth-methods/sso/sso-setup-suite#accessing-the-sso-setup-suite) per configuration (pass `ssoId` when generating the link). ![An example of how to configure multiple SSO within a tenant via Descope's SSO Setup Suite.](/assets/multi-sso-2.webp) ## How Users Reach the Right IdP ### Domains on Each SSO Config Assign **SSO domains** on each configuration (not only on the tenant). Descope uses the user’s email domain to pick the matching connection when you start SSO with the tenant and don’t pass an `ssoId`. Example under tenant `Acme`: | SSO config | SSO domains | IdP | | ---------- | ----------- | --- | | Default | `acme.com` | Acme Okta | | `contractors` | `acme-contractors.com` | Contractor Entra | A user signing in as `alex@acme.com` is routed to Okta. A user signing in as `sam@acme-contractors.com` is routed to Entra. ### Pass `ssoId` Explicitly When domain routing isn’t enough (shared domains, picker in your UI, deep link to a specific IdP), pass the configuration id when you start SSO. **Backend SDK** (optional `sso_id` / `ssoId` on start, shown in [SSO with Backend SDKs](/auth-methods/sso/with-sdks/backend)): ```python resp = descope_client.sso.start( tenant="acme-tenant-id", return_url="https://app.example.com/sso/callback", sso_id="contractors", # optional; omit to use domain routing / default ) ``` **Management SDK.** Load or delete a specific profile with `ssoId`: ```javascript const ssoSettings = await descopeClient.management.sso.loadSettings('tenantId', 'contractors'); ``` Full surface: [Configure SSO with SDKs](/management/tenant-management/sso/sdks). **SSO Setup Suite embed.** Append `?ssoId=...` so the iframe opens that profile ([embed guide](/sso/sso-setup-suite-embed)): ```html ``` ### Tenant Slug / ID Still Applies You still identify the **tenant** first (SSO domain, tenant slug/ID, or flow `tenant` param). Multi-SSO then picks **which IdP inside that tenant**. See [Tenant identification](/auth-methods/sso#tenant-identification-methods). ## SCIM per SSO Configuration [SCIM](/management/tenant-management/scim) is tied to each SSO configuration, not only the tenant. Each IdP can have its own SCIM base URL and token under that profile. Group → role mapping for that connection applies to both SCIM and JIT, and is configured in that configuration's Roles & Groups tab - select the SSO profile first - rather than per SSO method. The mapping is shared across the SAML and OIDC methods of one configuration, not across configurations, so a tenant with several profiles still maps groups once per profile. ## Flows After a user signs in, `lastAuth.ssoId` holds the SSO configuration used for that login. You can branch the flow on it (see [dynamic keys](/flows/dynamic-keys)). ## Checklist 1. Create the default SAML/OIDC connection on the tenant (Console or Setup Suite). 2. Add another SSO configuration and note its `ssoId`. 3. Put the right **SSO domains** on each config, or plan to pass `ssoId` from your app. 4. Map attributes and groups on each config ([SSO mapping](/sso/sso-mapping)). 5. If you use SCIM, configure it under each SSO profile that needs directory sync. 6. Test each connection (Setup Suite test, or [Mock SAML](/management/tenant-management/sso/mock-saml-testing) for a non-prod profile). 7. Ship start/exchange with the tenant id, and `ssoId` when domain routing isn’t enough. ## Related - [SSO overview](/auth-methods/sso) - [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) - [Configure SSO (Management SDKs)](/management/tenant-management/sso/sdks) - [SCIM multi-SSO](/management/tenant-management/scim#multi-tenant-and-multi-sso-architecture) - [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting) # SSO User and Group Mapping (/sso/sso-mapping) Map IdP attributes and groups to Descope users, RBAC roles, and FGA relations for SAML and OIDC SSO. # SSO User and Group Mapping When someone signs in with SSO, the IdP sends user attributes, and often group memberships, in the SAML assertion or OIDC claims. Mapping is how you tell Descope what to do with that data: which fields become user profile attributes, which groups become roles on the tenant, and (if you use Fine-Grained Authorization) which groups should create FGA relations for that user. The same maps apply when: - The user signs in with SSO (including [JIT](/sso/jit-provisioning) create/update) - [SCIM](/management/tenant-management/scim) syncs groups for that SSO config (RBAC role maps, with FGA maps applied on SSO login) ## What You Can Map | Mapping | What it does | | ------- | ------------ | | **User attributes** | IdP fields → Descope user fields (`email`, `name`, `phone`, [custom attributes](/management/user-management#custom-user-attributes)) | | **Groups → roles (RBAC)** | IdP group names → Descope [roles](/authorization/role-based-access-control) on that tenant | | **Groups → FGA** | IdP groups → [FGA / ReBAC relations](/authorization/rebac) (see [below](#groups-to-fga-relations)) | | **Default roles** | Roles to assign when no RBAC group map matches (common with JIT) | RBAC and FGA maps don't depend on each other. One IdP group can grant a role, an FGA relation, or both. SAML IdPs usually send a multi-value `groups` attribute (or something similar). OIDC doesn't standardize groups, so enable a groups (or custom) claim on the IdP, then set **Groups attribute name** in Descope to that claim. See [OIDC → Group claims](/auth-methods/sso/oidc#group-claims-oidc) for details. Both SAML and OIDC support RBAC and FGA group maps. Protocol notes: [Authorization with SSO providers](/management/tenant-management/sso/how-authorization-works-with-sso-providers). ### Groups to Roles | IdP group | Descope role | | --------- | ------------ | | `Engineering` | `Developer Team` | | `HR` | `HR Team` | | `Sales` | `Sales Team` | After SSO (and after SCIM group sync, if you use it), a user in `Engineering` has the `Developer Team` role on that tenant. Configure these maps, along with **Default roles**, under tenant → **Authentication Methods → Roles & Groups** in the Console. The same maps apply whether the tenant uses SAML or OIDC, and they are preserved if you switch the tenant's SSO method. By default, Descope adds a role resolved this way to whatever roles the user already has on the tenant — manually assigned roles are left alone. To make the mapping replace the user's existing tenant roles on every login or sync instead of adding to them, turn on **Override roles** in [Project-Level SSO Settings](/auth-methods/sso/settings#role-mapping-add-vs-override). ## Groups to FGA Relations If you already use [FGA](/authorization) (Descope's ReBAC / ABAC system), you can map IdP groups the same way you map them to roles, except the result is a [relation](/authorization/rebac/create-relations) instead of a role. Example: a user in the IdP's `Engineering` group signs in, and Descope creates a relation like "this user is a `member` of the `engineering` team," using whatever [schema](/authorization/rebac/define-schema) you've defined for the project. From then on, your app can [check](/authorization/rebac/check-relations) that relation the same way it would for any other FGA tuple you created by hand. You need a schema on the project before this works. If there isn't one, login fails with `SchemaDoesNotExist` because there's nothing for the mapping to attach to. Start with [Defining a schema](/authorization/rebac/define-schema) and [implementing it](/authorization/rebac/implement-schema). This is separate from group → role mapping. You can use one, the other, or both for the same group. ### What A Mapping Looks Like In FGA terms, a [relation](/authorization/rebac/create-relations) says: **this user** has **this relation** to **this resource**, under a **type (namespace)** from your schema. An SSO FGA map stores everything except the user. Descope fills in the user at login. For each IdP group name, you list one or more entries with: | Field | Meaning | Where it comes from | | ----- | ------- | ------------------- | | **Namespace** | The schema type (e.g. `team`, `doc`) | Your [FGA schema](/authorization/rebac/define-schema) | | **Relation definition** | The relation name on that type (e.g. `member`, `editor`) | Same schema | | **Resource** | The specific object ID (e.g. `engineering`, `handbook`) | Whatever resource IDs you use when [creating relations](/authorization/rebac/create-relations) | You configure those maps in the tenant's Roles & Groups tab, alongside the RBAC group maps and default roles, and they apply to both SAML and OIDC. In the Management API they are the `fgaMappings` field on the SAML and OIDC tenant SSO settings (see the [Management tenants SSO API](/api/management/tenants/sso)). In the Console and Management API you'll see **FGA mappings**. Some backend events still say **ReBAC groups mappings** (`rebacGroupsMappings`), which is the same feature. So if you map IdP group `Engineering` → namespace `team`, relation `member`, resource `engineering`, then on login Descope creates the same kind of relation you'd get from calling Create Relations with that user as the target. If your resource IDs are opaque (a database key, a folder ID) rather than something readable, the resource picker shows the raw ID by default. Set a [resource display name](/authorization/rebac/create-relations#resource-details) for that resource (its `resourceId` and `resourceType`, the same values as **Resource** and **Namespace** in the table above) and the Console mapping screen shows the display name instead. As shown below, "dev" resolves to a display name ("Team One"); "qa" still shows its raw resource ID ("t3"). ![FGA Mapping in the SSO Setup Suite, showing one team resolved to a display name ("Team One") and another still showing its raw resource ID ("t3").](/assets/sso-setup-suite-fga-mapping.webp) #### Default Relations (Optional) You can also set a fallback list, `defaultReBACRelations`, with the same shape (namespace / relation / resource). Descope only uses it when the tenant has **no** group-specific FGA maps at all. On that path, if a resource is set to `{{tenantID}}`, Descope replaces it with the real tenant ID at login. For SAML tenants, `configFGATenantIDResourcePrefix` and `configFGATenantIDResourceSuffix` on that tenant's SSO settings control what `{{tenantID}}` expands to: the resolved resource is `prefix + tenantID + suffix`. These two fields aren't available on OIDC settings. ### How to Set Up FGA Group Mapping 1. Create (or confirm) an [FGA schema](/authorization/rebac/define-schema) for the project. 2. Configure SSO for the tenant (SAML or OIDC). 3. Add FGA group maps for that tenant: - **Console**: tenant → **Authentication Methods → Roles & Groups** → FGA Group Mapping. - **API**: set `fgaMappings` when you call the SAML/OIDC tenant SSO settings endpoints. 4. Optionally set `defaultReBACRelations` if you want the fallback described above. 5. Sign in with a test user and [check the relation](/authorization/rebac/check-relations) (or inspect it in the Console). #### Example | IdP group | Namespace | Relation | Resource | | --------- | --------- | -------- | -------- | | `Engineering` | `team` | `member` | `engineering` | | `Doc Editors` | `doc` | `editor` | `handbook` | After SSO, Alice has a `member` relation to `team` / `engineering` (and an `editor` relation on `doc` / `handbook` if she's also in Doc Editors). Your app [checks](/authorization/rebac/check-relations) those the same way as any other FGA relation. ### SSO Setup Suite Your customer's admin can set attribute and group maps (including FGA, when you've enabled it for the suite) in the Setup Suite, then run the connection test. See [Setup Suite attribute mapping](/auth-methods/sso/sso-setup-suite#attribute-mapping-user-and-group). ### Management SDK / API Automate maps with [Configure SSO SDKs](/management/tenant-management/sso/sdks) or the [Management tenants SSO API](/api/management/tenants/sso). For FGA, use the `fgaMappings` field on the SAML/OIDC settings payload. ```python from descope import ( SSOSAMLSettings, FGAGroupMapping, FGAGroupMappingRelation, ) descope_client.mgmt.sso.configure_saml_settings( tenant_id="tenant-id", settings=SSOSAMLSettings( idp_url="https://idp.example.com/saml", idp_entity_id="my-entity-id", idp_cert="", fga_mappings={ "Engineering": FGAGroupMapping( relations=[ FGAGroupMappingRelation( namespace="team", relation_definition="member", resource="engineering", ), ], ), }, # Optional, SAML only: customize what the `{{tenantID}}` placeholder resolves to config_fga_tenant_id_resource_prefix="org_", config_fga_tenant_id_resource_suffix="", ), redirect_url="https://your.domain.com", ) ``` `SSOOIDCSettings` and `SSOSAMLSettingsByMetadata` accept the same `fga_mappings` field; only the SAML variants also take `config_fga_tenant_id_resource_prefix` / `config_fga_tenant_id_resource_suffix`. ```go samlSettings := &descope.SSOSAMLSettings{ IdpURL: "https://idp.example.com/saml", IdpEntityID: "my-entity-id", IdpCert: "", FgaMappings: map[string]*descope.FGAGroupMapping{ "Engineering": { Relations: []*descope.FGAGroupMappingRelation{ {Namespace: "team", RelationDefinition: "member", Resource: "engineering"}, }, }, }, // Optional, SAML only: customize what the `{{tenantID}}` placeholder resolves to ConfigFGATenantIDResourcePrefix: "org_", ConfigFGATenantIDResourceSuffix: "", } err := descopeClient.Management.SSO().ConfigureSAMLSettings(context.Background(), "tenant-id", samlSettings, "https://your.domain.com", nil, "") ``` `SSOOIDCSettings` and `SSOSAMLSettingsByMetadata` accept the same `FgaMappings` field; only the SAML variants also take `ConfigFGATenantIDResourcePrefix` / `ConfigFGATenantIDResourceSuffix`. ### Multiple IdPs on One Tenant Each SSO profile has its own maps. Configure them per `ssoId`. See [Multiple SSO providers](/sso/multi-sso). ## IdP-Side Checklist - Send the attribute and group names Descope expects (they have to match what you configured). - Entra ID: prefer stable group IDs over display names if groups get renamed often. See [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting). - Okta: make sure users/groups are assigned to the app. Assignments versus Push Groups is a common mix-up on that same page. ## Verify and Troubleshoot 1. Sign in with a known test user. 2. Check the login event in [Audits](https://app.descope.com/audits) ([SSO audit fields](/audit-trails-and-integrations/audit-events#sso-related-fields)). 3. For RBAC: confirm the user's roles on the tenant match the IdP groups you expected. 4. For FGA: [check the relations](/authorization/rebac/check-relations) (or look them up in the Console). 5. If groups never show up, check IdP app assignment and the **Groups attribute name** on the SSO config. | Code / error | Usually means | | ------------ | ------------- | | `SchemaDoesNotExist` | No FGA schema on the project; [define one](/authorization/rebac/define-schema) before using FGA maps | | `E062016` | Failed to update FGA / ReBAC maps from SSO groups | | `E062028` | Groups is mandatory, but the IdP sent no group values during an SSO Setup Suite connection test (requires JIT provisioning enabled) | | `E113201` | Mappable FGA helpers / Setup Suite pickers hit invalid SAML settings | More: [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting). ## Related - [JIT provisioning](/sso/jit-provisioning) - [SCIM](/management/tenant-management/scim) - [ReBAC overview](/authorization/rebac) · [Define a schema](/authorization/rebac/define-schema) · [Create relations](/authorization/rebac/create-relations) - [Authorization with SSO providers](/management/tenant-management/sso/how-authorization-works-with-sso-providers) - [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting) # Embedding SSO Setup Suite (/sso/sso-setup-suite-embed) Learn how to embed the SSO Setup Suite directly in your application. # Embedding SSO Setup Suite The SSO Setup Suite can be embedded directly within your application using an [iframe](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe), providing a seamless experience for your users to configure SSO settings without leaving your app. ## Implementation To embed the SSO Setup Suite, use an iframe with the SSO Suite URL. No authentication token is required in the URL itself - just use the base link without any token parameters. ```html ``` ## Authentication Requirements While no token is required in the iframe URL, **ensure that a valid Descope JWT is available within your application**. The embedded SSO Suite will use the authentication context from its parent application. The JWT will need to have [appropriate permissions](/auth-methods/sso/sso-setup-suite#method-2-direct-access-with-authentication) for SSO configuration. ## Query Parameters ### Tenant ID If the logged-in user is associated with multiple tenants, you can specify which tenant's SSO configuration to display by including the `tenantId` as a query parameter: ```html ``` If the JWT includes a `dct` claim, the selected tenant will automatically be picked up and this query parameter is unnecessary. If you need to change this value without running through a flow, you can use the [`selectTenant` function](/client-sdk/auth-helpers#available-functions) of our client SDKs. ### SSO Configuration ID When using multiple SSO configurations, you can specify which configuration to display by passing the `ssoId` as a query parameter. If the `ssoId` value is not provided, it will display the SSO Setup Suite for the Default provider: ```html ``` ## Theme Customization The SSO Setup Suite supports theme customization to match your application's appearance. Use the `theme` query parameter to control the theme. **If the `theme` parameter is not provided, the suite will automatically follow the user's operating system theme preference (same as `theme=os`).** ### Available Themes - `light` - Force light theme - `dark` - Force dark theme - `os` - Automatically match the user's operating system theme preference (default behavior) ```html ``` # SSO Setup Suite RBAC (/sso/sso-setup-suite-rbac) Learn how to configure SSO Setup Suite RBAC. # Filtering Roles in the SSO Setup Suite You can filter which roles appear for mapping in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) based on the authenticated user's role permissions. That way, only authorized users can configure or view the role mappings relevant to their permissions. ## Prerequisites - Ensure that the token response method is set to "Manage in Cookies" in the [Descope Session Management Settings](/management/project-settings#token-response-methods). ## Configuration Steps The Descoper should follow these steps to correctly configure roles and permissions: 1. **Assign Permissions for Roles**: - Assign permissions to roles that you want SSO Setup Suite users to access and map. Only roles containing permissions also held by the authenticated user will be shown. - **Example**: If you want to restrict access to roles associated with a specific application (e.g., "AppX") only to users who have a subscription for that application, create a role like "AppX Subscriber" with the "AppX" permission. 2. **Assign Roles to SSO Setup Suite User**: - Assign the user roles with the relevant permissions. - **Example**: To allow access to roles related to "AppX", assign any role which includes the "AppX" permission, along with the **Tenant Admin** role. 3. **Set Project-Level Role as Default Role for a Specific Tenant**: - Assign a project-level role as a default role for a specific tenant. This means that when a new user is created for the tenant, they will automatically be assigned this project-level role. - **Example**: To assign any project level role as the default role for the "AppX" tenant, select that role as the default role for the "AppX" tenant. ![project role as default role for a specific tenant](/assets/s4-project-role-as-default-tenant.webp) ## SSO Setup Suite Access Steps The Tenant Admin (SSO Setup Suite user) should follow these steps to access the SSO Setup Suite: 1. **Authenticate User**: - The user needs to be authenticated, and have a valid cookie on your Descope domain (either `api.descope.com`, or your [custom domain](/how-to-deploy-to-production/custom-domain) if you have one configured). - This is in place of directly sending the user the generated SSO Setup Suite link, and can be achieved by having the user authenticate through a sign in flow on your Descope domain. 2. **Access the SSO Setup Suite**: - The user can then visit `https:///sso/setup/__ProjectID__?tenantId=`. - The roles that appear in the mapping dropdown will match the permissions of the authenticated user. Only users with the appropriate permissions see the relevant roles. - **Example**: A user with the "AppX" permission can now view and map roles associated with "AppX". By following these steps, you can ensure that users accessing the SSO Setup Suite can only configure or view role mappings relevant to their assigned permissions. # SSO and SCIM Tutorials (/sso/tutorials) Video walkthroughs of the SSO Setup Suite, SCIM, and IdP-initiated login with Descope. # SSO and SCIM Tutorials For configuring SSO (and SCIM) with any identity provider, start with the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite): generate a link, open it yourself or send it to the customer's IT admin, and use the IdP template or generic SAML/OIDC path. The videos below are optional walkthroughs of that experience and related topics. ## SSO Setup Suite ## Example IdP Walkthroughs These show Setup Suite-style configuration against common providers. The same approach applies to other SAML/OIDC IdPs. ### Okta ### Microsoft Entra ID (Azure AD) ## SCIM SCIM can be configured in the Setup Suite under the same tenant SSO connection. Written docs: [SCIM](/management/tenant-management/scim). ### Okta ### Microsoft Entra ID (Azure AD) ## IdP-Initiated Login When users start from the IdP app portal instead of your login page: [SSO login flows](/sso/idp-initiated). # IdP vs SP (/sso-integrations/idp-vs-sp) Learn about the relationship between identity providers and service providers, in the context of Descope. # Identity Provider (IdP) vs Service Provider (SP) This is an informative guide on the differences between an identity provider (IdP) and a service provider (SP), as it pertains to Descope. ## Identity Provider (IdP) An Identity Provider (IdP) system creates, maintains, and manages identity information for principals and provides authentication services to relying applications within a federation or distributed network. It is responsible for verifying the identity of users and issuing authentication tokens or credentials. ## Service Provider (SP) A Service Provider (SP) is an entity that provides web-based applications, services, or resources to users after successful authentication from an IdP. The SP relies on the IdP to authenticate users and may grant or deny access to its services based on that authentication. ## What is SAML Security Assertion Markup Language (SAML) is an XML-based standard for exchanging authentication and authorization data between parties. In a typical SAML flow, a user tries to access a service (often called a "Service Provider" or SP). If the user isn't authenticated, they are redirected to an Identity Provider (IdP), where they authenticate. Upon successful authentication, the IdP returns a SAML assertion to the SP. This assertion contains statements (or assertions) about the user, such as their name, roles, and other attributes. The SP then uses this assertion to grant access to the user. This process enables Single Sign-On (SSO), allowing users to authenticate once with the IdP and gain access to multiple services without being prompted to log in again. ## What is OIDC OpenID Connect (OIDC) is a protocol that sits on top of the OAuth 2.0 protocol, designed to authenticate users. While OAuth 2.0 is primarily about authorization (delegating access to resources without sharing credentials), OIDC extends this with identity features, enabling clients to verify the end-users identity based on the authentication performed by an authorization server. OIDC uses JSON Web Tokens (JWT) to represent the identity information. In the context of OIDC, the term "Federated Identity Providers" often comes up. Federated identity refers to linking a person's electronic identity and attributes stored across multiple identity management systems. This means a user can use a single set of credentials to authenticate across multiple domains or services. ## SAML vs OIDC The most prominent differences between SAML and OIDC are their formats and the use cases they typically address. SAML uses XML for its assertions, while OIDC uses JSON Web Tokens (JWT). Additionally, SAML has been around longer and is often associated with enterprise Single Sign-On in web applications. In contrast, OIDC is a newer protocol commonly used for authenticating users in modern web, mobile, and API applications, building upon the OAuth 2.0 framework. Another key difference is in their focus: SAML encompasses authentication and authorization, while OIDC is primarily for authentication on top of the OAuth 2.0 authorization framework. # SSO Integrations (/sso-integrations) Learn about the SSO integrations that Descope offers, including Applications, Tenants / Custom Providers, and Descope as an Identity Federation Broker. # SSO Integrations Single Sign-On (SSO) and Identity Federation is a key feature in modern authentication, enabling users to access multiple applications with a single set of credentials. Descope offers flexible SSO integration options to cater to different organizational needs, whether you want Descope to be your Identity Provider (IdP), your Service Provider (SP), or both. If you would like to learn more about the relationship between Identity Providers (IdPs) and Service Providers (SPs), you can look at our [guide](/sso-integrations/idp-vs-sp) on this. ## Descope as the Identity Provider (Applications) When Descope is configured as the Identity Provider, it acts as the central authority for authentication, managing users, roles, and permissions across multiple applications. This setup allows you to provide seamless access to various applications by leveraging Descope Flows, including auth methods like OAuth social login, passkeys, as well as [multi-factor authentication](/mfa-and-step-up/mfa) and [device fingerprinting](/fingerprinting). ### Use Cases: - **Consolidate Authentication Across Apps:** Be able to aggregate the authentication experience, sessions, and user identities in one central location while providing access to multiple internal or third-party applications. - **Hosted Application:** Be able to utilize flows and authenticate users for applications using our [Auth Hosting App](/identity-federation/auth-hosting) application, without having to embed our Descope components in your application. - **Augmentation:** Be able to augment and integrate Descope authentication in existing auth implementations, such as [AWS Cognito](/identity-federation/applications/setup-guides/aws-cognito), [Firebase](/identity-federation/applications/setup-guides/firebase-oidc), etc. ### Learn More: - [Overview of Applications](/identity-federation/applications) ## Descope as the Service Provider (Tenants / Custom Providers) In this configuration, Descope integrates with external Identity Providers, allowing users from different organizations (or tenants) to access your application. This is particularly useful when you want to offer a single application to multiple clients, each with its own IdP, or when integrating with enterprise-level solutions like Azure AD or Okta. ### Use Cases: - **Multi-Tenant Applications:** Useful for SaaS providers who need to support multiple organizations with different IdPs. - **Custom Provider Integrations:** Allows integration with custom identity providers for niche use cases. ### Learn More: - [Configuring Descope as SP with Tenants and Custom Providers](/auth-methods/sso) - [SSO with External Identity Providers](/identity-federation/applications) ## Descope as Both IdP and SP (Identity Federation Broker) In some cases, you might want Descope to act as both the IdP and SP. This setup is common in complex environments where an application needs to authenticate users both internally (using Descope as IdP) and externally (by accepting identities from other IdPs). ### Use Cases: - **Hybrid Environments:** Ideal for large organizations with a mix of internal users (authenticated via Descope) and external users (authenticated via their organization's IdP). - **Advanced Customization:** Offers maximum flexibility for organizations with complex authentication requirements. ### Learn More: - [Using Descope as Identity Federation Broker](https://www.descope.com/use-cases/identity-federation) - [Customizing Authentication Flows](/flows) ### Relevant Sections - [Applications: SSO with Descope as IdP](/identity-federation/applications) - [Tenants and Custom Providers: SSO with Descope as SP](/auth-methods/sso) ## Conclusion Descope’s versatile SSO capabilities enable you to tailor your authentication strategy to meet the specific needs of your organization. Whether you’re centralizing user management, supporting multi-tenant applications, or implementing a hybrid approach, Descope provides the tools and flexibility to ensure secure and seamless access to your applications. # Django (/getting-started/angular/django) Learn how to integrate Descope with your Angular & Django in your application. # Angular & Django Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/angular/go) Learn how to integrate Descope with your Angular & Go in your application. # Angular & Go Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Angular (/getting-started/angular) Get started with Angular in minutes with Descope. # Angular Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- If you're using an AI-enhanced developer tool (Cursor, Claude Code, Copilot in VS Code, Windsurf, and similar), we recommend using our [Rules file for Angular](https://github.com/descope/ai/blob/main/rules/client-sdks/descope-angular.mdc) and placing it in the `/rules` directory of your project. This file contains structured integration instructions and code examples for the Descope Angular SDK. This is a quickstart guide to help you integrate Descope with your Angular application. Follow the steps below to get started. ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/angular/java) Learn how to integrate Descope with your Angular & Java in your application. # Angular & Java Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/angular/nodejs) Learn how to integrate Descope with your Angular & Node.js in your application. # Angular & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/angular/php) Learn how to integrate Descope with your Angular & PHP in your application. # Angular & PHP Quickstart This is a quickstart guide to help you integrate Descope with your Angular & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/angular/python) Learn how to integrate Descope with your Angular & Python in your application. # Angular & Python Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/angular/ruby) Learn how to integrate Descope with your Angular & Ruby in your application. # Angular & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your Angular & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Django (/getting-started/django) Learn how to integrate Descope's Python SDK in your Django application. # Django Quickstart This guide will help you integrate Descope's Python SDK into your Django application. Follow the steps below to get started. The `django-descope` plugin supports Django versions 3.2 through 6.0+. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" pip install django-descope ``` ### Import and Setup Backend SDK You'll need install and setup all of the packages from the SDK. This is done by ensuring `django_descope` is under `INSTALLED_APPS` in `settings.py`. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to export the Base URL (e.g. `export DESCOPE_BASE_URI="__BaseURL__"`) when initializing `descope_client`. Optionally, to make the Descope username populate from a custom claim instead of `sub`, set the `DESCOPE_USERNAME_CLAIM` to the corresponding username claim in the JWT. ```python title="settings.py" INSTALLED_APPS = [ ... 'django_descope', ] DESCOPE_PROJECT_ID=os.getenv("DESCOPE_PROJECT_ID") DESCOPE_USERNAME_CLAIM=os.getenv("DESCOPE_USERNAME_CLAIM") ``` ### Add Descope Middleware Ensure Descope Middleware is after the AuthenticationMiddleware and SessionMiddleware. ```python title="settings.py" MIDDLEWARE = [ ... 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ... 'django_descope.middleware.DescopeMiddleware', ] ``` ### Configure URLconf You will then need to include the Descope URLconf in your project `urls.py` like this. ```python title="urls.py" path('auth/', include('django_descope.urls')), ``` ### Configure URLconf The session validation is handled in the Django SDK, through the middleware. ```python title="settings.py" INSTALLED_APPS = [ ... 'django_descope', ] MIDDLEWARE = [ ... 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ... 'django_descope.middleware.DescopeMiddleware', ] DESCOPE_PROJECT_ID=os.getenv("DESCOPE_PROJECT_ID") ``` If you're interested in offline JWT validation, check out our [offline JWT validation guide](/sessions/validation/backend/offline-jwt-validation). ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always pass that value (or your custom audience) when validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. ```python title="middleware.py" from django.http import HttpResponseForbidden from descope import AuthException class DescopeAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): session_token = request.headers.get('Authorization') if session_token: try: jwt_response = descope_client.validate_session( session_token=session_token, audience="__ProjectID__" ) request.user = jwt_response # Store user info in request except AuthException: return HttpResponseForbidden() response = self.get_response(request) return response ``` Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [Python SDK](https://github.com/descope/python-sdk) - [Django Plugin](https://github.com/descope/django-descope) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # .NET (/getting-started/dotnet) Learn how to integrate Descope's DotNet SDK in your backend application. # .NET Quickstart This guide will help you integrate Descope's .NET SDK into your backend application. Follow the steps below to get started. ### Install Backend SDK Navigate to the .NET project directory which contains your `.csproj` file and install the SDK with `dotnet` using the following command: ```sh title="Terminal" dotnet add package Descope ``` ### Set up Environment file Create a `appsettings.json` file in the root directory of your project with your Descope Project ID, which can be found on the [Project Settings Page](https://app.descope.com/settings/project) of the console. If you plan to use Management functions, include a `Descope Management Key` here as well, which can be found on the [Company Settings Page](https://app.descope.com/settings/company/managementkeys) of the console. ```json title="appsettings.json" { "Descope": { "ProjectId": "__ProjectID__", "ManagementKey": "DESCOPE_MANAGEMENT_KEY" } } ``` ### Setup Backend SDK You'll need to configure a `DescopeClientOptions` object using the values from your `appsettings.json`, then create the client using one of the two approaches below. The SDK's management API client is generated using Microsoft Kiota and is exposed through whichever client you create. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, set the `BaseUrl` property on `DescopeClientOptions` (e.g. `BaseUrl = "https://auth.company.com"`). ```csharp title="Program.cs" using Descope; using Microsoft.Extensions.Configuration; var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); var options = new DescopeClientOptions { ProjectId = config["Descope:ProjectId"], // Required ManagementKey = config["Descope:ManagementKey"], // Optional, for management APIs BaseUrl = "__BaseURL__", // Optional, auto-detected; only set to override (e.g. custom domain) JwksCacheDuration = TimeSpan.FromMinutes(5) // Optional, how long public signing keys are cached (default: 5 minutes) }; ``` `ValidateSession` verifies tokens locally using Descope's public signing keys (JWKS), which the SDK fetches and caches for the duration set by `JwksCacheDuration` (default: 5 minutes). Most validations hit this cache and make no network call. On high-traffic services you can raise this value (e.g. `TimeSpan.FromMinutes(30)`) to reduce key-fetch requests. This is safe even during key rotation: if a token is signed by an unknown key ID, the SDK immediately re-fetches keys and retries once. #### Option 1: Dependency Injection Best for ASP.NET Core apps. `AddDescopeClient` registers `IDescopeClient` as a singleton in the DI container, so you can inject it anywhere into your services. This keeps configuration in one place, hands lifetime management to the framework, and makes testing easier since `IDescopeClient` can be swapped for a mock in your unit tests. ```csharp title="Program.cs" builder.Services.AddDescopeClient(options); ``` ```csharp title="MyService.cs" public class MyService { private readonly IDescopeClient descopeClient; public MyService(IDescopeClient client) { descopeClient = client; } } ``` #### Option 2: Factory (instance-based) Best for console apps, background workers, or when you need to manually control the client's lifetime. Create the client once and reuse that instance so key caching works across calls. ```csharp title="Program.cs" var descopeClient = DescopeManagementClientFactory.Create(options); ``` ### Implement Session Validation If you need more granular control over session validation and prefer to use built-in Microsoft packages, see our [.NET JWT Validation Guide](/sessions/validation/jwt-authorizers/dotnet-jwt-validation) for details on validating session tokens directly. You will need to fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. After `ValidateSessionAsync` succeeds, check the token claims so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. The `ValidateSession` function can be used to verify a user's session as shown below. This either validates the sessions or throws an error, depending on if the JWT is valid or not. ```csharp // Validate the session. Will return an error if expired try { var sessionToken = await descopeClient.Auth.ValidateSessionAsync(sessionJwt); } catch (DescopeException e) { // Handle the error } ``` Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [.NET SDK](https://github.com/descope/descope-dotnet) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Django (/getting-started/flutter/django) Learn how to integrate Descope with your Flutter & Django in your application. # Flutter & Django Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/flutter/go) Learn how to integrate Descope with your Flutter & Go in your application. # Flutter & Go Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Flutter (/getting-started/flutter) Get started with Flutter in minutes with Descope. # Flutter Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- This is a quickstart guide to help you integrate Descope with your Flutter application. Follow the steps below to get started. To learn more about how Native Flows work and how they provide a seamless, in-app authentication experience, see [Native Flows](/mobile-sdk/native-vs-browser-flows). For a live demo, check out our [video on Adding Auth to Your Flutter App](https://www.youtube.com/watch?v=vwy0exZp2dw). ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/flutter/java) Learn how to integrate Descope with your Flutter & Java in your application. # Flutter & Java Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/flutter/nodejs) Learn how to integrate Descope with your Flutter & Node.js in your application. # Flutter & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/flutter/php) Learn how to integrate Descope with your Flutter & PHP in your application. # Flutter & PHP Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/flutter/python) Learn how to integrate Descope with your Flutter & Python in your application. # Flutter & Python Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/flutter/ruby) Learn how to integrate Descope with your Flutter & Ruby in your application. # Flutter & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your Flutter & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/go) Learn how to integrate Descope's Go SDK in your backend application. # Go Quickstart This guide will help you integrate Descope's Go SDK into your backend application. Follow the steps below to get started. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" go get github.com/descope/go-sdk ``` ### Import and Setup Backend SDK You'll need import and setup all of the packages from the SDK. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to include `BaseUrl` as a parameter in your `client.Config` (e.g. `{BaseUrl : "__BaseURL__"}`) when initializing `descopeClient`. ```go title="app.go" import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) ``` ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always pass that value (or your custom audience) when validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. Additional audience values are passed as further arguments after the session token. ```go title="app.go" descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__"}) if err != nil { log.Println("failed to initialize: " + err.Error()) } // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // Fetch session token from HTTP Authorization Header sessionToken := "xxxx" authorized, userToken, err := descopeClient.Auth.ValidateSessionWithToken(ctx, sessionToken, "__ProjectID__") if (err != nil){ fmt.Println("Could not validate user session: ", err) } else { fmt.Println("Successfully validated user session: ", userToken) } ``` ### Next Steps Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [Go SDK](https://github.com/descope/go-sdk) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Django (/getting-started/html/django) Learn how to integrate Descope with your HTML & Django in your application. # HTML & Django Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/html/go) Learn how to integrate Descope with your HTML & Go in your application. # HTML & Go Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# OIDC Client Login (/getting-started/html/html-oidc) Learn how to integrate Descope with any frontend application using WebJS and OIDC. # OIDC Authentication with WebJS SDK ### Install the SDK Install the Descope WebJS SDK using your preferred package manager: ```sh title="Terminal" npm install @descope/web-js-sdk ``` ```sh title="Terminal" yarn add @descope/web-js-sdk ``` ```sh title="Terminal" pnpm add @descope/web-js-sdk ``` ```sh title="Terminal" bun install @descope/web-js-sdk ``` ### Initialize the SDK Initialize the Descope SDK with your project ID. You can find this ID on the [project page](https://app.descope.com/settings/project) of your Descope console. You can also add the optional `baseUrl` parameter if you're utilizing a [custom domain](/how-to-deploy-to-production/custom-domain) within your Descope project (ex: `https://auth.company.com`). ```javascript title="main.js" import { DescopeSdk } from '@descope/web-js-sdk'; const sdk = DescopeSdk({ projectId: '__ProjectID__', persistTokens: true, autoRefresh: true }); ``` ### Start OIDC Login Use `sdk.oidc.loginWithRedirect()` to initiate the OIDC login process: ```javascript title="login.js" async function startOIDCLogin() { const resp = await sdk.oidc.loginWithRedirect({ redirect_uri: window.location.origin // Optional: defaults to current URL login_hint: 'user@example.com', // Optional: pre-fill the form.externalId context key }); if (!resp.ok) { console.log("Failed to start OIDC flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) } else { console.log("Successfully Started OIDC flow") window.location.replace(resp.data.url) } } ``` #### `loginWithRedirect` Parameters You can pass any of the following parameters into the `loginWithRedirect` function to provide additional info to the OIDC provider: - `redirect_uri`: A custom URI that overrides the default redirect after login. This will default to being the current URL. - `login_hint`: A hint to Descope of the user identifier (e.g., email address). This will pre-fill the `form.externalId` context key in the flow you're redirected to. ### Handle the OIDC Redirect After successful authentication, the user is redirected back to your application. Call `sdk.finishLoginIfNeeded()` to complete the process: ```javascript title="redirect.js" async function handleOIDCRedirect() { try { // This will automatically take the OIDC query params from the current URL and finish the login process await sdk.finishLoginIfNeeded(); console.log("Successfully finished OIDC flow") // Redirect to authenticated area } catch (error) { console.error('Failed to finish OIDC login:', error); } } ``` ### Maintain Session To ensure the session persists across your application, add this to all authenticated pages: ```javascript title="session.js" async function checkSession() { try { await sdk.refresh({ skipIfNoSession: true }); const sessionToken = sdk.getSessionToken(); if (!sessionToken || sdk.isJwtExpired(sessionToken)) { // Redirect to login window.location.href = '/login'; } } catch (error) { console.error('Session check failed:', error); } } ``` ### Logout To log out the user, you can use either local logout or OIDC-compliant logout: ```javascript title="logout.js" async function handleLogout() { try { // Local logout await sdk.logout(); // OR OIDC-compliant logout await sdk.oidc.logout(); window.location.href = '/'; } catch (error) { console.error('Logout failed:', error); } } ``` ### Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/web). ### Congratulations Now that you've got the authentication down, go focus on building out the rest of your app!
# HTML (/getting-started/html) Get started with HTML in minutes with Descope. # HTML Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- This is a quickstart guide to help you integrate Descope with your HTML application. Follow the steps below to get started. ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/html/java) Learn how to integrate Descope with your HTML & Java in your application. # HTML & Java Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/html/nodejs) Learn how to integrate Descope with your HTML & Node.js in your application. # HTML & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/html/php) Learn how to integrate Descope with your HTML & PHP in your application. # HTML & PHP Quickstart This is a quickstart guide to help you integrate Descope with your HTML & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/html/python) Learn how to integrate Descope with your HTML & Python in your application. # HTML & Python Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/html/ruby) Learn how to integrate Descope with your HTML & Ruby in your application. # HTML & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your HTML & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/java) Learn how to integrate Descope's Java SDK in your backend application. # Java Quickstart This guide will help you integrate Descope's Java SDK into your backend application. Follow the steps below to get started. ### Install Backend SDK Install the SDK by including the SDK in your `pom.xml` file (for installation via Maven). ```xml title="pom.xml" java-sdk com.descope [1.0.0,) ``` ### Import and Setup Backend SDK You'll need import and setup all of the packages from the SDK. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to export the Base URL (e.g. `export DESCOPE_BASE_URI="__BaseURL__"`) when initializing `descope_client`. ```java title="Application.java" package com.descope.java_sample_app; // descope imports start import com.descope.client.*; import com.descope.exception.DescopeException; import com.descope.model.jwt.Token; import com.descope.sdk.auth.AuthenticationService; // descope imports end import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @RestController public class JavaSampleAppApplication { DescopeClient descopeClient = new DescopeClient( Config.builder().projectId("__ProjectID__").build() ); AuthenticationService authService = descopeClient.getAuthenticationServices().getAuthService(); public static void main(String[] args) { SpringApplication.run(JavaSampleAppApplication.class, args); } } ``` ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always check that value (or your custom audience) after validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. ```java title="Application.java" @SpringBootApplication @RestController public class JavaSampleAppApplication { public static void main(String[] args) { SpringApplication.run(JavaSampleAppApplication.class, args); } public void validateSession(String sessionToken, String refreshToken) { var descopeClient = new DescopeClient(Config.builder().projectId("__ProjectID__").build()); AuthenticationService as = descopeClient.getAuthenticationServices().getAuthService(); try { Token t = as.validateSessionWithToken(sessionToken); if (!"__ProjectID__".equals(t.getProjectId())) { // Reject: token issued for a different project throw new DescopeException("aud claim mismatch"); } } catch (DescopeException de) { // Handle the unauthorized error } // If validation fails because the session expired, refresh it try { Token t = as.refreshSessionWithToken(refreshToken); } catch (DescopeException de) { // Handle the unauthorized error } // If JWT rotation is enabled in your project settings, refreshing a session also returns a new // refresh token. Use the AuthenticationInfo variant to retrieve it try { AuthenticationInfo authInfo = as.refreshSessionWithTokenAuthenticationInfo(refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); } catch (DescopeException de) { // Handle the unauthorized error } // Or validate and refresh in one call try { Token t = as.validateAndRefreshSessionWithTokens(sessionToken, refreshToken); } catch (DescopeException de) { // unauthorized error } try { AuthenticationInfo authInfo = as.validateAndRefreshSessionWithTokensAuthenticationInfo(sessionToken, refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); } catch (DescopeException de) { // unauthorized error } } } ``` ### Next Steps Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [Java SDK](https://github.com/descope/descope-java) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Django (/getting-started/kotlin/django) Learn how to integrate Descope with your Kotlin & Django in your application. # Kotlin & Django Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/kotlin/go) Learn how to integrate Descope with your Kotlin & Go in your application. # Kotlin & Go Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Kotlin (/getting-started/kotlin) Get started with Kotlin in minutes with Descope. # Kotlin Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- This is a quickstart guide to help you integrate Descope with your Kotlin application. Follow the steps below to get started. To learn more about how Native Flows work and how they provide a seamless, in-app authentication experience, see [Native Flows](/mobile-sdk/native-vs-browser-flows). ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/kotlin/java) Learn how to integrate Descope with your Kotlin & Java in your application. # Kotlin & Java Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/kotlin/nodejs) Learn how to integrate Descope with your Kotlin & Node.js in your application. # Kotlin & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/kotlin/php) Learn how to integrate Descope with your Kotlin & PHP in your application. # Kotlin & PHP Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/kotlin/python) Learn how to integrate Descope with your Kotlin & Python in your application. # Kotlin & Python Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/kotlin/ruby) Learn how to integrate Descope with your Kotlin & Ruby in your application. # Kotlin & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your Kotlin & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# App Router (/getting-started/nextauth/app-router) Learn to integrate Descope with NextAuth App Router for efficient authentication routing. # NextAuth with App Router This guide will help you integrate Descope with your NextAuth application using App Router. Follow the steps below to get started. If you're interested in using our native SDK instead of NextAuth, you can read about the pros and cons of each in our [guide](/getting-started/nextauth/nextauth-vs-native). ### Install NextAuth.js To use Descope with [Auth.js v5](https://authjs.dev/getting-started), you can begin by installing it with this command: ```sh title="Terminal" npm install next-auth@beta ``` ```sh title="Terminal" yarn add next-auth@beta ``` ```sh title="Terminal" pnpm add next-auth@beta ``` ```sh title="Terminal" bun i next-auth@beta ``` It is also possible to use the legacy [NextAuth.js v4](https://next-auth.js.org/getting-started/introduction). You can install it with this command: ```sh title="Terminal" npm i --save next-auth ``` ```sh title="Terminal" yarn add next-auth ``` ```sh title="Terminal" pnpm add next-auth ``` ```sh title="Terminal" bun i next-auth ``` ### Setup Environment The only environment variable that is mandatory is the `AUTH_SECRET`, a random value used by the library to encrypt tokens and email verification hashes. You can generate one by running: ``` npx auth secret ``` This will also automatically add the secret to your `.env`. ### Setup NextAuth's `SessionProvider` In your `/app` directory, create a `provider.tsx` file and wrap the client components in SessionProvider to allow for session management and authentication throughout your Next application. ```tsx title="app/provider.tsx" 'use client'; import { SessionProvider } from 'next-auth/react'; export default function NextAuthSessionProvider({ children }: Readonly<{ children: React.ReactNode }>) { return {children}; } ``` ### Allow Access to Session Data In your `layout.tsx` file, import the `NextAuthSessionProvider` and wrap the nodes in the provider. ```tsx title="app/layout.tsx" import NextAuthSessionProvider from './provider' export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return (
{children}
); } ``` ### Import NextAuth Packages Import all necessary NextAuth packages in a `route.ts` file. The location of `route.ts` will exist in `app/api/auth/[...nextauth]`. ```typescript title="app/api/auth/[...nextauth]/route.ts" import NextAuth from "next-auth/next"; import type { NextAuthOptions } from "next-auth" import Descope from "next-auth/providers/descope" export const authOptions: NextAuthOptions = { providers: [], } const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ``` ### Configure Descope Provider Once you've imported the necessary packages, you'll need to initialize NextAuth and add Descope as a provider. With Auth.js v5, Descope is recognized as an [official provider](https://authjs.dev/getting-started/providers/descope). ```typescript title="app/api/auth/[...nextauth]/route.ts" import NextAuth from "next-auth" import Descope from "next-auth/providers/descope" export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [Descope], }) ``` In the Auth.js v5 beta, you may need to manually configure the Descope provider. To do so, you can provide the necessary OIDC information: ```typescript title="app/api/auth/[...nextauth]/route.ts" providers: [ { id: "descope", name: "Descope", type: "oauth", clientId: process.env.DESCOPE_PROJECT_ID || "__ProjectID__", clientSecret: process.env.DESCOPE_ACCESS_KEY || "DESCOPE_ACCESS_KEY", issuer: , checks: ["pkce", "state"], authorization: { params: { scope: "openid email profile", response_type: "code", } }, token: { url: `/oauth2/v1/token`, }, userinfo: { url: `/oauth2/v1/userinfo`, }, profile(profile: any) { return { id: profile.sub, name: profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`.trim(), email: profile.email, image: profile.picture, } }, } ], ``` Using the legacy NextAuth v4, you must provide the configuration for the Descope provider. ```typescript title="app/api/auth/[...nextauth]/route.ts" import NextAuth from "next-auth/next"; import Descope from "next-auth/providers/descope" import type { NextAuthOptions } from "next-auth" export const authOptions: NextAuthOptions = { providers: [ Descope({ clientId: "", clientSecret: "", issuer: "", }), ], }; const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ``` Replace `Descope Project ID` with your [Descope Project ID](https://app.descope.com/settings/project), `Descope Access Key` with your [access key](https://app.descope.com/accessKeys) from the Descope Console and your `Issuer URL` from the [Federated Apps](https://app.descope.com/applications). ### Add JWT Callback for Token Refresh To make sure that your NextAuth session is refreshed according to the settings defined in the Descope Console, you'll want to add a JWT callback function to refresh the Descope Access Token that NextAuth uses for its session/profile management. ```typescript title="app/api/auth/[...nextauth]/route.ts" import NextAuth from "next-auth/next"; import type { NextAuthOptions } from "next-auth" export const authOptions: NextAuthOptions = { providers: [ ... ], callbacks: { async jwt({token, account, profile}) { if (account) { return { ...token, access_token: account.access_token, expires_at: Math.floor(Date.now() / 1000 + account.expires_in), refresh_token: account.refresh_token, profile: { name: profile?.name, email: profile?.email, image: profile?.picture, }, } } else if (Date.now() < token.expires_at * 1000) { return token } else { try { const response = await fetch("__BaseURL__/oauth2/v1/token", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, body: new URLSearchParams({ client_id: "__ProjectID__", client_secret: "", grant_type: "refresh_token", refresh_token: token.refresh_token, }), method: "POST", }) const tokens = await response.json() if (!response.ok) throw tokens return { ...token, access_token: tokens.access_token, expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), refresh_token: tokens.refresh_token ?? token.refresh_token, } } catch (error) { console.error("Error refreshing access token", error) return {...token, error: "RefreshAccessTokenError"} } } }, async session({session, token}) { if (token.profile) { session.user = token.profile; } session.error = token.error session.accessToken = token.access_token return session }, } } const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ``` ### Accessing the Authentication Flow Add a sign-in button in the client to access your sign-in authentication flow. The `signIn` method has 'descope' as the provider id, and the callback URL set to `/dashboard` as an example to redirect back to. ```tsx title="components/navbar.tsx" 'use client' import { signIn } from "@/auth" export default function Navbar() { return (
{ "use server" await signIn("descope", { callbackUrl: "/dashboard" }) }} >
) } ``` ### Customizing the Authentication Flow URL The URL of the hosted authentication flow that is redirected to after calling `signIn` can be modified in your Descope console. Navigate to your Descope console -> Federated Apps -> OIDC default application (or your application) -> Flow Hosting URL. Or go directly [here](https://app.descope.com/applications/descope-default-oidc). ### Session Management To learn more about session management with NextAuth & Descope, see [Web client sessions](/sessions/management/web). ### Congratulations Now that you've got the authentication down, go focus on building out the rest of your app! --- ## Using NextAuth and Customization Once you've configured NextAuth to work with Descope as an OIDC provider, the next step is to utilize all of the various NextAuth functions in your application. You can visit our guide with detailed docs on how all of the `Sign In`, `Logout`, etc. functions work with NextAuth, in your Next.js application.
Otherwise, you can visit our **Flow Customization** section to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
## NextAuth and Widgets If you're using NextAuth with Descope, you will have to incorporate some elements of our Next.js SDK in your application to be able to utilize Descope [Widgets](/widgets). Using Widgets will require the use of our [Next.js SDK](/getting-started/nextjs) in your application requires a reference to your Descope Project ID. This works from wrapping your `layout.tsx` with our `` wrapper from our Next.js SDK. ```tsx title="layout.tsx" export default function App({ Component, pageProps,}: AppProps<{ session: Session }>) { return ( <>
) } ``` As long as the Widget components have access to your refresh tokens, you should be able to use them, even if using NextAuth. If you're managing your refresh tokens with `localStorage` instead of cookies, you'll need to do some additional steps, documented below. ### Managing Refresh Tokens If you are managing your refresh tokens with cookies, and using [Manage in Response Body](/security-best-practices/refresh-token-storage) for your Token Response Method instead, then you'll need to persist the refresh token in your application manually. If you are using **Manage in Cookies** instead, you can skip this part. ```tsx title="_app.tsx" const SessionManager = () => { const {data: session, status} = useSession(); useEffect(() => { if(status === "loading") return; if(!session){ console.log() localStorage.removeItem("DSR") return; } const sessionData = session as Session & {refreshToken: string}; if(sessionData.refreshToken){ localStorage.setItem("DSR", sessionData.refreshToken) } }, [session, status]) return null; } ``` You'll also need to modify the NextAuth callback functions to include the refresh token as well. ```tsx title="utils/options.ts" ], callbacks: { async jwt({ token, account }) { if (account?.id_token) { token.idToken = account.id_token; } if (account?.refresh_token) { token.refreshToken = account.refresh_token; } return token; }, async session({ session, token }) { (session as any).idToken = token.idToken; (session as any).refreshToken = token.refreshToken; return session; }, }, ``` Once you've done that, the widgets should work with the same session created with every login with Descope. # Built-in Functions (/getting-started/nextauth/functions) Explore using NextAuth functions with Descope for seamless application authentication. # Built-in NextAuth Functions NextAuth (or [AuthJS](https://authjs.dev/)) is an open source authentication library for web applications like NextJS, Svelte, Express, or SolidStart. As a native [NextAuth Provider](https://authjs.dev/reference/core/providers/descope), Descope can be seamlessly integrated into your application whether you are already using NextAuth or adding it from scratch. Be sure to check out our Getting Started guide for initializing a project from scratch [here](/getting-started). This page simply covers the key functions relevant to implementing NextAuth with Descope. ## Functions ### Sign in Signing in a user involves calling the sign in function from NextAuth and passing a provider name of `descope` in order for the Descope provider to be used. You can also pass a callback URL to redirect the user to after they've signed in. This will redirect the user to the Descope login page and then back to your application. The authentication flow that is redirected to can be found, and customized, in your Descope console, as elaborated on [here](/identity-federation/auth-hosting). ```javascript title="sign-in.tsx" import { signIn } from "next-auth/react" ``` ### Sign out For signing out a user, you'll have to sign out of both Descope and NextAuth. This can be done by calling the `signOut` function for NextAuth and then a `federatedSignOut` Server Action for Descope. The code could look something like this: ```javascript title="sign-out.tsx" 'use client' import { federatedSignOut } from "@/app/api/auth/federated-sign-out"; import { signOut } from "next-auth/react"; import { useRouter } from "next/navigation"; ``` The `federatedSignOut` Server Action is a custom function that you'll create in your NextJS application. ```javascript title="federated-sign-out.ts" 'use server' import { authOptions } from "@/app/_utils/options"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; export async function federatedSignOut() { // NEXTAUTH_URL is set in .env.local, defining the URL of the app const redirectUrl = process.env.NEXTAUTH_URL || '/' try { const session = await getServerSession(authOptions); if (!session) { redirect(redirectUrl); } const res = await fetch("__BaseURL__/oauth2/v1/logout", { method: "POST", body: new URLSearchParams({ // @ts-ignore id_token_hint: session.idToken, // Needed for OAuth logout endpoint post_logout_redirect_uri: redirectUrl, }), }); if (res.status === 200) { return { message: "Successfully logged out of Descope", } } else { throw new Error("Failed to log out of Descope"); } } catch (error) { throw new Error('Failed to log out of Descope') } } ``` ### NextAuth options ```javascript title="auth-options.ts" import { NextAuthOptions } from "next-auth"; export const authOptions: NextAuthOptions = { providers: [ { id: "descope", name: "Descope", type: "oauth", wellKnown: `__BaseURL__/${process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID}/.well-known/openid-configuration`, authorization: { params: { scope: "openid email profile descope.custom_claims" }, }, idToken: true, clientId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, clientSecret: process.env.DESCOPE_ACCESS_KEY, checks: ["pkce", "state"], profile(profile, tokens) { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, idToken: tokens.id_token, ...tokens, }; }, }, ], secret: process.env.NEXTAUTH_SECRET callbacks: { async jwt({token, account, profile}) { if (account) { return { ...token, access_token: account.access_token, expires_at: Math.floor(Date.now() / 1000 + account.expires_in), refresh_token: account.refresh_token, profile: { name: profile?.name, email: profile?.email, image: profile?.picture, }, } } else if (Date.now() < token.expires_at * 1000) { return token } else { try { const response = await fetch("__BaseURL__/oauth2/v1/token", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, body: new URLSearchParams({ client_id: "__ProjectID__", client_secret: "", grant_type: "refresh_token", refresh_token: token.refresh_token, }), method: "POST", }) const tokens = await response.json() if (!response.ok) throw tokens return { ...token, access_token: tokens.access_token, expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), refresh_token: tokens.refresh_token ?? token.refresh_token, } } catch (error) { console.error("Error refreshing access token", error) return {...token, error: "RefreshAccessTokenError"} } } }, async session({session, token}) { if (token.profile) { session.user = token.profile; } session.error = token.error session.accessToken = token.access_token return session }, } }; ``` ## Customize Now that you have the end-to-end application working with all of the built-in NextAuth functions, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# NextAuth (/getting-started/nextauth) Get started with NextAuth in minutes with Descope. # NextAuth Quickstart # Next.js vs NextAuth with Descope (/getting-started/nextauth/nextauth-vs-native) A comparison of integrating Descope with Next.js natively vs. integrating with NextAuth # Next.js vs NextAuth with Descope In modern web development, user authentication and management are key to building secure and scalable applications. Descope provides a versatile identity management platform, and for developers using Next.js, there are two main ways to integrate Descope: using [NextAuth](https://authjs.dev/getting-started/providers/descope) or integrating Descope natively through our [Next.js SDK](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk). This article compares both approaches, evaluating their differences in terms of implementation, flexibility, and functionality. ## Descope: Simplified Authentication Descope is an identity and access management platform designed to streamline the authentication process. Its rich feature set includes multi-factor authentication (MFA), social login, and session management, all tailored to provide a flexible user experience. ## Next.js: A Leading Framework for Modern Web Apps Next.js is a popular framework built on React, offering server-side rendering, static site generation, and full-stack capabilities. Its flexibility and performance make it a go-to choice for web developers. ### Option 1: Integrating Descope with NextAuth NextAuth is an open-source authentication library specifically designed for Next.js, providing support for over 80 providers, including OAuth2, OpenID Connect (OIDC), and custom authentication. Descope can be integrated with NextAuth as a custom provider through its OIDC support. #### How Descope Works with NextAuth: 1. **Authentication Request**: Users initiate sign-in through NextAuth with Descope configured as the provider. 2. **Token Exchange**: NextAuth forwards the request to Descope, which handles the authentication process and returns a token. 3. **Session Management**: NextAuth manages the session with the received tokens, typically using signed cookies. 4. **User Interaction**: The authenticated session allows users to interact with the Next.js application seamlessly. #### Key Considerations: - **Simplicity**: NextAuth abstracts much of the session handling and authentication, making it easier to manage tokens and sessions without much custom logic. - **Middleware Support**: NextAuth’s middleware handles session creation, renewal, and protection of API routes. - **Limitations**: While Descope’s core authentication features can be used through NextAuth, some advanced Descope functionalities, like particular flows, may not be available. ### Option 2: Integrating Descope Natively with Next.js SDK For a more direct integration, developers can use Descope’s Next.js SDK. This SDK is built specifically to work seamlessly with Next.js, providing deeper access to Descope’s authentication features and giving more control over the flow and logic of authentication. #### Benefits of Using Descope’s Next.js SDK: 1. **Full Customization**: With the Next.js SDK, developers have full control over authentication, allowing custom login, registration flows, and more. You can fully manage your user interface and logic using Descope without depending on other external libraries like NextAuth. 2. **Optimized for Next.js**: The SDK is designed to leverage Next.js features, such as server-side rendering, for handling authentication securely and efficiently. 3. **Advanced Authentication Features**: Descope’s advanced functionalities, like MFA, SSO, Step Up and session management, are fully supported and customizable without third-party limitations. 4. **Session Management**: The SDK simplifies session validation and management, allowing developers to store, validate, and refresh tokens directly within their application. #### Key Considerations: - **Greater Flexibility**: By using Descope’s SDK, you can access all of Descope’s features and fine-tune every aspect of the authentication process, including UI, authentication methods, and session expiration. - **Middleware Support**: You can secure client and backend routes with Descope’s middleware, directly and seamlessly managing user access and token validation. ## Comparison: NextAuth vs. Descope’s Next.js SDK | Feature | NextAuth with Descope | Descope’s Next.js SDK | |----------------------------------|--------------------------------------------|----------------------------------------------------| | **Ease of Setup** | Quick to set up if already using NextAuth | Quick to set up, simply requires embedding components | | **Customization** | Limited to NextAuth’s structure | Full control over all Descope flows and UI | | **Session Management** | Managed automatically by NextAuth | Managed automatically by Descope | | **Flow Support** | Basic support via OIDC flows | Complete flow support, including step up, edit profile, etc. | ## Conclusion Both NextAuth and Descope’s Next.js SDK offer solid solutions for integrating Descope with Next.js, but they cater to different needs. NextAuth provides an easier, more familiar path for developers already using NextAuth. On the other hand, Descope’s Next.js SDK is ideal for a broader range of projects, providing more flexibility and control over the authentication process, especially when implementing advanced features like MFA, SSO, and customized flows. Choosing the right approach depends on your project’s complexity and requirements. If you prioritize ease of integration and are content with basic authentication flows, NextAuth is a great option. However, for full access to Descope’s feature set and deeper customization, the Descope Next.js SDK provides a more powerful, flexible solution. # Pages Router (/getting-started/nextauth/pages-router) Discover how to set up Descope with NextAuth Pages Router for secure and efficient authentication. # NextAuth with Pages Router This guide will help you integrate Descope with your NextAuth application using the Pages Router. Follow the steps below to get started. If you're interested in using our native SDK instead of NextAuth, you can read about the pros and cons of each in our [guide](/getting-started/nextauth/nextauth-vs-native). ### Install NextAuth.js To use Descope with [Auth.js v5](https://authjs.dev/getting-started), you can begin by installing it with this command: ```sh title="Terminal" npm install next-auth@beta ``` ```sh title="Terminal" yarn add next-auth@beta ``` ```sh title="Terminal" pnpm add next-auth@beta ``` ```sh title="Terminal" bun i next-auth@beta ``` It is also possible to use the legacy [NextAuth.js v4](https://next-auth.js.org/getting-started/introduction). You can install it with this command: ```sh title="Terminal" npm i --save next-auth ``` ```sh title="Terminal" yarn add next-auth ``` ```sh title="Terminal" pnpm add next-auth ``` ```sh title="Terminal" bun i next-auth ``` ### Setup Environment The only environment variable that is mandatory is the `AUTH_SECRET`, a random value used by the library to encrypt tokens and email verification hashes. You can generate one by running: ``` npx auth secret ``` This will also automatically add the secret to your `.env`. ### Import NextAuth Packages Import all necessary NextAuth packages in a `[...nextauth].ts` file. The location of `[...nextauth].ts` will exist in `pages/api/auth`. ```typescript title="pages/api/auth/[...nextauth].ts" import NextAuth from "next-auth/next"; import type { NextAuthOptions } from "next-auth" export const authOptions: NextAuthOptions = { providers: [], } export default NextAuth(authOptions) ``` ### Initialize Descope as a Provider Once you've imported the necessary packages, you'll need to initialize NextAuth and add Descope as a provider. With the advent of Auth.js v5, setting Descope as a provider is very simple: ```typescript title="pages/api/auth/[...nextauth].ts" import NextAuth from "next-auth" import Descope from "next-auth/providers/descope" export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [Descope], }) ``` With NextAuth v4 and in the beta, you may need to use the following configuration for the Descope provider: ```typescript title="pages/api/auth/[...nextauth].ts" import NextAuth from "next-auth/next"; import type { NextAuthOptions } from "next-auth" export const authOptions: NextAuthOptions = { providers: [ { id: "descope", name: "Descope", type: "oauth", wellKnown: `__BaseURL__/__ProjectID__/.well-known/openid-configuration`, authorization: { params: { scope: "openid email profile" } }, idToken: true, clientId: "__ProjectID__", clientSecret: "", checks: ["pkce", "state"], profile(profile) { return { id: profile.sub, name: profile.name, email: profile.email, image: profile.picture, } }, } ], callbacks: { async jwt({token, account, profile}) { if (account) { return { ...token, access_token: account.access_token, expires_at: Math.floor(Date.now() / 1000 + account.expires_in), refresh_token: account.refresh_token, profile: { name: profile?.name, email: profile?.email, image: profile?.picture, }, } } else if (Date.now() < token.expires_at * 1000) { return token } else { try { const response = await fetch("__BaseURL__/oauth2/v1/token", { headers: {"Content-Type": "application/x-www-form-urlencoded"}, body: new URLSearchParams({ client_id: "__ProjectID__", client_secret: "", grant_type: "refresh_token", refresh_token: token.refresh_token, }), method: "POST", }) const tokens = await response.json() if (!response.ok) throw tokens return { ...token, access_token: tokens.access_token, expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in), refresh_token: tokens.refresh_token ?? token.refresh_token, } } catch (error) { console.error("Error refreshing access token", error) return {...token, error: "RefreshAccessTokenError"} } } }, async session({session, token}) { if (token.profile) { session.user = token.profile; } session.error = token.error session.accessToken = token.access_token return session }, } }; export default NextAuth(authOptions) ``` ### Setup NextAuth's `SessionProvider` In your `_app.tsx` file, wrap the components in `SessionProvider` to allow for session management and authentication throughout your Next application. ```tsx title="_app.tsx" import type { AppProps } from "next/app"; import { Session } from "next-auth"; import { SessionProvider } from "next-auth/react" export default function App( { Component, pageProps }: AppProps<{ session: Session }> ) { return ( ) } ``` ### Accessing the Authentication Flow Add a sign-in button in the client to access your sign-in authentication flow. The `signIn` method has `descope` as the provider id, and the callback URL set to `/dashboard` as an example to redirect back to. ```tsx title="components/Navbar.tsx" import { signIn } from "@/auth" export default function Navbar() { return (
{ "use server" await signIn("descope", { callbackUrl: "/dashboard" }) }} >
) } ``` ### Session Management To learn more about session management with NextAuth & Descope, see [Web client sessions](/sessions/management/web). ### Congratulations Now that you've got the authentication down, go focus on building out the rest of your app! --- ## Using NextAuth and Customization Once you've configured NextAuth to work with Descope as an OIDC provider, the next step is to utilize all of the various NextAuth functions in your application. You can visit our guide with detailed docs on how all of the `Sign In`, `Logout`, etc. functions work with NextAuth, in your Next.js application.
Otherwise, you can visit our **Flow Customization** section to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
## NextAuth and Widgets For a sample app using the Pages Router with NextAuth and Widgets, please refer to our sample app in GitHub [here](https://github.com/descope-sample-apps/nextjs-hackathon-template-pages). If you're using NextAuth with Descope, you will have to incorporate some elements of our Next.js SDK in your application to be able to utilize Descope [Widgets](/widgets). Using Widgets will require the use of our [Next.js SDK](/getting-started/nextjs) in your application requires a reference to your Descope Project ID. This works from wrapping your `_app.tsx` with our `` wrapper from our Next.js SDK. ```tsx title="_app.tsx" export default function App({ Component, pageProps,}: AppProps<{ session: Session }>) { return ( <>
) } ``` As long as the Widget components have access to your refresh tokens, you should be able to use them, even if using NextAuth. If you're managing your refresh tokens with `localStorage` instead of cookies, you'll need to do some additional steps, documented below. ### Managing Refresh Tokens If you are managing your refresh tokens with cookies, and using [Manage in Response Body](/security-best-practices/refresh-token-storage) for your Token Response Method instead, then you'll need to persist the refresh token in your application manually. If you are using **Manage in Cookies** instead, you can skip this part. ```tsx title="_app.tsx" const SessionManager = () => { const {data: session, status} = useSession(); useEffect(() => { if(status === "loading") return; if(!session){ console.log() localStorage.removeItem("DSR") return; } const sessionData = session as Session & {refreshToken: string}; if(sessionData.refreshToken){ localStorage.setItem("DSR", sessionData.refreshToken) } }, [session, status]) return null; } ``` You'll also need to modify the NextAuth callback functions to include the refresh token as well. ```tsx title="utils/options.ts" ], callbacks: { async jwt({ token, account }) { if (account?.id_token) { token.idToken = account.id_token; } if (account?.refresh_token) { token.refreshToken = account.refresh_token; } return token; }, async session({ session, token }) { (session as any).idToken = token.idToken; (session as any).refreshToken = token.refreshToken; return session; }, }, ``` Once you've done that, the widgets should work with the same session created with every login with Descope. # Next.js (/getting-started/nextjs) Learn how to integrate Descope with Next.js in your application. # Next.js Quickstart If you're using an AI-enhanced developer tool (Cursor, Claude Code, Copilot in VS Code, Windsurf, and similar), we recommend downloading our [Rules file for Next.js](https://github.com/descope/ai/blob/main/rules/client-sdks/descope-nextjs.mdc) and placing it in the `/rules` directory of your project. This file contains structured integration instructions and code examples for the Descope Next.js SDK. This is a quickstart guide to help you integrate Descope with your Next.js application. Follow the steps below to get started. ### Install NextJS SDK Install the SDK with the following command: ```sh title="Terminal" npm i --save @descope/nextjs-sdk ``` ```sh title="Terminal" yarn add @descope/nextjs-sdk ``` ```sh title="Terminal" pnpm add @descope/nextjs-sdk ``` ```sh title="Terminal" bun i @descope/nextjs-sdk ``` ### Import and Wrap Application with `AuthProvider` Wrap the entire application with ``. You need your Project ID for this step, which you can find on the [project page](https://app.descope.com/settings/project) of your Descope console. You can also add the optional `baseUrl` parameter if you're utilizing a [custom domain](/how-to-deploy-to-production/custom-domain#base-url-for-the-descope-sdk) within your Descope project (ex: `https://auth.company.com`). ```tsx title="app/layout.tsx" import { AuthProvider } from '@descope/nextjs-sdk'; export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( {children} ); } ``` You can customize client-side token behavior using optional parameters like `persistTokens`, and `sessionTokenViaCookie`. Learn more in the [Auth Helpers](/client-sdk/auth-helpers) documentation. ### Import Descope Functions and Add Flows Component In server-side rendering (SSR) frameworks like Next.js, all Descope components must be rendered on the **client-side only**. You can achieve this by dynamically importing our components and explicitly disabling server-side rendering (`ssr: false`). Read more about it [here](/getting-started/nextjs/ssr-considerations#dynamic-import-with-ssr-disabled). To trigger the Descope Flow, you will need to add this component. The screens you've customized in your Flow will appear here. You can also customize the component with the following: - `flowId`: ID of the flow you wish to use - `onSuccess` and `onError`: functions that execute when authentication succeeds or fails For the full list of component customization options, refer to our [Descope Components Doc.](/client-sdk/descope-components) ```tsx title="app/sign-in.tsx" import { Descope } from '@descope/nextjs-sdk'; const Page = () => { return ( console.log(e.detail.user)} onError={(e) => console.log('Could not log in!')} /> ); }; ``` ### Utilize the NextJS SDK Hooks and Functions Descope provides many different hooks to check if the user is authenticated, session is loading etc. You can use these to customize the user experience: - `isAuthenticated`: Boolean for the authentication state of the current user session. - `isSessionLoading`: Boolean for the loading state of the session. Can be used to display a "loading" message while the session is still loading. - `useUser`: Returns information about the currently authenticated user. - `sessionToken`: The JWT for the current session. For the full list of available hooks and functions, refer to the [Auth Helpers Doc.](/client-sdk/auth-helpers) ```tsx title="app/page.tsx" 'use client'; import { useCallback } from 'react'; import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; const Page = () => { // NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context, // and will throw an exception if this requirement is not met const { isAuthenticated, isSessionLoading, sessionToken } = useSession(); // useUser retrieves the logged in user information const { user, isUserLoading } = useUser(); if (isSessionLoading || isUserLoading) { return

Loading...

; } if (isAuthenticated) { return ( <>

Hello {user.name}

); } return ( <>

You are not logged in

); } ``` ### Add Logout Functionality You can use the React [useCallback](https://react.dev/reference/react/useCallback) hook to create your logout function. `useDescope()`: Returns further operations related to authentication including logout. ```tsx title="app/page.tsx" 'use client'; import { useCallback } from 'react'; import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; const Page = () => { // NOTE - `useDescope`, `useSession`, `useUser` should be used inside `AuthProvider` context, // and will throw an exception if this requirement is not met const { isAuthenticated, isSessionLoading, sessionToken } = useSession(); // useUser retrieves the logged in user information const { user, isUserLoading } = useUser(); // useDescope retrieves Descope SDK for further operations related to authentication // such as logout const sdk = useDescope(); const handleLogout = useCallback(() => { sdk.logout(); }, [sdk]); if (isSessionLoading || isUserLoading) { return

Loading...

; } if (isAuthenticated) { return ( <>

Hello {user.name}

); } return ( <>

You are not logged in

); } ``` ### Setting Up the Middleware You can use NextJS Middleware to require authentication for specific pages and routes in your application. The Descope SDK provides a middleware function that can be used to require authentication for your protected pages and routes. Read more about the Next SDK [here](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk?tab=readme-ov-file#descope-sdk-for-nextjs) The Descope Next.js SDK's middleware support requires **Next.js 13 or later**. As of Next.js 16, the `middleware.ts` file convention is deprecated and renamed `proxy.ts` (same behavior and `config`/`matcher`). `middleware.ts` still works today, but if you're on Next.js 16+, use the `proxy.ts` tab below, or run Next's [migration codemod](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#migration-to-proxy): `npx @next/codemod@canary middleware-to-proxy .` Create the file at the root of your project (or inside `src/` if you use one): ```typescript title="middleware.ts" import { authMiddleware } from '@descope/nextjs-sdk/server' export default authMiddleware({ // The Descope project ID to use for authentication // Defaults to process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID projectId: '__ProjectID__', // The URL to redirect to if the user is not authenticated // Defaults to process.env.SIGN_IN_ROUTE or '/sign-in' if not provided redirectUrl?: string, // An array of public routes that do not require authentication // In addition to the default public routes: // - process.env.SIGN_IN_ROUTE or /sign-in if not provided // - process.env.SIGN_UP_ROUTE or /sign-up if not provided // NOTE: In case it contains query parameters that exist in the original URL, // they will override the original query parameters. e.g. if the original URL is /page?param1=1¶m2=2 and the redirect URL is /sign-in?param1=3, // the final redirect URL will be /sign-in?param1=3¶m2=2 publicRoutes?: string[] }) export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'] } ``` ```typescript title="proxy.ts" import { authMiddleware } from '@descope/nextjs-sdk/server' export default authMiddleware({ // The Descope project ID to use for authentication // Defaults to process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID projectId: '__ProjectID__', // The URL to redirect to if the user is not authenticated // Defaults to process.env.SIGN_IN_ROUTE or '/sign-in' if not provided redirectUrl?: string, // An array of public routes that do not require authentication // In addition to the default public routes: // - process.env.SIGN_IN_ROUTE or /sign-in if not provided // - process.env.SIGN_UP_ROUTE or /sign-up if not provided // NOTE: In case it contains query parameters that exist in the original URL, // they will override the original query parameters. e.g. if the original URL is /page?param1=1¶m2=2 and the redirect URL is /sign-in?param1=3, // the final redirect URL will be /sign-in?param1=3¶m2=2 publicRoutes?: string[] }) export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'] } ``` ### Congratulations Now that you've got the authentication down, go focus on building out the rest of your app! --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Protecting Routes with Middleware (/getting-started/nextjs/next-middleware) Learn how to protect specific pages and API routes in Next.js using Descope middleware. # Protecting Routes with Middleware The Descope Next.js SDK provides authentication middleware to enforce secure access control across application routes. Middleware allows authentication checks before requests reach a page or API, ensuring only authorized users can proceed. Although the middleware isn't strictly required, it is recommended for consistent and secure route protection. The Descope Next.js SDK's middleware support requires **Next.js 13 or later**. As of Next.js 16, the `middleware.ts` file convention is deprecated and renamed `proxy.ts` (same behavior and `config`/`matcher`). `middleware.ts` still works today, but if you're on Next.js 16+, use `proxy.ts` instead, or run Next's [migration codemod](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#migration-to-proxy): `npx @next/codemod@canary middleware-to-proxy .` This guide covers: - Configuring authentication middleware - Defining public and private routes - Using wildcard paths for flexible route protection - Redirecting unauthenticated users - Handling session expiration and forced re-authentication ## Setting Up Authentication Middleware In Next.js, middleware intercepts requests before they reach a route. The Descope SDK provides `authMiddleware()` to enforce authentication for protected pages. ### Creating Middleware Create a `middleware.ts` file (or `proxy.ts`, see the note above) in the root of your Next.js project. ```ts title="middleware.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, redirectUrl: '/sign-in', publicRoutes: ['/home', '/about'], privateRoutes: ['/dashboard', '/profile'], }); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], }; ``` ```ts title="proxy.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, redirectUrl: '/sign-in', publicRoutes: ['/home', '/about'], privateRoutes: ['/dashboard', '/profile'], }); export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], }; ``` ### How Middleware and the SDK Works - All routes are private by default. - Public routes are explicitly defined using `publicRoutes`. - Private routes can be defined using `privateRoutes`, but if both `publicRoutes` and `privateRoutes` are used, `privateRoutes` is ignored. - Middleware redirects unauthenticated users to `redirectUrl`. #### The Session Header When a request is processed, the middleware: 1. **Extracts the session JWT** from the request (`Authorization` header or `DS` cookie). 2. **Validates the JWT** using Descope's backend. 3. **Encodes session data** as a Base64 JSON string. 4. **Attaches the session data to the request headers** under the `X-Descope-Session` header. ```ts const addSessionToHeadersIfExists = ( headers: Headers, session: AuthenticationInfo | undefined ): Headers => { if (session) { const requestHeaders = new Headers(headers); requestHeaders.set( DESCOPE_SESSION_HEADER, Buffer.from(JSON.stringify(session)).toString('base64') ); return requestHeaders; } return headers; }; ``` After validation, the middleware modifies the request headers to include the session data before forwarding the request. ```ts return NextResponse.next({ request: { headers: addSessionToHeadersIfExists(req.headers, session) } }); ``` This allows server-side components and API routes to access authentication details without manually validating the session again. #### Session Storage Differences Between Next.js and React/JS By default, the Descope Next.js SDK stores the DS (Descope Session) in a **cookie**, whereas the Web JS SDK and React SDK store it in **localStorage**. This means: - **Next.js SDK (cookies)**: - Accessible via JavaScript. - Stored securely in a secure, `samesite=lax` cookie. - Automatically included in server-side requests. - Persistent across browser sessions. - **React SDK (local storage)**: - Accessible via JavaScript. - Requires manual handling for secure transmission to the backend. For more information on configuring the `AuthProvider`, including options like adjusting the `SameSite` cookie settings, refer to our [Auth Provider documentation](https://docs.descope.com/client-sdk/descope-components#auth-provider) This allows client-side applications to use the token for making authenticated API requests. ## Multi-Project SDK Support Descope's Next.js SDK supports multi-project setups, where each tenant in a multi-tenant application is mapped to a separate Descope project. This approach is ideal when tenants need isolated configurations or dedicated user stores. A typical setup looks like this: * Each request includes a header (e.g., `x-tenant-id`) that identifies the tenant. * The application dynamically initializes the Descope SDK using the project ID associated with the tenant. * Middleware handles authentication using the appropriate project. ### SDK Instance Caching To support multi-tenant applications efficiently, the SDK automatically caches instances based on project ID: * **On the first request** for a given project ID, a new SDK instance is created. * **On subsequent requests** with the same project ID, the existing instance is reused. This per-project caching ensures optimal performance by avoiding redundant SDK initializations while allowing different tenants to use separate Descope configurations. Per-project configuration extends to Next.js SDK functions like `session()`. When assigning a separate project to each tenant, the SDK automatically inherits the appropriate project settings, ensuring consistent authentication across your application. ### Example: Tenant-Based Authentication Here's a complete example showing how to implement tenant-based authentication in your middleware: ```ts import { authMiddleware } from '@descope/nextjs-sdk/server'; import { NextRequest } from 'next/server'; type TenantConfig = { projectId: string; baseUrl?: string }; // Tenant configuration mapping const tenantConfigs: Record = { 'tenant-a': { projectId: 'tenant-a-project', baseUrl: '__BaseURL__', }, 'tenant-b': { projectId: 'tenant-b-project', baseUrl: '__BaseURL__', }, }; export const middleware = async (request: NextRequest) => { const tenantId = request.headers.get('x-tenant-id'); if (!tenantId || !tenantConfigs[tenantId]) { return new Response('Invalid tenant', { status: 400 }); } const config = tenantConfigs[tenantId]; const auth = authMiddleware({ projectId: config.projectId, baseUrl: config.baseUrl, redirectUrl: '/sign-in', privateRoutes: ['/dashboard', '/profile'], publicRoutes: ['/home', '/about'], }); return await auth(request); }; export const config = { matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], }; ``` ## Using Wildcard Paths for Route Protection Wildcard paths (`*`) simplify route protection by applying authentication requirements to entire sections of an application. ### Protecting API Endpoints ```ts export default authMiddleware({ privateRoutes: ['/api/*'], }); ``` ### Protecting an Admin Section ```ts export default authMiddleware({ privateRoutes: ['/admin/*'], }); ``` ## Redirecting Unauthenticated Users By default, unauthenticated users are redirected to `/sign-in`. To customize this behavior, update `redirectUrl`. ### Redirecting to a Custom Login Page ```ts export default authMiddleware({ redirectUrl: '/auth/login', }); ``` ## Controlling Log Levels in the Descope Next.js SDK The Descope Next.js SDK allows you to control logging levels for debugging and monitoring authentication-related events. This is useful for diagnosing issues during development and ensuring secure authentication flows in production. ### Supported Log Levels The `logLevel` option in `authMiddleware()` can be set to one of the following values: | Log Level | Description | |-----------|-------------| | `debug` | Logs detailed debug messages, useful for troubleshooting authentication flows. | | `info` | Logs general informational messages, such as successful logins and token validations. | | `warn` | Logs warnings about potential misconfigurations or non-critical issues. | | `error` | Logs only critical authentication failures and errors. | ### Configuring Log Levels You can set the log level when defining your middleware: ```ts import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, logLevel: 'debug', // Change to 'info', 'warn', or 'error' as needed redirectUrl: '/sign-in', }); ``` ### Example: Debugging Authentication Setting the log level to `debug` provides detailed output in the console, helping to trace authentication issues. ```ts import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, logLevel: 'debug', // Enables verbose debugging logs redirectUrl: '/sign-in', }); ``` #### Sample Debug Logs When `logLevel: 'debug'` is set, you might see logs like: ``` Auth middleware starts Auth middleware finishes ``` If an error occurs: ``` Auth middleware starts Failed to validate JWT: Failed to validate JWT [JWSInvalid: Compact JWS must be a string or Uint8Array] { code: 'ERR_JWS_INVALID', name: 'JWSInvalid' } Redirecting to /sign-in ``` ### Recommended Log Levels - **Development:** Use `debug` or `info` to get detailed logs while debugging authentication. - **Production:** Use `warn` or `error` to log only critical authentication failures. For full implementation details, check the sample projects: - [App Router Sample](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk/examples/app-router) - [Pages Router Sample](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk/examples/pages-router) # OIDC Client Login (/getting-started/nextjs/nextjs-oidc) Learn how to integrate Descope with Next.js in your application, using OIDC and our SDK. # Next.js OIDC Client Quickstart This guide walks you through how to integrate Descope into your Next.js app as an **OIDC client**, using redirect-based login with [Federated Apps](/identity-federation/applications). ### Install the SDK Install the Descope SDK for Next.js using your preferred package manager: ```sh npm install @descope/nextjs-sdk ``` ```sh yarn add @descope/nextjs-sdk ``` ```sh pnpm add @descope/nextjs-sdk ``` ```sh bun install @descope/nextjs-sdk ``` ### Wrap Your App with `AuthProvider` In your `layout.tsx`, wrap your app in `AuthProvider` and configure OIDC by enabling `oidcConfig`. ```tsx title="app/layout.tsx" import { AuthProvider } from '@descope/nextjs-sdk'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` You may also pass a full `oidcConfig` object to customize settings such as `redirectUri`, `scope`, and `applicationId`. Read more in our Client SDK docs section. ### Trigger Login with Redirect Use the `useDescope()` hook and call `sdk.oidc.loginWithRedirect()` to start the login process. ```tsx title="app/page.tsx" 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; export default function Page() { const sdk = useDescope(); return ( ); } ``` #### `loginWithRedirect` Parameters You can pass any of the following parameters into the `loginWithRedirect` function to provide additional info to the OIDC provider: - `redirect_uri`: A custom URI that overrides the default redirect after login. This will default to being the current URL. - `login_hint`: A hint to Descope of the user identifier (e.g., email address). This will pre-fill the `form.externalId` context key in the flow you're redirected to. ### Redirect Back and Automatic Session Handling After a successful login, the OIDC provider will redirect the user to your app. The `AuthProvider` will automatically process the returned tokens and establish a session. No additional code is needed on the redirect page — just ensure the page is wrapped with `AuthProvider`. ### Access User and Session Info You can use `useSession()` and `useUser()` to access authentication state and user info. ```tsx title="app/dashboard.tsx" 'use client'; import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; export default function Dashboard() { const { isAuthenticated, isSessionLoading } = useSession(); const { user, isUserLoading } = useUser(); if (isSessionLoading || isUserLoading) return

Loading...

; if (isAuthenticated) { return

Welcome, {user.name}

; } return

You are not logged in

; } ``` ### Logout the User To log out, you can call `sdk.logout()` for local session clearing, or `sdk.oidc.logout()` for a full OIDC-compliant logout with redirect: ```tsx title="app/page.tsx" 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; export default function LogoutButton() { const sdk = useDescope(); return ( <> ); } ``` ### (Optional) Use Middleware to Protect Routes Use the Descope middleware to guard specific routes: The Descope Next.js SDK's middleware support requires **Next.js 13 or later**. As of Next.js 16, the `middleware.ts` file convention is deprecated and renamed `proxy.ts` (same behavior and `config`/`matcher`). `middleware.ts` still works today, but if you're on Next.js 16+, use the `proxy.ts` tab below, or run Next's [migration codemod](https://nextjs.org/docs/app/api-reference/file-conventions/proxy#migration-to-proxy): `npx @next/codemod@canary middleware-to-proxy .` ```ts title="middleware.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: '__ProjectID__', redirectUrl: '/sign-in', publicRoutes: ['/sign-in', '/about'], }); export const config = { matcher: ['/((?!_next|.*\\..*).*)'], }; ``` ```ts title="proxy.ts" import { authMiddleware } from '@descope/nextjs-sdk/server'; export default authMiddleware({ projectId: '__ProjectID__', redirectUrl: '/sign-in', publicRoutes: ['/sign-in', '/about'], }); export const config = { matcher: ['/((?!_next|.*\\..*).*)'], }; ``` ### You're All Set! Your Next.js app now uses Descope as an OIDC provider with redirect-based login, logout, and session management. # SSR Considerations (/getting-started/nextjs/ssr-considerations) Understand server-side rendering considerations when using Descope with Next.js. # SSR Considerations When integrating the Descope Next.js SDK into your application, it's important to handle Server-Side Rendering (SSR) appropriately. This guide covers: - Rendering Descope components on the client side - Dynamically importing Descope components - Understanding SSR implications ## Client-Side Rendering Requirement Descope components rely on browser-specific APIs and cannot be rendered directly during server-side rendering. Attempting to render these components server-side will result in runtime errors. To avoid these issues, you must render all Descope components exclusively on the client side. ### Dynamic Import with SSR Disabled Use the Next.js `dynamic` import function with `ssr: false` to load Descope components on the client side only. **Example:** ```tsx title="app/sign-in.tsx" 'use client'; import dynamic from 'next/dynamic'; const Descope = dynamic( () => import('@descope/nextjs-sdk').then(mod => mod.Descope), { ssr: false }, ); const SignInPage = () => { return ( console.log('Logged in user:', e.detail.user)} onError={(e) => console.error('Login error:', e.detail)} /> ); }; export default SignInPage; ``` ## Common Errors and Solutions If you see errors such as: ``` TypeError: Illegal constructor ``` or ``` ReferenceError: window is not defined ``` These indicate that you're attempting to render Descope components during SSR. Applying dynamic imports with `ssr: false` will resolve these issues. ## When to Use SSR Disabled Imports Always disable SSR for: - Descope authentication components (e.g., sign-in flows, widgets). - Components relying on browser-specific features or APIs. ## Impact on SEO and Performance Disabling SSR for specific authentication components generally does not impact SEO significantly, as these are typically used behind authenticated routes. Performance remains optimal as Next.js efficiently handles client-side hydration. ## Examples in Sample Projects Refer to the following examples for practical implementation: - [App Router Sample](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk/examples/app-router) - [Pages Router Sample](https://github.com/descope/descope-js/tree/main/packages/sdks/nextjs-sdk/examples/pages-router) ## Summary By dynamically importing Descope components with SSR disabled, you ensure smooth integration, optimal performance, and secure authentication experiences in your Next.js application. # Reading Session and User Data (/getting-started/nextjs/user-and-session-data) Learn how to read session and user data using the Descope Next.js SDK. # Reading Session and User Data The Descope Next.js SDK provides utilities for retrieving user session data in both the App Router (`app/`) and Pages Router (`pages/`). This guide covers: - Accessing session data on the client - Retrieving session data on the server - Differences between App Router and Pages Router - When to use `session()` vs. `getSession(req)` - Using `createSdk()` for backend authentication ## Client Side The Descope SDK provides hooks to access session/user data in client components. For more details, visit our [Auth Helpers](/client-sdk/auth-helpers) page. ### Displaying Logged-in User Information ```tsx 'use client'; import { useDescope, useSession, useUser } from '@descope/nextjs-sdk/client'; export default function Dashboard() { const { isAuthenticated, isSessionLoading } = useSession(); const { user } = useUser(); const sdk = useDescope(); if (isSessionLoading) { return

Loading...

; } if (!isAuthenticated) { return

You are not logged in

; } return (

Welcome, {user?.name}

); } ``` ## Server Side Session data can be retrieved inside API routes, Middleware, and Server Components using `session()` or `getSession(req)`. ### Comparison of Methods | Function | Use Case | Works in Middleware? | Works in API Routes? | Works in Server Components? | |---------------|----------|--------------------|--------------------|---------------------------| | `session()` | App Router, Middleware, Server Components | Yes | Yes | Yes | | `getSession(req)` | Pages Router API routes | No | Yes | No | ### Using `session()` in Middleware and App Router The `session()` function reads session data from cookies and headers. #### Protecting a Server Component ```tsx import { session } from '@descope/nextjs-sdk/server'; async function Dashboard() { const currentSession = await session(); if (!currentSession) { return

Access Denied

; } return

Welcome, {currentSession.token.sub}

; } ``` - Works in Server Components - Works in Middleware - Not dependent on `authMiddleware()`, but middleware is recommended ### Using `getSession(req)` in Pages Router API Routes `getSession(req)` retrieves session data in Next.js API routes (`pages/api/`). #### Protecting an API Route (Pages Router) ```ts import { getSession } from '@descope/nextjs-sdk/server'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { const currentSession = getSession(req); if (!currentSession) { return res.status(401).json({ message: 'Unauthorized' }); } return res.status(200).json({ user: currentSession.token.sub }); } ``` - Works only in Pages Router (`pages/api/`) - Do not use in Middleware (use `session()` instead) ## Using SDK in Server Components/API Routes For backend API interactions, use `createSdk()` to access the Descope Management API. #### Fetching User Data from the Descope Management SDK ```ts import { createSdk } from '@descope/nextjs-sdk/server'; const sdk = createSdk({ projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID, managementKey: process.env.DESCOPE_MANAGEMENT_KEY }); export async function GET() { const { ok, data: user } = await sdk.management.user.load('user123'); if (!ok) { return new Response('User not found', { status: 404 }); } return Response.json(user); } ``` ## When to Use Each Method | Use Case | App Router | Pages Router | Middleware | API Routes | |----------|-----------|--------------|------------|------------| | Client Hooks (`useSession`, `useUser`) | Yes | Yes | No | No | | Middleware (`authMiddleware()`) | Yes | Yes | Yes | No | | Session Function (`session()`) | Yes | No | Yes | Yes | | API Routes (`getSession(req)`) | No | Yes | No | Yes | | Management API (`createSdk()`) | Yes | Yes | Yes | Yes | ## Additional Considerations For more details on handling session and authentication events, refer to the [Client SDK Auth Helpers](/client-sdk/auth-helpers) documentation. This includes: - **Session and user event listeners** (`onSessionTokenChange`, `onIsAuthenticatedChange`, `onUserChange`) - **Handling session expiration and refresh** - **Redirecting users after logout** - **Managing authentication state across multiple tabs** By leveraging these utilities, you can ensure a seamless and secure authentication experience for users across your Next.js application. # Node.js (/getting-started/nodejs) Learn how to integrate Descope's Node.js SDK in your backend application. # Node.js Quickstart This guide will help you integrate Descope's Node.js SDK into your backend application. Follow the steps below to get started. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" yarn add @descope/node-sdk ``` ```sh title="Terminal" pnpm add @descope/node-sdk ``` ```sh title="Terminal" bun i @descope/node-sdk ``` ### Import and Setup Backend SDK You'll need import and setup all of the packages from the SDK. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to include `baseUrl` in the parameters (e.g. `{baseUrl : '__BaseURL__'}`) when you initialize `DescopeClient`. ```js title="index.js" import DescopeClient from '@descope/node-sdk'; try { const descopeClient = DescopeClient({ projectId: '__ProjectID__' }); } catch (error) { console.log("failed to initialize: " + error) } ``` For better security, you can use separate keys for auth-related vs. general management operations (principle of least privilege). Create both key types in the [Descope Console](https://app.descope.com/settings/project). ```js title="index.js" // For auth-related management operations (separated from general management) const authClient = DescopeClient({ projectId: 'my-project-ID', authManagementKey: 'auth-management-key', // create in Descope Console }); // For general management operations const managementClient = DescopeClient({ projectId: 'my-project-ID', managementKey: 'management-key', }); ``` ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always pass that value (or your custom audience) when validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. The `audience` parameter accepts a string or an array of strings. ```js title="index.js" // Fetch session token from HTTP Authorization Header const sessionToken = "xxxx"; try { const authInfo = await descopeClient.validateSession(sessionToken, { audience: '__ProjectID__' }); console.log("Successfully validated user session:"); console.log(authInfo); } catch (error) { console.log("Could not validate user session " + error); } ``` If you're interested in offline JWT validation, check out our [offline JWT validation guide](/sessions/validation/backend/offline-jwt-validation). Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [Node.js SDK](https://github.com/descope/node-sdk) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Custom Caching (/getting-started/php/custom-caching) Learn how to implement a custom caching mechanism to store frequently accessed data. # Custom Caching Mechanism The Descope PHP SDK uses a caching mechanism to store frequently accessed data, like JWKs, for session token validation. By default APCu is used for caching, but if it is not available caching is disabled. In this case, a custom caching mechanism can be implemented using the `CacheInterface` that exists within the SDK. ## Custom Caching with `CacheInterface` You can provide a custom caching mechanism using `CacheInterface` through the SDK. The following methods are supported through the SDK: - `get(string $key)`: Retrieve a value by key. - `set(string $key, $value, int $ttl = 3600): bool`: Store a value with a time-to-live (TTL). - `delete(string $key): bool`: Remove a value by key. Here is an example of setup and use of Laravel's cache system with the Descope SDK: ```php title="LaravelCache.php" namespace App\Cache; use Descope\SDK\Cache\CacheInterface; use Illuminate\Support\Facades\Cache; class LaravelCache implements CacheInterface { public function get(string $key) { return Cache::get($key); } public function set(string $key, $value, int $ttl = 3600): bool { // Laravel TTL is in minutes return Cache::put($key, $value, max(1, ceil($ttl / 60))); } public function delete(string $key): bool { return Cache::forget($key); } } ``` ```php use Descope\SDK\DescopeSDK; use App\Cache\LaravelCache; $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'], ], new LaravelCache()); ``` ## Additional Resources - [PHP SDK](https://github.com/descope/descope-php) # PHP (/getting-started/php) Learn how to integrate Descope's PHP SDK in your backend application. # PHP Quickstart This guide will help you integrate Descope's PHP SDK into your backend application. The same SDK is also used with Laravel. Follow the steps below to get started. ### Install Backend SDK Install the SDK with `Composer` using the following command: ```sh title="Terminal" composer require descope/descope-php ``` ### Set up Environment file Create a `.env` file in the root directory of your project with your `Descope Project ID`, which can be found in the [Console](https://app.descope.com/settings/project) If you plan to use Management functions, include a `Descope Management Key` here as well, which can be found [here](https://app.descope.com/settings/company/managementkeys). ```php title=".env" DESCOPE_PROJECT_ID=__ProjectID__ DESCOPE_MANAGEMENT_KEY=DESCOPE_MANAGEMENT_KEY ``` ### Setup Backend SDK You'll need to initialize a `DescopeSDK` object using your Project ID. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to export the Base URL (e.g. `export DESCOPE_BASE_URI="__BaseURL__"`) when initializing `descope_client`. ```php require 'vendor/autoload.php'; use Descope\SDK\DescopeSDK; $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'], // Optional, only used for Management functions 'debug' => false, // Optional, logs failed API requests to your PHP error log (default: false) 'requestTimeout' => 10, // Optional, seconds (default: 60) ]); ``` You can optionally configure the SDK's HTTP request timeout using `requestTimeout`, specified in seconds. It defaults to 60 seconds. The SDK also applies a fixed 10-second connection timeout when establishing HTTP connections. ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. After `verify()` succeeds, check the claims (for example with `getClaims`) so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. The `$descopeSDK->verify($sessionToken)` function can be used to verify a user's session as shown below. This returns a TRUE or FALSE depending on if the JWT is valid or not. ```php if (isset($_POST["sessionToken"])) { if ($descopeSDK->verify($_POST["sessionToken"])) { $_SESSION["user"] = json_decode($_POST["userDetails"], true); $_SESSION["sessionToken"] = $_POST["sessionToken"]; session_write_close(); // User session validated and token saved } else { error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page } } else { error_log("Session token is not set in POST request."); // Redirect to login page } ``` By default, the SDK uses APCu for caching, provided it is enabled and configured in your environment. If APCu is not available, and no other caching mechanism is provided, caching is disabled. Read about custom caching for the Descope SDK [here](/getting-started/php/custom-caching). The SDK can log failed API requests to your PHP error log to help you troubleshoot. Logging is disabled by default. Read about [backend SDK logging](/backend-sdk/logging) to enable it. Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [PHP SDK](https://github.com/descope/descope-php) - [Laravel Sample App](https://github.com/descope-sample-apps/react-laravel-sample-app) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Python (/getting-started/python) Learn how to integrate Descope's Python SDK in your backend application. # Python Quickstart This guide will help you integrate Descope's Python SDK into your backend application. Follow the steps below to get started. ## Which Client Do You Need? Use `DescopeClient` for most apps. Use `DescopeClientAsync` if you're on `asyncio` (FastAPI, aiohttp, etc.). Both live in the same package and expose the same method names. With the async client, you will need to `await` the network calls. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" pip3 install descope ``` ### Import and Setup Backend SDK You'll need import and setup all of the packages from the SDK. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to export the Base URL (e.g. `export DESCOPE_BASE_URI="__BaseURL__"`) when initializing `descope_client`. ```python title="app.py" from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient ) try: descope_client = DescopeClient(project_id='__ProjectID__') except Exception as error: print("failed to initialize. Error:") print(error) ``` ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always pass that value (or your custom audience) when validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value for your application. ```python title="app.py" def validate_session(): try: jwt_response = descope_client.validate_session( # Fetch session token from HTTP Authorization Header session_token="xxxx", audience="__ProjectID__" ) print("Successfully validated user session:") print(jwt_response) except Exception as error: print("Could not validate user session. Error:") print(error) ``` If you're interested in offline JWT validation, check out our [offline JWT validation guide](/sessions/validation/backend/offline-jwt-validation). Once you've implemented the basic session validation, you can enhance your application with these additional features: `DescopeClientAsync` mirrors `DescopeClient`. Auth methods, management APIs, and return values are the same, so follow the other [non-async guide](/getting-started/python) and the existing SDK docs for those. The differences are how you create the client and which calls need `await`. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" pip3 install descope ``` ### Import and Setup Backend SDK Prefer an async context manager so connections are closed for you: ```python title="app.py" from descope import DescopeClientAsync async def main(): async with DescopeClientAsync(project_id='__ProjectID__') as descope_client: jwt_response = await descope_client.validate_session( session_token=session_token, audience="__ProjectID__", ) ``` Or create it yourself and close it when you're done: ```python title="app.py" descope_client = DescopeClientAsync(project_id='__ProjectID__') try: jwt_response = await descope_client.validate_session( session_token=session_token, audience="__ProjectID__", ) finally: await descope_client.aclose() ``` Pass `management_key` the same way as on the sync client when you need management APIs. ### Implement Session Validation Network calls are coroutines (`validate_session`, `refresh_session`, auth methods like `otp` / `oauth`, and `mgmt.*`). Helpers that only read an already-decoded JWT stay sync, such as `validate_permissions` and `validate_roles`: ```python title="app.py" jwt_response = await descope_client.validate_session( session_token=session_token, audience="__ProjectID__", ) ok = descope_client.validate_permissions(jwt_response, ["Permission to validate"]) ``` To move from sync to async, swap the client class and add `await` on network calls. ## Additional Resources - [Python SDK](https://github.com/descope/python-sdk) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # Django (/getting-started/react/django) Learn how to integrate Descope with your React & Django in your application. # React & Django Quickstart This is a quickstart guide to help you integrate Descope with your React & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/react/go) Learn how to integrate Descope with your React & Go in your application. # React & Go Quickstart This is a quickstart guide to help you integrate Descope with your React & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# React (/getting-started/react) Get started with React in minutes with Descope. # React Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- If you're using an AI-enhanced developer tool (Cursor, Claude Code, Copilot in VS Code, Windsurf, and similar), we recommend downloading our [Rules file for React](https://github.com/descope/ai/blob/main/rules/client-sdks/descope-react.mdc) and placing it in the `/rules` directory of your project. This file contains structured integration instructions and code examples for the Descope React SDK. This is a quickstart guide to help you integrate Descope with your React application. Follow the steps below to get started. The standalone [`descope/react-sdk`](https://github.com/descope/react-sdk) repository is deprecated. The React SDK now lives at [`descope/descope-js`](https://github.com/descope/descope-js/tree/main/packages/sdks/react-sdk). The npm package name (`@descope/react-sdk`) remains the same — but please file any issues against the new repo. ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/react/java) Learn how to integrate Descope with your React & Java in your application. # React & Java Quickstart This is a quickstart guide to help you integrate Descope with your React & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/react/nodejs) Learn how to integrate Descope with your React & Node.js in your application. # React & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your React & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/react/php) Learn how to integrate Descope with your React & PHP in your application. # React & PHP Quickstart This is a quickstart guide to help you integrate Descope with your React & PHP application. Follow the steps below to get started. Check out our [sample app](https://github.com/descope-sample-apps/react-laravel-sample-app) using React and Laravel for an easy place to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/react/python) Learn how to integrate Descope with your React & Python in your application. # React & Python Quickstart This is a quickstart guide to help you integrate Descope with your React & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# OIDC Client Login (/getting-started/react/react-oidc) Learn how to integrate Descope with a React application, using OIDC and our React SDK. # React OIDC Client Quickstart This guide walks you through how to integrate Descope into your React app as an **OIDC client**, using redirect-based login with [Federated Apps](/identity-federation/applications). ### Install the SDK Install the Descope SDK for React using your preferred package manager: ```sh npm install @descope/react-sdk ``` ```sh yarn add @descope/react-sdk ``` ```sh pnpm add @descope/react-sdk ``` ```sh bun install @descope/react-sdk ``` ### Wrap Your App with `AuthProvider` In your root component (e.g., `App.tsx`), wrap your app in `AuthProvider` and configure OIDC by enabling `oidcConfig`. ```tsx title="App.tsx" import { AuthProvider } from '@descope/react-sdk'; import AppRoutes from './AppRoutes'; export default function AppRoot() { return ( ); } ``` You may also pass a full `oidcConfig` object to customize settings such as `redirectUri`, `scope`, and `applicationId`. Read more in our Client SDK docs section. ### Trigger Login with Redirect Use the `useDescope()` hook and call `sdk.oidc.loginWithRedirect()` to start the login process. ```tsx title="LoginButton.tsx" import { useDescope } from '@descope/react-sdk'; export default function LoginButton() { const sdk = useDescope(); return ( ); } ``` #### `loginWithRedirect` Parameters You can pass any of the following parameters into the `loginWithRedirect` function to provide additional info to the OIDC provider: - `redirect_uri`: A custom URI that overrides the default redirect after login. This will default to being the current URL. - `login_hint`: A hint to Descope of the user identifier (e.g., email address). This will pre-fill the `form.externalId` context key in the flow you're redirected to. ### Redirect Back and Automatic Session Handling After a successful login, the OIDC provider will redirect the user to your app. The `AuthProvider` will automatically process the returned tokens and establish a session. No additional code is needed on the redirect page — just ensure the app is wrapped with `AuthProvider`. ### Access User and Session Info You can use `useSession()` and `useUser()` to access authentication state and user info. ```tsx title="Dashboard.tsx" import { useSession, useUser } from '@descope/react-sdk'; export default function Dashboard() { const { isAuthenticated, isSessionLoading } = useSession(); const { user, isUserLoading } = useUser(); if (isSessionLoading || isUserLoading) return

Loading...

; if (isAuthenticated) { return

Welcome, {user.name}

; } return

You are not logged in

; } ``` ### Logout the User To log out, you can call `sdk.logout()` for local session clearing, or `sdk.oidc.logout()` for a full OIDC-compliant logout with redirect: ```tsx title="LogoutButton.tsx" import { useDescope } from '@descope/react-sdk'; export default function LogoutButton() { const sdk = useDescope(); return ( <> ); } ``` ### You're All Set! Your React app now uses Descope as an OIDC provider with redirect-based login, logout, and session management.
# Ruby (/getting-started/react/ruby) Learn how to integrate Descope with your React & Ruby in your application. # React & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your React & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Django (/getting-started/react-native/django) Learn how to integrate Descope with your React Native & Django in your application. # React Native & Django Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/react-native/go) Learn how to integrate Descope with your React Native & Go in your application. # React Native & Go Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# React Native (/getting-started/react-native) Get started with React Native in minutes with Descope. # React Native Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- This is a quickstart guide to help you integrate Descope with your React Native application. Follow the steps below to get started. You can also refer our [React Native Sample App](https://github.com/descope-sample-apps/react-native-sample-app) for a complete implementation of [Native Flows](/mobile-sdk/native-vs-browser-flows) with Descope. Expo is supported; however, to use the [React Native SDK](https://github.com/descope/descope-react-native), you need a [custom development build](https://docs.expo.dev/develop/development-builds/introduction/). Refer to our [example repo](https://github.com/descope/descope-react-native/tree/main/example-expo) for a working example. [Expo Go](https://docs.expo.dev/get-started/expo-go/) is a pre-built app and cannot load the native modules from this SDK. If you use Expo Go, you can still integrate Descope using the [Expo OIDC](https://www.descope.com/blog/post/expo-authentication) approach instead of this SDK. ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/react-native/java) Learn how to integrate Descope with your React Native & Java in your application. # React Native & Java Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/react-native/nodejs) Learn how to integrate Descope with your React Native & Node.js in your application. # React Native & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/react-native/php) Learn how to integrate Descope with your React Native & PHP in your application. # React Native & PHP Quickstart This is a quickstart guide to help you integrate Descope with your React Native & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/react-native/python) Learn how to integrate Descope with your React Native & Python in your application. # React Native & Python Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/react-native/ruby) Learn how to integrate Descope with your React Native & Ruby in your application. # React Native & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your React Native & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/ruby) Learn how to integrate Descope's Ruby SDK in your backend application. # Ruby Quickstart This guide will help you integrate Descope's Ruby SDK into your backend application. Follow the steps below to get started. ### Install Backend SDK Install the SDK with the following command: ```sh title="Terminal" gem install descope ``` ### Import and Setup Backend SDK You'll need import and setup all of the packages from the SDK. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain) with your Descope project, make sure to export the Base URL (e.g. `export DESCOPE_BASE_URI="__BaseURL__"`) when initializing `descope_client`. ```ruby title="app.rb" require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__' } ) ``` ### Implement Session Validation You will need to then fetch the session token from the Authorization header of each request, and use the SDK to validate the token. The frontend SDK will store the session token in either a cookie or your browser's local storage. If using a cookie, the token will be sent to your app server automatically with every request. By default, the `aud` claim in your session token is your Descope Project ID. Always pass that value (or your custom audience) when validating, so you only accept tokens issued for your application. You can change the audience in a [JWT Template](/management/token/jwt-templates) if you need a custom value. ```ruby title="app/controllers/application_controller.rb" class ApplicationController < ActionController::Base before_action :validate_session private def validate_session session_token = request.headers['Authorization'] return head :unauthorized unless session_token begin jwt_response = $descope_client.validate_session( session_token: session_token, audience: '__ProjectID__' ) @current_user = jwt_response rescue Descope::AuthException head :unauthorized end end end ``` If you're interested in offline JWT validation, check out our [offline JWT validation guide](/sessions/validation/backend/offline-jwt-validation). Once you've implemented the basic session validation, you can enhance your application with these additional features: ## Additional Resources - [Python SDK](https://github.com/descope/descope-ruby-sdk) - [Management Docs](/management) - [User Migration Guide](/migrate) ## Have You Implemented the Frontend Yet? When integrating Descope into your application, you have **three options** depending on how much control you want over your frontend authentication experience and session management: | Option | Description | Best For | |:-------|:------------|:---------| | **Use Descope Flows** | Design your authentication screens and flows visually in the Descope Console with little or no frontend code. We handle all session management for you. | Fastest setup with minimal custom frontend work. | | **Use Descope Client SDKs** | Build your own login screens and authentication experiences in your frontend using code, while relying on Descope's SDKs to manage sessions (login, logout, refresh). | Customizable UX with simplified session handling. | | **Use Descope Backend SDKs** | Build your own frontend *and* your own backend APIs for authentication. You fully manage sessions, tokens, and authentication logic yourself. | Maximum flexibility and control, at the cost of more engineering effort. | # SvelteKit (/getting-started/sveltekit) Learn to integrate Descope with SvelteKit in your application. # SvelteKit Quickstart This is a quickstart guide to help you integrate Descope with your SvelteKit application. Follow the steps below to get started. ### Install SvelteKit Auth Install the required dependencies: ```sh title="Terminal" npm install @auth/sveltekit ``` ```sh title="Terminal" yarn add @auth/sveltekit ``` ```sh title="Terminal" pnpm add @auth/sveltekit ``` ```sh title="Terminal" bun add @auth/sveltekit ``` ### Configure Auth.js with Descope To enable authentication in SvelteKit, import `SvelteKitAuth` and set up Descope as an authentication provider. Ensure environment variables are loaded to securely manage credentials. Then, configure Descope within `SvelteKitAuth` to handle authentication seamlessly. Create or update your `src/hooks.server.ts` file: ```tsx title="src/hooks.server.ts" import { SvelteKitAuth } from "@auth/sveltekit"; import Descope from "@auth/core/providers/descope"; import { AUTH_DESCOPE_ID, AUTH_DESCOPE_SECRET, AUTH_DESCOPE_ISSUER } from "$env/static/private"; export const handle = SvelteKitAuth({ providers: [ Descope({ clientId: AUTH_DESCOPE_ID, clientSecret: AUTH_DESCOPE_SECRET, issuer: AUTH_DESCOPE_ISSUER }) ] }); ``` - `AUTH_DESCOPE_ID`: Can be found in your Descope account under the [Project Settings](https://app.descope.com/settings/project). - `AUTH_DESCOPE_SECRET`: Can be generated in your Descope account under the [Access Keys](https://app.descope.com/accesskeys) page. - `AUTH_DESCOPE_ISSUER`: Can be found in your Descope account under the [Applications page](https://app.descope.com/applications). You can get all the above required links by going to the [Federated Apps](https://app.descope.com/applications) in the Descope Console. ### Add TypeScript Types (Optional but Recommended) This tells TypeScript that session data exists in Locals and PageData. It helps prevent TypeScript errors while working with authentication. Update your `src/app.d.ts`: ```tsx title="src/app.d.ts" declare global { namespace App { interface Locals { session: import("@auth/core").Session | null; } interface PageData { session: import("@auth/core").Session | null; } } } export {}; ``` ### Basic Authentication Component The authentication flow works by checking whether a user is signed in. If they are, their email and a logout button are displayed; otherwise, a login button is shown. Clicking "Sign in with Descope" initiates the login process, while clicking "Sign out" logs the user out, ensuring a seamless authentication experience. ```tsx title="src/routes/+page.svelte" {#if $page.data.session}

Signed in as {$page.data.session.user?.email}

{:else} {/if} ``` ### Protected API Route If the user is not logged in, the system returns a 401 Unauthorized error. However, if the user is authenticated, it responds with a JSON object containing the user's details. ```tsx title="src/routes/api/protected/+server.ts" import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; export const GET: RequestHandler = async ({ locals }) => { if (!locals.session) { throw error(401, 'Unauthorized'); } return new Response(JSON.stringify({ message: 'This is a protected API route', user: locals.session.user })); }; ``` ### Protected Page Route If the user is not logged in, they are redirected to the login page `/auth/signin`. If they are logged in, their details are loaded. ```typescript title="src/routes/protected/+page.server.ts" import { redirect } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ locals }) => { if (!locals.session) { throw redirect(303, '/auth/signin'); } return { user: locals.session.user }; }; ``` To set up a Descope Federated App, log into the [Descope Console](https://app.descope.com/) and navigate to [Applications](https://app.descope.com/applications) > Federated Apps to create a new app. You will find all your credentials here in the Federated Apps. ### Custom Callback URL (Advanced Configuration) You can customize the callback URL in your configuration: ```typescript title="src/hooks.server.ts" export const handle = SvelteKitAuth({ providers: [ Descope({ clientId: AUTH_DESCOPE_ID, clientSecret: AUTH_DESCOPE_SECRET, issuer: AUTH_DESCOPE_ISSUER }) ], callbacks: { async session({ session, token }) { return session; }, async jwt({ token, user }) { return token; } } }); ``` ### Congratulations Now that you've got the authentication down, go focus on building out the rest of your app! If you're curious to learn more, take a look at this [Svelte Sample App](https://github.com/descope-sample-apps/svelte-sample-app). --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Django (/getting-started/swift/django) Learn how to integrate Descope with your Swift & Django in your application. # Swift & Django Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/swift/go) Learn how to integrate Descope with your Swift & Go in your application. # Swift & Go Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Swift (/getting-started/swift) Get started with Swift in minutes with Descope. # Swift Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- This is a quickstart guide to help you integrate Descope with your Swift application. Follow the steps below to get started. To learn more about how Native Flows work and how they provide a seamless, in-app authentication experience, see [Native Flows](/mobile-sdk/native-vs-browser-flows). ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/swift/java) Learn how to integrate Descope with your Swift & Java in your application. # Swift & Java Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/swift/nodejs) Learn how to integrate Descope with your Swift & Node.js in your application. # Swift & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/swift/php) Learn how to integrate Descope with your Swift & PHP in your application. # Swift & PHP Quickstart This is a quickstart guide to help you integrate Descope with your Swift & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/swift/python) Learn how to integrate Descope with your Swift & Python in your application. # Swift & Python Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/swift/ruby) Learn how to integrate Descope with your Swift & Ruby in your application. # Swift & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your Swift & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# TanStack Router (/getting-started/tanstack-router) Add Descope authentication to a client-side TanStack Router application using the Descope React SDK. # TanStack Router Quickstart This guide shows how to add Descope authentication to a [TanStack Router](https://tanstack.com/router) application using the [`@descope/react-sdk`](https://www.npmjs.com/package/@descope/react-sdk). TanStack Router, scaffolded here with Vite, renders entirely in the browser. Your Descope's Flows (the `` web component) and the React SDK hooks work directly with your app, with no SSR pass to guard against. The client creates and reads the session, and the React SDK hooks gate route access. If you're using an AI-powered IDE or any AI tools, check out our [Descope MCP server](/mcp/mcp-server). ### Install the React SDK Install the Descope React SDK in your TanStack Router project: ```sh title="Terminal" npm i --save @descope/react-sdk ``` ```sh title="Terminal" yarn add @descope/react-sdk ``` ```sh title="Terminal" pnpm add @descope/react-sdk ``` ```sh title="Terminal" bun i @descope/react-sdk ``` ### Wrap your app with `AuthProvider` Wrap the `RouterProvider` with `` in your app entry point (`src/main.tsx`) so the Descope SDK context is available to every route. You need your **Project ID** for this step, which you can find on the [project page](https://app.descope.com/settings/project) of your Descope Console. If you use a [custom domain (CNAME)](/how-to-deploy-to-production/custom-domain), also pass a base URL to ``. ```tsx title="src/main.tsx" import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { RouterProvider, createRouter } from '@tanstack/react-router'; import { AuthProvider } from '@descope/react-sdk'; import { routeTree } from './routeTree.gen'; const router = createRouter({ routeTree }); declare module '@tanstack/react-router' { interface Register { router: typeof router; } } const rootElement = document.getElementById('app')!; if (!rootElement.innerHTML) { const root = ReactDOM.createRoot(rootElement); root.render( , ); } ``` You can customize client-side token behavior using optional parameters like `persistTokens`, and `sessionTokenViaCookie`. Learn more in the [Auth Helpers](/client-sdk/auth-helpers) documentation. ### Render the flow and read the session Render the `` flow to sign users in, and use `useSession()` to read authentication state. Because everything runs in the browser, you can render `` directly - no SSR guard required. You can customize the flow component with the following: - `flowId`: ID of the flow you wish to use - `onSuccess` and `onError`: functions that execute when authentication succeeds or fails For the full list of component customization options, refer to our [Descope Components Doc](/client-sdk/descope-components). ```tsx title="src/routes/index.tsx" import { createFileRoute } from '@tanstack/react-router'; import { Descope, useSession, useUser } from '@descope/react-sdk'; export const Route = createFileRoute('/')({ component: HomePage, }); function HomePage() { // `isSessionLoading` is true until the SDK resolves the session on the // client; render a loading state to avoid content flicker. const { isAuthenticated, isSessionLoading } = useSession(); // `user` comes from useUser(), not useSession(). const { user } = useUser(); if (isSessionLoading) return

Checking authentication…

; if (!isAuthenticated) { return ( console.log('Signed in:', e.detail.user)} onError={(e) => console.log('Could not log in!', e)} /> ); } return

Welcome back, {user?.name ?? user?.email}!

; } ``` Descope provides hooks and functions to read the session and user state so you can customize the user experience: - `isAuthenticated`: Boolean for the authentication state of the current user session. - `isSessionLoading`: Boolean for the loading state of the session. Use it to display a loading message while the session is still resolving. - `useUser`: Returns information about the currently authenticated user. - `getSessionToken`: Returns the JWT for the current session. For the full list of available hooks and functions, refer to the [Auth Helpers Doc](/client-sdk/auth-helpers). ### Protect routes with `beforeLoad` The component-level check above works, but the idiomatic TanStack Router approach is to guard at the *route* level so unauthenticated users are redirected before a protected route ever renders. Do this with a [pathless layout route](https://tanstack.com/router/latest/docs/framework/react/guide/authenticated-routes) and its `beforeLoad` hook. Because `beforeLoad` runs outside React render, you cannot call the `useSession()` hook there. Use `getSessionToken()` instead - it is a plain function that reads the token synchronously. ```tsx title="src/routes/_authenticated.tsx" import { Outlet, createFileRoute, redirect } from '@tanstack/react-router'; import { getSessionToken } from '@descope/react-sdk'; export const Route = createFileRoute('/_authenticated')({ beforeLoad: ({ location }) => { if (!getSessionToken()) { // Send unauthenticated users to /login, remembering where they were // headed so we can return them there after they sign in. throw redirect({ to: '/login', search: { redirect: location.href } }); } }, component: () => , }); ``` Any route nested under this layout is now protected - for example, a page at `src/routes/_authenticated/dashboard.tsx` renders only when a session token is present. Move the `` flow to its own `/login` route (rather than rendering it conditionally as in the previous step). After a successful sign-in, call `router.invalidate()` so the guard re-runs with the new session, then send the user to their original destination. ```tsx title="src/routes/login.tsx" import { createFileRoute, useRouter } from '@tanstack/react-router'; import { Descope } from '@descope/react-sdk'; export const Route = createFileRoute('/login')({ validateSearch: (search) => ({ redirect: typeof search.redirect === 'string' ? search.redirect : '/', }), component: LoginComponent, }); function LoginComponent() { const router = useRouter(); const { redirect } = Route.useSearch(); return ( { // Re-run beforeLoad guards with the new session, then continue. await router.invalidate(); router.navigate({ to: redirect }); }} onError={(e) => console.log('Could not log in!', e)} /> ); } ``` `beforeLoad` guards prevent unauthenticated users from *seeing* a route, but they run in the browser and can be bypassed. They do not protect data - any backend must still validate the session token itself. ### Add logout Use `useDescope()` for auth operations such as `logout`. Read the session token with `getSessionToken()` when calling your own APIs. ```tsx title="src/components/AuthActions.tsx" import { useDescope, getSessionToken } from '@descope/react-sdk'; export default function AuthActions() { const sdk = useDescope(); const handleLogout = async () => { await sdk.logout(); }; // Example: how you'd call a protected backend. The client session is UX // only - the server must validate this token before returning data. const callApi = async () => { const sessionToken = getSessionToken(); await fetch('https://your-api.example.com/resource', { headers: { Accept: 'application/json', Authorization: `Bearer ${sessionToken}`, }, }); }; return ; } ``` Reading `isAuthenticated` on the client can improve UX, but does not protect data. It's generally better to send the session token to a backend that validates it, and return protected data from that instead. See [Backend session validation](/sessions/validation/backend), for more details. For a complete, runnable example, take a look at the [Descope TanStack Router sample app](https://github.com/descope-sample-apps/tanstack-descope-sample-app). --- ## Customize Now that you have authentication working, you can configure and personalize Descope - your brand, styles, and custom user authentication journeys. We recommend starting by customizing your user-facing screens, such as signup and login.
# Django (/getting-started/vue.js/django) Learn how to integrate Descope with your Vue.js & Django in your application. # Vue.js & Django Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Django application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Go (/getting-started/vue.js/go) Learn how to integrate Descope with your Vue.js & Go in your application. # Vue.js & Go Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Go application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Vue.js (/getting-started/vue.js) Get started with Vue.js in minutes with Descope. # Vue.js Quickstart This guide will only include the frontend integration. If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, select your backend technology below: --- If you're using an AI-enhanced developer tool (Cursor, Claude Code, Copilot in VS Code, Windsurf, and similar), we recommend downloading our [Rules file for Vue](https://github.com/descope/ai/blob/main/rules/client-sdks/descope-vue.mdc) and placing it in the `/rules` directory of your project. This file contains structured integration instructions and code examples for the Descope Vue SDK. This is a quickstart guide to help you integrate Descope with your Vue.js application. Follow the steps below to get started. ## Continue with Backend SDK If you would like to also handle [Session Management](/sessions/validation/backend) in your backend, keep on reading by selecting your backend technology below: --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Java (/getting-started/vue.js/java) Learn how to integrate Descope with your Vue.js & Java in your application. # Vue.js & Java Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Java application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Node.js (/getting-started/vue.js/nodejs) Learn how to integrate Descope with your Vue.js & Node.js in your application. # Vue.js & Node.js Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Node.js application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# PHP (/getting-started/vue.js/php) Learn how to integrate Descope with your Vue.js & PHP in your application. # Vue.js & PHP Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & PHP application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Python (/getting-started/vue.js/python) Learn how to integrate Descope with your Vue.js & Python in your application. # Vue.js & Python Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Python application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Ruby (/getting-started/vue.js/ruby) Learn how to integrate Descope with your Vue.js & Ruby in your application. # Vue.js & Ruby Quickstart This is a quickstart guide to help you integrate Descope with your Vue.js & Ruby application. Follow the steps below to get started. --- ## Customize Now that you have the end-to-end application working, you can choose to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc. We recommend starting with customizing your user-facing screens, such as signup and login.
# Bubble (/getting-started/web-development-platforms/bubble) If you're using Bubble to develop your web applications, this tutorial will walk you through how to integrate Descope into your Bubble site. # Descope With Bubble Using [Bubble](https://bubble.io/) to create web applications offers flexibility and control over your design and functionality. This guide will help you implement Descope's advanced authentication features into your Bubble project. There are two ways to integrate Descope into your Bubble application: 1. Using OIDC and the [Auth Hosting Application](/identity-federation/auth-hosting). This is the recommended integration as it syncs with Bubble's user management system 2. Embedding your Flow as an HTML component ## Implementing Descope with OIDC The Descope Auth Hosting Application is designed to host and run your Descope Flows without needing to implement any of our SDKs. Using this method logs the user into Bubble as a Bubble user allowing you to use Bubble's user actions and conditions. **This is the recommended integration method**. ### Setting up OIDC 1. Access your Bubble [editor](https://bubble.io/page), select your project, and navigate to the ***Plugins*** tab. 2. Install the ***API Connector*** plugin by clicking ***+ Add Plugins***. 3. Once installed click on ***Add Another API*** in the plugin. ![Installing API Plugin for Bubble](/assets/bubble-api-plugin.webp) 4. In Descope go to [Applications](https://app.descope.com/applications) and click on ***OIDC default application***. 5. In the ***Flow Hosting URL*** field you can change the flow parameter to match the flow ID of the flow you want to be displayed. ![Setting Up Descope OIDC Application](/assets/bubble-oidc-app.webp) 6. Go to [Access Keys](https://app.descope.com/m2m/accessKeys) and create an access key. Make sure to copy the access key to clipboard before closing it. ![Creating Descope Access Key for Bubble](/assets/bubble-access-keys.webp) 7. In the Bubble API plugin expand the API you added: * Change Authentication to ***OAuth2 User-Agent Flow*** * Paste the Access key you copied into ***App Secret*** * Go to [Project Settings](https://app.descope.com/settings/project) and copy the Project ID into ***App ID*** * Paste `openid profile email descope.claims` into ***Scope*** * Paste `__BaseURL__/oauth2/v1/authorize` into ***Login dialog redirect*** * Paste `__BaseURL__/oauth2/v1/token` into ***Access token endpoint*** * Paste `__BaseURL__/oauth2/v1/userinfo` into ***User profile endpoint*** * Paste `sub` into ***User ID key path*** * Make sure the checkboxes below are checked ![Setting Up Bubble API to Descope](/assets/bubble-api-plugin-setup.webp) If you have a custom domain set up in Descope, you can replace the `api.descope.com` in the URLs for the endpoints above with your custom domain. To learn more about CNAME see [here](/how-to-deploy-to-production/custom-domain) ### Setting up Login/Sign-Up in Bubble 1. Add a button to your Bubble app, then add a workflow to the button. This will be the button that initiates login/sign-up. 2. In the workflow add an action ***Account->Sign-up/login with a social network***. For OAuth provider click on the API you created. 3. After the user completes this step they will be logged into Descope and Bubble, you can add any additional navigation logic or actions. 4. You must initialize you Descope API by previewing your Bubble app with `?debug_mode=true` in the URL and click the button you created. You should get an API initialized message from Bubble. ![Initializing the Descope API](/assets/bubble-api-success.webp) 5. Because the user is now logged in as a Bubble user you can use Bubble techniques to restrict pages and data to authenticated users only. Some examples include: * Creating a workflow on protected pages that sends users to the log-in page if they are not logged in. * Grouping all sensitive authenticated user data and elements and displaying them conditionally if a user is logged in and making sure ***This element is visible by page load*** is unchecked. * You can use any of the same workflows you would if you were using basic Bubble authentication. ### Updating Roles and Permissions in Bubble If you have your Roles/Permissions set up as Option Sets in Bubble, you can get a users roles and permissions from Descope when they log in. 1. In the API Plugin go to the Descope API you created and click ***Add Another Call***. 2. Add the user info endpoint you used earlier `__BaseURL__/oauth2/v1/userinfo`. Make sure the API is set to ***Use as Data*** 3. Once you've initialized the main API by running your Bubble app in debug mode click on ***Initialize call*** in the API plugin. ![Adding User Info API Call to API Plugin](/assets/bubble-add-user-info-call.webp) 4. In the response you can set ***Roles*** as your roles option set and ***Permissions*** as your permissions option set and then click ***Save***. ![Setting Roles and Permissions in Response](/assets/bubble-set-roles-api.webp) 5. Now you can add the ***Change Thing*** action to your login workflow. Set the ***Thing to change*** to `Current User`. Add an additional field for roles or permissions, use `get data from API` and select the Descope API User Info Call. After this you can handle the logic to suit your roles and permissions setup. ![Roles and Permissions workflow update](/assets/bubble-add-roles-workflow.webp) 6. Now when your user logs in, the roles and permissions will update in Bubble to match the ones set in Descope. ![Before and After log in with Roles sync](/assets/bubble-user-roles-populate.webp) ### Logging out of Bubble Because the user is logged in as a Bubble User you can use Bubble's actions to log the user out. 1. In your logout workflow you can use the ***Log the user out*** action. 2. If you want to make sure users need to reauthenticate every time they are logged out of Bubble, go to [Applications](https://app.descope.com/applications) and click ***Force Authentication***. This will make sure that if they are logged out of Bubble they will no be automatically logged in. ![Enabling Force Authentication in the Descope Application](/assets/bubble-force-auth-bubble.webp) ## Implement Descope as HTML Component ### Getting Started with Bubble Settings 1. Access your Bubble [editor](https://bubble.io/page), select your project, and navigate to the ***Settings*** tab. 2. Go to the ***SEO/meta tags*** section and scroll down to ***Script/meta tags in header***: 3. Insert the Descope SDK script into this section to enable the authentication features across your site: ```html ``` ### Setting Up the Login/Sign-Up Page 1. Return to your Bubble editor and create a new page by clicking ***New Page***. Name it ***Login***. 2. Drag a **HTML element** onto the canvas from the **Visual Elements** panel: 3. Paste the following HTML and script into the HTML element: ```html ``` This will create a login/sign-up interface on your page, and handle user authentication and redirection upon successful login. Here is the updated section that includes a note on how to protect only specific pages instead of the entire site using the footer script: ### Managing Unauthorized Access Ensure users who are not authenticated are redirected to the login page: 1. In the same Settings tab under ***SEO/meta tags***, scroll to ***Script to run in the page footer*** and add this code: ```html ``` This script ensures that only specific pages (as listed in `protectedPages`) require the user to be authenticated. If the user is not authenticated or their session has expired, they will be redirected to the login page. Adjust the `protectedPages` array to include the paths of the pages you want to protect. This approach allows you to selectively protect pages, giving you greater control over which parts of your Bubble application require user authentication. ### Displaying User Details in Your Frontend To fetch and display user details after authentication: ```javascript title="getProfile.js" const sessionToken = sdk.getSessionToken(); if (sessionToken) { getProfile(); } async function getProfile() { const profile = await sdk.me(sdk.getRefreshToken()); document.getElementById('userName').innerText = profile.data.name; document.getElementById('userEmail').innerText = profile.data.email; } ``` ### Final Thoughts Integrating Descope into your Bubble application provides robust authentication capabilities, seamlessly integrated into your no-code development workflow. For additional features like logout functionality or backend session validation, please refer to our comprehensive documentation: 1. Logout Using Client SDK - [here](/sessions/management/web#logout-using-client-sdk) 2. Backend Session Validation - [here](/sessions/validation/backend) 3. Roles and Permissions - This can be found in Step 7 of the guide [here](/getting-started/web-development-platforms#using-a-descope-sdk-and-web-component) If you have any questions or require further assistance, please contact our support team at [Descope](/support). This guide should now enable you to integrate Descope with your Bubble platform smoothly and efficiently. # FlutterFlow (/getting-started/web-development-platforms/flutterflow) If you're using FlutterFlow to develop your web applications, this tutorial will walk you through how to use Descope for your in app authentication. # FlutterFlow [FlutterFlow](https://www.flutterflow.io/) provides a simple, low-code platform for building Flutter applications. This guide walks you through integrating Descope authentication into your FlutterFlow project. We'll use [OIDC Endpoints](/getting-started/oidc-endpoints) along with the default OIDC federated application available in the [Descope Console](https://app.descope.com/applications/descope-default-oidc) to complete the integration. With the following setup, you will be able to build secure login flows using Descope's visual editor, support advanced features like SSO, passwordless login, and MFA, and manage users and sessions through Descope's platform all within your FlutterFlow application. ## Step 1: Configure Authentication in FlutterFlow Start by identifying the key pages in your app where authentication will occur. At a minimum, you'll need: * A **login page** to initiate the authentication process * A **redirect/loading page** to handle the redirect after login * A **post-login page** (e.g., home or dashboard) to navigate to once authentication is complete Custom authentication should then be enabled for the FlutterFlow project. In your FlutterFlow project settings: 1. Go to the **Authentication** section 2. Enable authentication 3. Set the **Authentication Type** to **Custom** 4. Assign the **Entry Page** to your login page 5. Assign the **Logged In Page** to your post-login destination ![Flutterflow authentication settings](/assets/flutterflow-auth-settings.webp) ## Step 2: Use Descope's OIDC Endpoints To authenticate using our OIDC endpoints, you must use the endpoints included under the default OIDC app in the [Console](https://app.descope.com/applications/descope-default-oidc). First, use `/authorize` to complete the flow and receive an authorization code. Then, the `/token` endpoint to receive the access, refresh, and id tokens for the user. These endpoints can be found as part of the default OIDC application that exists under the [Federated Apps](https://app.descope.com/applications) section of the Descope Console. They are listed under `SP Configuration`. These URLs will use your custom domain if it is configured for your Descope project. Read the [Custom Domain docs](/how-to-deploy-to-production/custom-domain) to learn more. ![OIDC endpoints available in Descope](/assets/oidc-endpoint-location.webp) This is the action flow for the login page on FlutterFlow: ![Flutterflow actions for login page](/assets/flutterflow-login-action.webp) The first step in starting the login process is generating PKCE values for the OIDC token exchange. ## Step 3: Generate PKCE Values To securely use OIDC, you'll implement PKCE (Proof Key for Code Exchange). To generate the PKCE values in FlutterFlow, create a custom action in the Custom Code section of the FlutterFlow app. This custom action should add the `pkce` dependency by adding this line `pkce: ^1.1.0` to the right hand side of the screen. The action should then contain the following code: ```dart // Automatic FlutterFlow imports import '/backend/schema/structs/index.dart'; import '/flutter_flow/flutter_flow_theme.dart'; import '/flutter_flow/flutter_flow_util.dart'; import '/custom_code/actions/index.dart'; // Imports other custom actions import '/flutter_flow/custom_functions.dart'; // Imports custom functions import 'package:flutter/material.dart'; // Begin custom action code // DO NOT REMOVE OR MODIFY THE CODE ABOVE! import 'dart:math'; import 'package:pkce/pkce.dart'; Future pkce() async { const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; final random = Random.secure(); final length = 32; final pkcePair = PkcePair.generate(); final state = List.generate(length, (_) => charset[random.nextInt(charset.length)]) .join(); debugPrint(pkcePair.codeChallenge); return { "codeVerifier": pkcePair.codeVerifier, "codeChallenge": pkcePair.codeChallenge, "state": state }; } ``` This will generate the code challenge, verifier, and state needed for the OIDC Token Exchange. It should then be formatted into an App State variable as a custom data type under the `App Values` section of the FlutterFlow app. This variable should be persisted so that the code verifier is still available after returning from the flow. ## Step 4: Launch the Authorization Request A authorization code is returned as a parameter in the URL after redirecting from a flow from the `/authorize` endpoint. The sign in button on your login page should use the `Launch URL` action with the following URL: ```http __BaseURL__/oauth2/v1/authorize ?response_type=code &client_id={DESCOPE_PROJECT_ID} &redirect_uri={YOUR_REDIRECT_URI} &scope=openid &code_challenge={PKCE_CODE_CHALLENGE} &code_challenge_method=S256 &state={STATE} ``` This URL should use your custom domain if it is configured for your Descope project. Read the [Custom Domain docs](/how-to-deploy-to-production/custom-domain) to learn more. The following parameters need to be completed: - **Client ID**: This is your Descope Project ID which can be found in the [Console](https://app.descope.com/settings/project). - **Redirect URI**: This is where you want the flow to redirect to, this guide will create a `/loading` page on the FlutterFlow app to redirect to. - **PKCE Code Challenge**: Must be generated on every authentication, read above section. - **State**: Used to prevent CSRF, generated on every authentication, read above section. After this URL launches and the flow runs, it will redirect back to the URI specified in the authorization request. In this guide, it will redirect to a loading page that will continue the authentication process as soon as the page loads. ## Step 5: Exchange the Code for Tokens ![Flutterflow actions for loading page](/assets/flutterflow-loading-action.webp) After redirecting back to the app from the flow, you should be on the loading page. This loading page should have the action flow trigger `On Page Load` to make the API call to the `/token` endpoint. This API call should be created in the API call section of FlutterFlow as a `POST` request to `__BaseURL__/oauth2/v1/token`. The API call should have the following headers and body: **Headers**: - **Content-Type**: `application/x-www-form-urlencoded`. - **Authorization**: Basic, this should be a base64 encoded version of the string `YOUR_DESCOPE_PROJECT_ID:`. **Body**: - **grant_type**: `authorization_code`. - **code**: This comes from the parameter in the URL upon redirect from the flow. - **redirect_uri**: Should be exactly the same as the redirect URI used for `/authorize`. - **client_id**: This is the Descope Project ID. - **codeVerifier**: This is generated at the same time as the PKCE Code Challenge, more details on PKCE generation below. ## Step 6: Store and Use Token Response This request will return a response containing an access token, refresh token, id token, and a token expiry time. This is all necessary for logging in using FlutterFlow's custom authentication. The response from the `/token` endpoint is stored as a Page State variable in a custom data type created on the Data Schema page in FlutterFlow. The Response data type should include the following: - **access_token**: String - **refresh_token**: String - **expires_in**: Integer - **id_token**: String - **token_type**: String - **scope**: String The tokens from the response are then passed into FlutterFlow's custom authentication log in action. When the `expires_in` value is passed into the action, it needs to be converted into the DateTime in seconds from Epoch that the tokens will expire rather than time until expiration. This can be done with a custom function and the code below: ```dart DateTime incrementExpiryTime(int seconds) { DateTime now = DateTime.now(); return now.add(Duration(seconds: seconds)); } ``` ## Step 7: Configuring Token Refresh The access token generated on login expires after 10 minutes by default so to maintain the session past 10 minutes, the refresh token must be used to generate a new access token. In FlutterFlow, create a new API call to the `/token` endpoint with the following headers and body: **Headers**: - **Content-Type**: `application/x-www-form-urlencoded`. - **Authorization**: Basic, this should be a base64 encoded version of the string `YOUR_DESCOPE_PROJECT_ID:`. **Body**: - **grant_type**: `refresh_token`. - **refresh_token**: This is the refresh token generated from the initial token exchange. This API call should be set up to run by the `On Page Load` trigger and will use the refresh token from the currently authenticated user. After receiving the API response, it should be handled like in Step 6. The new token values should then be passed into the `Update Authenticated User` action. ![Flutterflow actions for refreshing token](/assets/flutterflow-refresh-flow.webp) # Framer Workaround (/getting-started/web-development-platforms/framer-workaround) Workaround for Framer Plugin Issue # Framer Plugin Workaround Framer is currently having issues with their package management service, and some users may face errors when using our [Framer plugin](/getting-started/web-development-platforms/framer). To unblock developers while the Framer team is looking into a solution, we have published this workaround guide. ## The Issue The Framer plugin utilizes our React SDK to embed the flow component and handle session management. In certain Framer projects, the Framer package manager is unable to resolve the import of our React SDK and also resolve the other components on the page. ## The Workaround Although it requires some manual configuration, you can follow these steps to add Descope authentication to their Framer site, utilizing our WebJS and Web Component SDKs instead of React. Make sure to follow all of the steps completely: ### 1. Create Component File First, you will create a new code file in your Framer project. Call it `DescopeWebComponent.tsx` and select "New Component". ![create code file](/assets/framer_descope_web_component.webp) After creating the file, delete the contents of the file and paste the following instead: ``` typescript import { useEffect, useRef } from "react" const DescopeLogin = () => { const containerRef = useRef(null) const projectId = "__ProjectID__" // replace with your Descope Project ID const flowId = "" // replace with your Descope Flow ID useEffect(() => { // Dynamically load Descope scripts const loadScripts = async () => { const loadScript = (src) => new Promise((resolve, reject) => { const script = document.createElement("script") script.src = src script.async = true script.onload = resolve script.onerror = reject document.head.appendChild(script) }) try { await loadScript( "https://descopecdn.com/npm/@descope/web-component@3.21.0/dist/index.js" ) await loadScript( "https://descopecdn.com/npm/@descope/web-js-sdk@1.16.0/dist/index.umd.js" ) const sdk = window.Descope({ projectId: projectId, persistTokens: true, autoRefresh: true, }) const sessionToken = sdk.getSessionToken() const notValidToken = sessionToken ? sdk.isJwtExpired(sessionToken) : true if (!sessionToken || notValidToken) { const container = containerRef.current if (container && !container.querySelector("descope-wc")) { container.innerHTML = `` const wcElement = container.querySelector("descope-wc") wcElement.addEventListener("success", () => { sdk.refresh() }) wcElement.addEventListener("error", (err) => { console.error("Descope error:", err) }) } } } catch (error) { console.error("Error loading Descope scripts", error) } } loadScripts() }, []) return (
) } import React from "react" function App() { return (
) } export default App ``` Replace `__ProjectID__` with your Descope Project ID and `` with your Descope Flow ID, and save the file. ### 2. Insert Flow Component Now you can drag and drop your flow component from the Project Assets toolbar onto anywhere you'd like on your page. ![drag component](/assets/framer_drag_flow_component.webp) ### 3. Create Overrides File Now that you've added authentication to your page, you need a way to manage the authenticated user. We will achieve this using Framer Overrides. You will create a new code file in your Framer project. Call it `DescopeOverrides.tsx` and select "New Override". ![overrides file](/assets/framer_new_overrides.webp) After creating the file, delete the contents of the file and paste the following instead: ``` typescript "use client" import { Override } from "framer" import React, { useState, useEffect, useCallback } from "react" import type { ComponentType } from "react" declare global { interface Window { Descope: any } } const projectId = "__ProjectID__" // replace with your Descope Project ID const redirectPage = "" // replace with your Redirect URL const loadDescopeScripts = (): Promise => { return new Promise((resolve, reject) => { if (typeof window === "undefined") return reject("Window undefined") if (document.getElementById("descope-webjs-sdk")) { resolve() return } const webJsScript = document.createElement("script") webJsScript.id = "descope-webjs-sdk" webJsScript.src = "https://descopecdn.com/npm/@descope/web-js-sdk@1.16.0/dist/index.umd.js" webJsScript.async = true const webComponentScript = document.createElement("script") webComponentScript.id = "descope-web-component-sdk" webComponentScript.src = "https://descopecdn.com/npm/@descope/web-component@3.21.0/dist/index.js" webComponentScript.async = true webComponentScript.type = "module" webJsScript.onload = () => { document.head.appendChild(webComponentScript) webComponentScript.onload = () => resolve() webComponentScript.onerror = (e) => reject(e) } webJsScript.onerror = (e) => reject(e) document.head.appendChild(webJsScript) }) } let sdkInstance: any = null const initSDK = () => { if (typeof window === "undefined") return null if (!window.Descope) return null if (!sdkInstance) { sdkInstance = window.Descope({ projectId, persistTokens: true, autoRefresh: true, }) } return sdkInstance } const logout = async () => { if (!sdkInstance) return await sdkInstance.logoutAll() window.location.reload() } const ClientSideWrapper: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [isClient, setIsClient] = useState(false) useEffect(() => { setIsClient(true) }, []) return isClient ? <>{children} : null } const AuthWrapperContent = ({ Component, hidden, redirect, redirectURL, ...props }: { Component: ComponentType hidden: boolean redirect: boolean redirectURL: string [key: string]: any }) => { const [sdk, setSdk] = useState(null) const [isReady, setIsReady] = useState(false) const [isAuthenticated, setIsAuthenticated] = useState(false) useEffect(() => { const init = async () => { try { await loadDescopeScripts() const sdkInstance = initSDK() setSdk(sdkInstance) if (sdkInstance) { const sessionToken = sdkInstance.getSessionToken() const isExpired = sessionToken ? sdkInstance.isJwtExpired(sessionToken) : true if (!sessionToken || isExpired) { try { await sdkInstance.refresh() } catch (e) { console.warn("Token refresh failed", e) setIsAuthenticated(false) setIsReady(true) return } } const freshToken = sdkInstance.getSessionToken() const stillExpired = freshToken ? sdkInstance.isJwtExpired(freshToken) : true setIsAuthenticated(!!freshToken && !stillExpired) } else { setIsAuthenticated(false) } } catch (err) { console.error("Descope init failed", err) setIsAuthenticated(false) } finally { setIsReady(true) } } init() }, []) useEffect(() => { if (!isReady) return if (hidden && !isAuthenticated && redirect) { window.location.href = redirectURL } else if (!hidden && isAuthenticated && redirect) { window.location.href = redirectURL } }, [isReady, isAuthenticated, hidden, redirect, redirectURL]) if (!isReady) return
Loading...
if (hidden) { return isAuthenticated ? : null } else { return !isAuthenticated ? : null } } const LogoutButtonContent = ({ Component, ...props }: { Component: ComponentType }) => { const [isReady, setIsReady] = useState(false) useEffect(() => { loadDescopeScripts() .then(() => { initSDK() setIsReady(true) }) .catch(() => {}) }, []) const handleLogout = useCallback(() => { logout() }, []) if (!isReady) return null return } export const protectedComponent = ( Component: ComponentType ): ComponentType => { return (props) => ( ) } export const unprotectedComponent = ( Component: ComponentType ): ComponentType => { return (props) => ( ) } export const protectedPage = ( Component: ComponentType ): ComponentType => { return (props) => ( ) } export const unprotectedPage = ( Component: ComponentType ): ComponentType => { return (props) => ( ) } export const logoutButton = ( Component: ComponentType ): ComponentType => { return (props) => ( ) } export function RefreshSession(): Override { initSDK() // Call this at the top of the page via the override return {} } ``` Replace `__ProjectID__` with your Descope Project ID and `` with the URL of the page you want unauthenticated users to be redirected to (usually your login page), and save the file. ### 4. Apply Overrides You will have to apply a refresh override on **each page** of your Framer project so that the user's session token is correctly refreshed. To apply the refresh override: 1. Select a page 2. Navigate to "Code Overrides" at the bottom of the toolbar on the right 3. Select "DescopeOverrides" as the file, and "RefreshSession" as the override. The Descope Overrides file also includes overrides that you can utilize to protect your pages and components from unauthenticated users. You can learn more about those overrides [here](/getting-started/web-development-platforms/framer#apply-overrides). # Framer (/getting-started/web-development-platforms/framer) If you're using Framer to develop your web applications, this tutorial will walk you through how to integrate Descope into your Framer site. # Descope With Framer Using [Framer](https://framer.com/) to create web applications offers flexibility and control over your design and functionality. This guide will help you implement Descope's advanced authentication features into your Framer project. We have developed the [Descope Framer Plugin](https://www.framer.com/marketplace/plugins/descope/) to simplify this integration. Framer is currently having issues with their package management service, and some users may face errors when using our Framer plugin. To unblock developers while the Framer team is looking into a solution, we have published this [workaround guide](/getting-started/web-development-platforms/framer-workaround). ![Descope Framer Marketplace Plugin](/assets/framer-marketplace-plugin.webp) ## Launch the Plugin You can find the Descope plugin in the [Framer Marketplace](https://www.framer.com/marketplace/plugins/descope/). Click `Open Plugin in` and choose the project you would like to add Descope authentication to, or a new project. Alternatively, if you are already in your Framer project, choose `Open Plugins` in the Framer menu and search for Descope. ![Open Descope Framer Plugin](/assets/open-framer-plugin.webp) ## Configure Descope Settings Once you've launched the plugin, configure the following Descope project settings: - `Project ID`: the ID of your project, found on the [settings page](https://app.descope.com/settings/project) of the Descope console - `Redirect URL`: the URL that the user will be redirected to if they try to access a page they do not have access to, usually your website's home or login page - `Custom Base URL` (Optional): your [Custom Domain](/how-to-deploy-to-production/custom-domain) if you have configured it for your Descope project - `Custom Google Provider Name` (Optional): your custom [Google provider](/auth-methods/oauth/google-one-tap#google-provider-configuration) name. This is only required if you are using [Google One Tap](/auth-methods/oauth/google-one-tap) ![Descope Framer Settings](/assets/descope-framer-settings.webp) After configuring the Descope project settings, you must set up the overrides file in order to implement access control. To do so: 1. Copy the code snippet. 2. Navigate to the Assets page in the top left corner of the Framer editor. 3. Click (+) next to Code, name your file "DescopeAuth.tsx" select "New Override" and click "Create". 4. Paste the code snippet into the file and click "Save". 5. Apply the overrides, as described in the [overrides section](#apply-overrides) below, to conditionally display the page or component based on the user's authentication status. ## Insert Components Once you have configured your Descope settings within the plugin, you can navigate to the `Components` tab to insert authentication flows and the user profile widget. Before inserting a flow, specify the flow ID. You also have the option to set Style IDs for either the flow or the widget. If left empty, the component will render in your project's default style. ![Framer Flow Component](/assets/framer-flow-component.webp) After inserting, you can drag and drop the flow or widget component onto wherever you'd like on the page. ![Framer Flow On Page](/assets/framer-flow-page.webp) ## Apply Overrides After adding the authentication flow to your page, you can apply the following overrides to implement access control: - `protectedPage`: Hides the selected page from an unauthenticated user and redirects to the redirect URL specified - `unprotectedPage`: Hides the selected page from an authenticated user and redirects to the redirect URL specified - `protectedComponent`: Hides the selected component from an unauthenticated user - `unprotectedComponent`: Hides the selected component from an authenticated user - `logoutButton`: Turns the selected component into a logout button - `oneTapPage`: Adds [Google One Tap](/auth-methods/oauth/google-one-tap) to the selected page The `oneTapPage` shortcode code can only be used when a [custom Google Provider](/auth-methods/oauth/google-one-tap#google-provider-configuration) is configured. To apply an override: 1. Select a page or component 2. Navigate to "Code Overrides" at the bottom of the toolbar on the right 3. Select DescopeAuth as the file, and your chosen function as the override ## Final Thoughts The Descope Framer Plugin is the ideal solution for anyone looking to improve the security and user experience of their Framer site. With advanced features like passwordless login, MFA, and SSO, you can protect your site while providing a seamless experience for your users. # Web Development Platforms (/getting-started/web-development-platforms) Detailed walkthroughs on how to integrate Descope's authentication solutions across popular website builders like Webflow, Squarespace, etc. # Descope with Website Development Platforms While platforms like **Webflow**, **Squarespace**, **WordPress**, and **WeWeb** may offer basic user authentication through plugins or built-in tools, these options often fall short when you need advanced security, flexibility, or integration across multiple services. The guides in this section explain how you can use Descope with each of these platforms, to replace the pre-existing simpler authentication systems. ## Why Use Descope Instead of Built-In Authentication? While platforms like **Webflow**, **Squarespace**, **WordPress**, and **WeWeb** may offer basic user authentication through plugins or built-in tools, these options often fall short when you need advanced security, flexibility, or integration across multiple services. Here’s why you might choose Descope instead: ### **Advanced Authentication Capabilities** Most website builders support only the most basic username/password login. With Descope, you can instantly upgrade your site's authentication experience by offering: - **Passwordless options** (email Magic Link, OTP, social login, passkeys) - **Multi-Factor Authentication (MFA)** - **Step-up authentication** for sensitive actions - **Adaptive authentication** based on device, IP, or behavior **Example:** Instead of relying on a plugin with limited MFA support, a WordPress site collecting sensitive customer data can use Descope to add email + TOTP-based MFA without writing any backend logic. ### **Enterprise-Grade SSO** Built-in auth features rarely support **SSO (Single Sign-On)** or **federated identity**—especially not with standards like SAML or OIDC. Descope can turn your website into a central identity provider or broker, letting users log in once and access other tools (Salesforce, Overdrive, internal dashboards, etc.). **Example:** A B2B portal built in Webflow can use Descope to let enterprise customers sign in via Azure AD and then automatically gain access to integrated services like a custom support portal or knowledge base. ### **Custom User Workflows and Branding** Descope Flows let you design completely custom signup/login experiences—visually—without being locked into the native builder UI. You can maintain consistent branding across every step of the authentication flow. **Example:** A marketing agency with a WeWeb site can design branded, multi-step onboarding flows for different clients, including group or tenant assignment, custom fields, and progressive profiling. ### **First-Class Observability and Data Sync** With Descope, you can track logins, detect anomalies, and integrate with analytics tools using [Audit Trails](/audit-trails-and-integrations) and [Connectors](/connectors/connector-configuration-guides/analytics). You're not limited to what your website builder reports. **Example:** A site built with Squarespace can use connectors to sync user signup events directly into a CRM like HubSpot. ## Supported Platforms Descope supports pretty much any website development platform that allows you to run custom javascript code on pages. Most of the common website builder tools that exist support this. We have specific guides for a variety of platforms below, or if there is no dedicated guide for the platform that you are using, you can skip to the section on generic [integration techniques](#ways-of-integration). ## Guides } title="Bubble" description="Use this guide to enable Descope authentication with your Bubble site" /> } title="FlutterFlow" description="Use this guide to enable Descope authentication with your FlutterFlow site" /> } title="Framer" description="Use this guide to enable Descope authentication with your Framer site" /> } title="Squarespace" description="Use this guide to enable Descope authentication with your Squarespace site" /> } title="Webflow" description="Use this guide to enable Descope authentication with your Webflow site" /> } title="WeWeb" description="Use this guide to enable Descope authentication with your WeWeb site" /> } title="Wordpress" description="Use this guide to enable Descope authentication with your Wordpress site" /> ## Ways of Integration There are three main ways that you can integrate Descope with web development platforms, like the ones mentioned above. Typically it will either be done natively with our [Descope JS SDK](https://github.com/descope/descope-js), by using Descope as an OIDC or SAML SSO provider, or with a custom plugin we've developed. ### Using a Descope SDK and Web Component 1. **Add the SDK**: First, include the Descope WebJS SDK in your website's global header section. This is necessary for enabling the Descope features across all pages of your site, so if there is a place you can apply this to all pages you should put it there. Otherwise you can copy this script tag to each web page you're developing. ``` html ``` 2. **Embed the Descope Web Component**: On specific pages like the login or sign-up pages, embed the Descope Web Component. This code snippet will use the Descope Web Component to render your Descope flows, and allow user interaction such as logging in or registering new users. ```html ``` If you would like to change the flow being used, you can input a different flow-id where you see `sign-up-or-in`. The flow-id's can be found in the Descope console [here](https://app.descope.com/flows). If you would like to change where the website will redirect when login is successful, you can change the `window.location.replace` value of `'/'` under the `onSuccess()` function. 3. **Protecting Pages**: Now that you have a way of signing the user into your site, it's important to make sure that users are redirected to the login page and asked to sign-in if they are not already authenticated. You can apply this to specific pages or your whole site, in order to protect your site contents from un-authenticated users. ```html ``` 4. **Logout with Client SDK**: To give your users the ability to sign out of your site, you'll need to rely on our Client SDK in order perform this action. You can embed this code as a code snippet on your page, in the form of a **Sign Out** button or something like that. ```html ``` 5. **Fetching User Details**: If you would like to return user details to display in the frontend of your application, you can use this code snippet to extract user details from the `sessionToken` created when the user is authenticated. ```javascript const sessionToken = sdk.getSessionToken(); if (sessionToken) { getProfile() } async function getProfile() { const profile = await sdk.me(sdk.getRefreshToken()) userName.innerHTML = profile.data.name userEmail.innerHTML = profile.data.email } ``` 6. **(Optional) Backend Session Validation**: If you're using your own backend and developing protected APIs with your site, you can use our backend session validation functions documented [here](/sessions/validation/backend), in order to protect your backend. 7. **(Optional) Roles and Permissions**: Role and Permissions will be returned in the JWT token that's created based on the identity of the user. To access these, you can access these from the `sessionToken` object, which you can retrieve in the same way as you did in the previous steps from `getSessionToken()`. ### Integration as an OIDC Provider Descope can also function as an OpenID Connect (OIDC) provider. This is particularly useful if the website builder or platform supports OIDC natively or through plugins. 1. **Configuration**: Configure Descope as an OIDC provider within the platform's authentication settings. You can follow the steps [here](/identity-federation/applications/oidc-apps), in order to configure Descope as an OIDC provider of your website. 2. **Protecting Pages**: You might need to protect pages using the code in Step 3 in the section above, unless the website building platform supports native OAuth integration. 3. **Roles and Permissions**: You may need to consider adding claims related to Roles and Permissions, or custom claims to your OIDC JWT, documented [here](/identity-federation/applications/oidc-apps#custom-claims). The usage of these claims will manifest in custom JS scripts on your web page or in some custom role handling logic built into the platform. ## Final Thoughts As you can now see, it's super easy to get started and build out scalable, secure authentication with the power of our platform in your websites. If you have any other questions about Descope or how to integrate with these Web Development Platforms, feel reach to reach out to [us](/support)! # Shopify (/getting-started/web-development-platforms/shopify) If you're using Shopify Plus to develop your web applications, this tutorial will walk you through how to integrate Descope into your Shopify Plus site. # Descope With Shopify Plus [Shopify Plus](https://www.shopify.com/plus) empowers fast-growing brands with the digital infrastructure to scale quickly. This guide will help you implement Descope's advanced authentication features into your Shopify project. ## Creating a Shopify page to host the auth flow 1. Create a new page in Shopify, and add the custom liquid code to render the auth flow in its content area. ![Shopify page with custom liquid code](/assets/custom-liquid.webp) 2. In the custom liquid code field, add the following code and make sure to replace the `__ProjectID__` and `` with the actual values. ```liquid ``` ## Setting up Descope as an OIDC provider 1. Now, we'll create an OIDC application in Descope. Navigate to the [Federated Apps](https://app.descope.com/applications) page in the Descope Console and click on the `+ App` button. ![Create an OIDC application in Descope](/assets/create-shopify-oidc-app.webp) 2. Under the **IdP Configuration** tab, set the **Flow Hosting URL** to be the URL for the Shopify page where you added the Descope flow component. 3. We'll use these configuration details when setting up Descope as an Identity Provider in Shopify console. 4. Also create the Access Key from the [Access Keys](https://app.descope.com/accessKeys) page in the Descope Console. Make sure to save the access key as we will use it in next step. ![Create an Access Key in Descope](/assets/access-key-shopify.webp) ## Setting up Descope as an Identity Provider in Shopify store 1. Within your Shopify store, navigate to the **Customer Accounts > Authentication > Manage**. If you don't see this option, make sure the you have a Shopify Plus account. 2. Select **Connect to provider**, and name the provider as "Descope". ![Descope as an Identity Provider in Shopify store](/assets/descope-shopify-provider.webp) 3. Enter the following details into the application for configuration: - **Well-known or discovery endpoint URL**: This is the Discovery URL of the Descope OIDC application you created in Descope. - **Client ID**: Descope Project ID. - **Client secret**: The Access Key you created in Descope. - **Additional scopes**: Add `profile`. - **Post logout redirect URLs**: `post_logout_redirect_uri` 4. After entering the details, test the configuration before activating Descope as an Identity Provider. 5. After successfully testing the configuration, activate Descope as an Identity Provider. ## Using Google One Tap for Shopify Plus You can enable Google One Tap for authentication in your Shopify store. Refer to our [Google One Tap](/auth-methods/oauth/google-one-tap) docs to learn how to configure Google One Tap. If you already have Federated OIDC application configured for Shopify Plus, you can reuse the same setup for Google One Tap. Make sure to update the authentication flow you're using to include the **Load User** action, which checks whether the user is already logged in and exits the flow accordingly. To ensure the Google One Tap prompt is available across your store, you must add the Google One Tap script to a global layout file just before the closing `` tag. In most Shopify themes, this file is `theme.liquid`. ```liquid ``` # Squarespace (/getting-started/web-development-platforms/squarespace) # Descope With Squarespace Using [Squarespace](https://squarespace.com/) to design and build your web applications doesn't mean you can't enjoy advanced authentication and authorization. This guide will show you how to integrate Descope into your Squarespace website seamlessly. ## Getting Started with Squarespace Settings 1. Access your Squarespace [dashboard](https://account.squarespace.com), and select the site you wish to edit, then navigate to ***Settings***. 2. In Settings, click on ***Advanced***, then select ***Code Injection***: 3. Add the Descope SDK script into the **Header** section to enable Descope features across your website: ```html ``` ## Creating a Login/Sign-Up Page 1. Go to your site's main menu and choose ***Pages***, then click ***+ Add Page*** and name it ***Login***. 2. On the newly created Login page, add a **Code Block** from the **Build** section: 3. Insert this HTML and script into the Code Block: ```html ``` Update `x.x.x` to the latest version of the Web Component, available [here](https://www.npmjs.com/package/@descope/web-component) This code embeds the Descope web component, enabling user authentication and redirection upon successful login. ## Handling Redirects for Unauthorized Access To ensure users are redirected to log in if they are not authenticated: 1. In the same Code Injection section, add the following script to the **Footer**: ```html ``` You will have to apply this to either your entire site, or copy this code to specific page footers to protect only specific pages. ## Fetching User Details for Your Frontend To display user-specific details: ```javascript const sessionToken = sdk.getSessionToken(); if (sessionToken) { getProfile(); } async function getProfile() { const profile = await sdk.me(sdk.getRefreshToken()); document.getElementById('userName').innerText = profile.data.name; document.getElementById('userEmail').innerText = profile.data.email; } ``` ## Final Thoughts By integrating Descope into your Squarespace site, you've equipped your website with robust authentication capabilities. For further customization or if you need logout functionality and backend session validation, refer to our detailed documentation: 1. Logout Using Client SDK - [here](/sessions/management/web#logout-using-client-sdk) 2. Backend Session Validation - [here](/sessions/validation/backend) 3. Roles and Permissions - This can be found in Step 7 of the guide [here](/getting-started/web-development-platforms#using-a-descope-sdk-and-web-component) If you encounter any challenges or have questions, our support team at [Descope](/support) is ready to assist you. # Webflow (/getting-started/web-development-platforms/webflow) If you're using Webflow to design and build your web applications, this tutorial will walk you through how to integrate Descope into your Webflow website. # Descope With Webflow If you're using [Webflow](https://webflow.com/) to design and build your web applications, this tutorial will walk you through how to integrate Descope into your Webflow website. By following along, you can enjoy the same no-code development experience but for your authentication and authorization. If you would like to use a template we've provided, so you can see how Descope is embedded in custom code and in `HTML embed` components, please visit the [Webflow Template Marketplace](https://webflow.com/made-in-webflow/website/terminal-descope). ## Getting Started in Webflow Settings *Before you begin, you'll need to sign up for [Descope](https://www.descope.com/sign-up). Please complete the sign up process and then return back to this guide.* 1. Head to your Webflow [dashboard](https://webflow.com/dashboard), and under the site you wish to edit, open up the three dots on the site you would like to integrate Descope with and select ***Settings***. ![Descope webflow guide - webflow dashboard](/assets/descope-webflow-dashboard.webp)
2. Select ***Custom Code*** in the top menu bar: ![Descope webflow guide - webflow custom code](/assets/descope-webflow-custom-code.webp)
3. Add the following snippet to the **Head Code** section, which will allow your website to use the Descope SDK across all pages: ```html ``` ![Descope webflow guide - site wide custom code](/assets/descope-webflow-site-wide-custom-code.webp) ## Implementing Login/Sign-Up Page 1. Return to your site, and add a new page called ***Login***: ![Descope webflow guide - custom code on pages 1](/assets/descope-webflow-login-page-1.webp) Also, double-check to make sure that **Slug** is also set to `login`: ![Descope webflow guide - custom code on pages 2](/assets/descope-webflow-login-page-2.webp) 2. Next, you will want to embed an HTML element to the page. You will find this by clicking **Add** (the box with the + mark on the left-hand sidebar) and scrolling down to the **Advanced** options: ![Descope webflow guide - custom code on pages 3](/assets/descope-webflow-login-page-3.webp) 3. Once that element has been added, double-click that element on the canvas, paste this code in the HTML Embed code editor: ```html ``` This code will display the web component of the `sign-up-or-in flow`, and upon successful authentication, redirect to the homepage of the site. Afterward, you can publish your site and verify that the authentication is working properly! ## Customizing Descope HTML Element 1. If you would like to change the flow being used, you can input a different flow-id where you see `sign-up-or-in`. The flow-id's can be found in the Descope console [here](https://app.descope.com/flows). 2. If you would like to change where the website will redirect when login is successful, you can change the `window.location.replace` value of `'/'` under the `onSuccess()` function. ## Handing Redirect if Not Authenticated The final step to using Descope with Webflow, is making sure that user's trying to access a protected resource are redirected to the login page and asked to sign-in if they are not already authenticated. 1. To implement this, go back to your site **Settings** and click on **Custom Code**, where we were at the beginning: ![Descope webflow guide - handle redirect if not authenticated](/assets/descope-webflow-handle-redirect-not-auth.webp) 2. Scroll down to the `Footer Code` section, and paste this code: ```html ``` ## Fetching User Details for Your Frontend If you would like to return user details to display in the frontend of your application, you can this information from the `sessionToken` created when the user is authenticated. ```javascript const sessionToken = sdk.getSessionToken(); if (sessionToken) { getProfile() } async function getProfile() { const profile = await sdk.me(sdk.getRefreshToken()) userName.innerHTML = profile.data.name userEmail.innerHTML = profile.data.email } ``` You can also retrieve this information from the `onSuccess()` function located in the step above. Once this is completed, you should have a full-fledged authenticated Webflow application, running with Descope. However, there are a few extra pieces you might want to add to your website. ## Final Thoughts If you want to add a Logout functionality or handle **Session Validation** in your backend, you can do by following the instructions from our Docs website: 1. Logout Using Client SDK - [here](/sessions/management/web#logout-using-client-sdk) 2. Backend Session Validation - [here](/sessions/validation/backend) 3. Roles and Permissions - This can be found in Step 7 of the guide [here](/getting-started/web-development-platforms#using-a-descope-sdk-and-web-component) As you can now see, it's super easy to get started and build out scalable, secure authentication with the power of our SDKs in your Webflow website. If you have any other questions about Descope or how to integrate with Webflow, feel reach to reach out to [us](/support)! # WeWeb (/getting-started/web-development-platforms/weweb) If you're using WeWeb to design and build your web applications, this tutorial will walk you through how to integrate Descope into your WeWeb website. # Descope With WeWeb [WeWeb](https://www.weweb.io/) allows you to build web applications quickly and efficiently. This guide will help you integrate Descope's authentication solutions into your WeWeb projects, allowing you to focus on creating without worrying about complex authentication systems. ## Getting Started with WeWeb The easiest way to integrate Descope with WeWeb is to use our default [OIDC Application](/identity-federation/applications/oidc-apps) and WeWeb's built-in OIDC authentication plugin. ### Configure WeWeb for OIDC 1. Navigate to the Auth section in your WeWeb dashboard, in the menu bar on the top of the screen, and select OIDC. ![Adding OIDC WeWeb plugin](/assets/weweb-oidc-plugin.webp) 2. Configure the plugin for OIDC by entering the client ID, secret, and other details provided by Descope. You'll need the following items: - **Domain** - Issuer which you can find under [Default OIDC Application](https://app.descope.com/applications/descope-default-oidc) - **Client ID** - Descope Project ID which you can find under [Project Settings](https://app.descope.com/settings/project) - **Client Secret** - Descope Access Key, which you can create under [Access Keys](https://app.descope.com/m2m/accessKeys) ![OIDC configuration in WeWeb](/assets/weweb-oidc-plugin-config.png) You can read more specific instructions on how to configure OIDC on the WeWeb documentation [page](https://docs.weweb.io/plugins/auth-systems/open-id.html) Continue reading to learn how to handle redirection and user sessions, along with user and role management. ### Handling Redirects and User/Role Management 1. WeWeb can be configured to redirect to your login page automatically. Your login page can then restart the OIDC process and redirect to our hosted page. You can do this by selecting the options in this section below: ![OIDC redirect option configuration](/assets/weweb-oidc-plugin-redirect-config.webp) 2. Next, you'll need to configure WeWeb to recognize the roles that are passed back from Descope in the JWT tokens: ![OIDC roles configuration](/assets/weweb-oidc-plugin-roles-config.webp) If you're using specific roles associated with specific Tenants, then Role Management with the native OIDC plugin will not work. In this case you'll need to integrate Descope manually with our WebJS SDK and Web Component. ### Protecting Pages The final step you'll need to do to integrate Descope with WeWeb, is to protect specific pages on your site from being accessed by non-authenticated users. To manage this access, you can define who has access to every specific page with the **Private Access** option in the WeWeb page editor screen: ![Private access screen](/assets/weweb-oidc-plugin-private-screen.webp) If you have any questions on any of the steps mentioned above, you can refer to the WeWork documentation [page](https://docs.weweb.io/plugins/auth-systems/open-id.html) for more clarity on these features. ### Embed Authentication on Your Site When using OIDC, you will be default be using our [Auth Hosting](/identity-federation/auth-hosting) application that will host and run your flows. You can choose to also embed your flow using our WebJS SDK in your website directly and redirect to it by changing the [Flow Hosting URL](/identity-federation/auth-hosting#more-customization-options) under OIDC Applications. If you want to embed the web component and SDK directly in your application, follow our general instructions on how to do so [here](/getting-started/web-development-platforms#using-a-descope-sdk-and-web-component) If you have any other questions about Descope or how to integrate with WeWeb, feel reach to reach out to [us](/support)! # WordPress (/getting-started/web-development-platforms/wordpress) If you're using WordPress to develop your web applications, this tutorial will walk you through how to integrate Descope into your WordPress site. # Descope With WordPress Add Descope's advanced authentication features to your WordPress site using our [WordPress Plugin](https://wordpress.org/plugins/descope/). This guide walks you through the integration process. Watch our [video tutorial](https://www.youtube.com/watch?v=g7wa17QmmjQ) for a live demonstration of the WordPress Plugin. ## Installation 1. Open your WordPress dashboard 2. Navigate to **Plugins --> Add New Plugin** 3. Search for "Descope" 4. Click **Install** and then **Activate** ![Install Descope Wordpress Plugin](/assets/descope-wordpress-marketplace.webp) ## Configuration ### Basic Setup 1. Go to the **Descope Settings** tab in your WordPress dashboard 2. Under **Descope Configuration**, enter your Descope Project ID ### SSO Setup (Optional) If you plan to use SSO: 1. Set up a [SAML or OIDC Federated Application](/identity-federation/applications) 2. Navigate to the **SSO Configuration** tab in WordPress 3. Enter your SAML/OIDC configuration details ## Shortcodes WordPress shortcodes let you add [Embedded Flows](/flows), [SAML/OIDC SSO](/auth-methods/sso), [Google OneTap](/auth-methods/oauth/google-one-tap), [Protected Pages](/getting-started/web-development-platforms/wordpress#protected-page), and [Logout Buttons](/getting-started/web-development-platforms/wordpress#logout-button) to your site. ### Embedded Flows Add authentication flows to any page using the `[descope_wc]` shortcode: ``` [descope_wc flow_id="sign-up-or-in"] ``` You can customize your flow to include any of our Authentication Methods from the Descope Console. If you don't specify a `flow_id`, the flow will default to "sign-up-or-in". ![WordPress login page shortcode](/assets/wp-loginpage-shortcode.webp) ![WordPress login page](/assets/wordpress-loginpage.webp) ### SAML/OIDC SSO First, make sure you have set up your federated app in your Descope console, as described in our [Federated Applications](/identity-federation/applications) guide. Then, make sure to provide your SAML or OIDC Configuration Details in the Descope Settings page of your WordPress dashboard. Some values have been masked below, but make sure to provide all the values in the relevant column. ![WordPress SSO config](/assets/wordpress-sso-config.webp) Add the `[saml_login_form]` or `[oidc_login_form]` shortcode to your page to add the SSO capabilities. ![WordPress SSO shortcodes](/assets/wordpress-sso-shortcodes.webp) When the user is not logged in, the shortcode will display a `login` button, and when the user is logged in, a `logout` button will be displayed instead. ### Google One Tap Add Google One Tap authentication using the `[onetap_form]` shortcode: ``` [onetap_form provider_id="google"] ``` - `provider_id` is optional (defaults to "google") - Requires [Google Provider configuration](/auth-methods/oauth/google-one-tap#google-provider-configuration) - Only visible to unauthenticated users ![WordPress One Tap shortcode](/assets/wordpress-onetap-shortcode.webp) ![WordPress One Tap popup](/assets/wordpress-onetap-popup.webp) ### Logout Button Add a logout button that's only visible to authenticated users: ``` [logout_button] ``` ![WordPress Logout Button shortcode](/assets/wordpress-logout-shortcode.webp) ### Protected Page Protect pages from unauthorized access using the `[descope_protected_page]` shortcode. Configure redirection behavior for unauthenticated users: #### Basic Redirection ``` [descope_protected_page redirect_page_path="/login-page"] ``` Redirects unauthenticated users to another page, such as your login page. #### OIDC Auto-Login ``` [descope_protected_page redirect_page_path="/oidc_login" return_to="/protected-content"] ``` - Automatically initiates OIDC login for unauthenticated users - Optional `return_to` parameter specifies the post-authentication redirect OIDC auto-login requires proper [OIDC configuration](/getting-started/web-development-platforms/wordpress#samloidc-sso). ## User Syncing The WordPress user table is the source of truth for your site's users. The user table is synced with Descope every 24 hours, but you can also manually sync users under Sync Users in the Descope Settings tab of your WordPress dashboard. ![WordPress sync users](/assets/wordpress-sync-users.webp) This is also where you can map WordPress custom fields to a Descope custom attribute you have already created. Any WordPress custom fields mapped here will show up in the Descope user table as custom attributes. # Downstream Credential Access (/agentic-identity-hub/auth-patterns/downstream-credential-access) Exchange inbound access tokens for downstream credentials scoped to Descope Resources or Connections, from MCP servers, APIs, or any intermediate service. # Downstream Credential Access When a service holds an inbound Descope access token and needs to call a **different** downstream API, it usually needs a credential scoped to that target, not the token it already has. That applies to MCP servers fetching third-party OAuth tokens, MCP tools calling internal APIs registered as separate [Resources](/resources), and any middleware that brokers access on an agent's behalf. This page covers the server-side pattern: validate the inbound token, exchange it at the [Descope STS](/api/third-party-apps) (RFC 8693), and call the downstream service with the outbound credential. For the MCP-specific walkthrough (tool handlers, sequence diagrams, and Python SDK shortcuts), see [Calling downstream APIs from MCP tools](/mcp/calling-apis-from-mcp). ## Two Tokens: Inbound and Outbound Every exchange involves two tokens, and keeping them straight makes the rest of this page easier to follow: - **Inbound token**: the access token your service receives from the agent or MCP client. Its `aud` is your service (your [Resource](/resources)), and it carries who is calling and the scopes they consented to. You validate it, but you never forward it downstream. - **Outbound credential**: what Descope hands back for the downstream call. It is either a **Connection token** (an OAuth token or API key from the [Connections](/agentic-identity-hub/core-components/connections) vault) or a new **Resource token** (a Descope JWT minted with a *different* audience, for another Descope Resource). This is what you actually send downstream. ## Choose a Path | Downstream target | Pattern | What you receive | | ----------------- | ------- | ---------------- | | A [Descope Resource](/resources) that accepts the inbound token's audience and scopes | **Passthrough** | Use the inbound access token as-is | | An internal API registered as a different Descope Resource | **STS → Resource** | A Descope JWT scoped to that resource's `aud` and scopes | | A third-party OAuth service or API-key-based service | **STS → Connection** | The stored OAuth access token or API key from [Connections](/agentic-identity-hub/core-components/connections) | Passthrough applies when the downstream service is the same Resource the caller already has a token for, or when its audience and scope requirements match the inbound token. For most tool handlers you should exchange the token rather than forward it. Passthrough falls short when: - The inbound token's `aud` is your service, but the downstream target expects its own audience or a Connection token from the Connections vault, not your token. - Policy is evaluated once at your ingress, so you cannot apply per-target rules (the user, the tenant, the calling client, and your server) at the downstream hop. - The audit log records no distinct exchange event for each downstream target the tool reached. - The full inbound scope set is exposed downstream instead of a credential downscoped for that call. **STS → Resource** and **STS → Connection** use the **same token exchange request**. The `resource` parameter tells Descope which path to take. See [Using a Resource Token](/resources#using-a-resource-token) for Resource-to-Resource exchange, and [Fetching Connection Tokens](/agentic-identity-hub/core-components/connections/fetching-connection-tokens) for Connection retrieval APIs and SDK methods. ## Your Server Is Both a Resource and a Client To broker downstream access, a single server plays two Descope roles at the same time. It is both a **[Resource](/resources)** and a **[Client](/agentic-identity-hub/core-components/clients)**: - **As a Resource** (the inbound side), it receives the caller's access token, whose `aud` is your service URL, and validates that JWT against Descope's JWKS. - **As a Client** (the outbound side), it authenticates to the STS with its own `client_id` and `client_secret` to exchange the inbound token for the outbound credential. The same process wears both hats: |"agent's access token (aud = your Resource)"| R R -->|"after validation"| C C -->|"token exchange with Descope STS"| STS STS -->|"Resource JWT or Connection credential"| C C -->|"calls the downstream resource or connection"| Downstream`} /> The separate client ID (within the server) tells the STS which system is making the exchange, so that: - [Policies](/agentic-identity-hub/policies) evaluate against the full context, covering the original user, the calling client, and your server - The audit log records the complete delegation chain: user → calling client → your server → downstream service - Policies can target your server specifically using `client.tags` or `client.name` Without a registered client, the STS cannot distinguish a legitimate server-side exchange from a caller trying to fetch credentials directly. ## Setup ### 1. Register a Client for Your Server In the Descope Console, go to [Clients](https://app.descope.com/agentic-hub/clients) and create a client to represent the service making exchanges. - Enable the **Client Credentials** grant type - Disable grant types the server will not use - Copy the generated **Client ID** and **Client Secret** This client is separate from the agents or MCP clients that call your service. It represents the server itself acting as an OAuth client toward the STS. ### 2. Store Server Credentials Securely Store the client ID and secret in your server's environment (or secrets manager). These credentials authenticate every token exchange request. ### 3. Define What the Server Can Reach Register each downstream target your code will exchange for: | Downstream target | Where to configure it | What the STS returns | | ----------------- | --------------------- | -------------------- | | Internal API that validates Descope JWTs | [Connect → Resources](https://app.descope.com/resources) as an **API Resource** | A Resource-scoped Descope JWT | | Third-party OAuth or API-key service | [Agentic Identity Hub → Connections](https://app.descope.com/agentic-hub/connections) | The OAuth token or API key stored in the Connections vault | #### MCP scopes → Connection scopes MCP Server Resource scopes are what a user consents to when they authorize your MCP server (for example `mcp:read_hubspot_contacts`). In the MCP server's scopes config, you map each of those scopes to the downstream **Connection scopes** it needs (for example `crm.objects.contacts.read`). Descope uses that mapping at two points, and you never pick Connection scopes by hand at either: - **During the connect**: when the user links the Connection, Descope requests exactly the Connection scopes that correspond to the MCP server scopes the user consented to, so the token stored in the Connections vault carries the right scopes. - **At fetch time**: when your server fetches the token, Descope reads the scopes on the access token (the same MCP server resource scopes) and automatically returns the stored Connection token with the matching Connection scopes. ![MCP Server scopes mapped to Connection scopes in the Descope Console](/assets/mcp-server-scopes.webp) Configure this mapping in your [MCP server configuration](https://app.descope.com/agentic-hub/mcp-servers) underneath the Scopes section, not on the Policies page. See [MCP Server Resource scopes](/resources/scopes-and-roles#mcp-server-resources) and [MCP server settings](/agentic-identity-hub/core-components/mcp-servers/settings) for more information. ### 4. Create a Policy for Who Can Access The scope mapping in step 3 lives on the MCP server config. A [Policy](/agentic-identity-hub/policies) is a separate control for **who** is allowed to reach a target: which agents or clients can access which Resources, and what your server, acting as a client, is allowed to exchange for. Descope evaluates the policy during token exchange and when fetching a Connection token from the vault with an access token. Without a matching allow policy, the request is denied even when the scope mapping is correct. ## Making the Exchange At request time your server validates the inbound access token, then calls the Descope token endpoint as its registered client. The same request shape covers both **STS → Resource** and **STS → Connection**; the `resource` parameter selects the target. >Agent: Ask the agent to do something Agent->>Server: Request with inbound access token (aud = your Resource) Server->>Server: Validate JWT against Descope JWKS Note over Server,STS: Server authenticates as a Client Server->>STS: POST /oauth2/v1/token (token-exchange) Note over Server,STS: Basic client_id:client_secret
subject_token = inbound JWT
resource = Resource URL or Connection ID STS->>STS: Validate server client credentials STS->>Policy: Evaluate user + calling client + your server Policy-->>STS: Allow / deny (+ allowed scopes) alt resource = another Descope Resource STS-->>Server: Resource-scoped Descope JWT Server->>Downstream: Call internal API with new JWT else resource = Connection ID STS-->>Server: OAuth token or API key from the Connections vault Server->>Downstream: Call third-party API with Connection credential end Downstream-->>Server: API result Server-->>Agent: Tool / response result`} /> ```http POST /oauth2/v1/token Authorization: Basic {base64(client_id:client_secret)} Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:token-exchange &subject_token={inbound_access_token} &subject_token_type=urn:ietf:params:oauth:token-type:access_token &resource={resource_url_or_connection_id} ``` Descope validates your server's credentials, evaluates policy against the inbound token's claims, and returns the outbound credential: either a resource-scoped Descope JWT or a token from the Connections vault. See the [Token API](/api/third-party-apps) for full parameter reference, including optional scope narrowing and resource indicators. ## Audit Trail Every exchange produces an audit event that records: - The original user identity from the inbound token - The calling client that initiated the request - Your server's client ID that made the exchange - The downstream Resource or Connection accessed - The policy decision applied This gives you a traceable record from the user who authorized access, through the agent or client that requested it, to the server that fetched the outbound credential. # Auth Patterns (/agentic-identity-hub/auth-patterns) Credential issuance patterns, integration architectures, and the OAuth standards behind the Agentic Identity Hub. # Auth Patterns This page covers the credential issuance patterns and integration architectures behind the [Agentic Identity Hub](/agentic-identity-hub). ## Lifecycle Map Every client authenticates at the Descope Authorization Server (AS) and receives a **Resource token**: an access token minted for a specific [Resource](/resources), carrying that Resource's audience and the scopes the agent was granted. The agent presents that token to the Resource: your MCP server, MCP/API gateway, or backend API. Inside the Resource, when a tool needs a downstream credential, the Resource exchanges the token at the [Descope token endpoint](/api/third-party-apps/token-endpoint) for a [Connection](/agentic-identity-hub/core-components/connections) token (a vaulted third-party OAuth token or API key) or for another Resource token with a different audience. The agent holds only its own Resource token throughout. ## Clients and Agentic Identities An agent in Descope comprises of two objects. Understanding them individually makes the rest of this page easier to follow: **[Client](/agentic-identity-hub/core-components/clients):** the agent's registration. It holds credentials, an owner, tags, and a **Client ID**. It exists before the agent has done anything. **[Agentic identity](/agentic-identity-hub/core-components/agents):** a specific authorization record that binds a specific client to a specific principal (a user, a tenant, or, for autonomous agents, the client itself) on whose behalf the client is permitted to act. A single client can hold multiple agentic identities, one for each principal that has granted it access. The distinction maps onto standard OAuth. A registered OAuth client is one entity. A user's grant to that client is another. Descope formalizes the same split for agents and gives each agentic identity a stable, addressable ID. | Operation | Client | Agentic identity | | --------- | ------ | ---------------- | | Configure credentials and rotation | ✓ | | | Set tags and ownership | ✓ | | | Define attribute-based policies | ✓ | | | Suspend the agent across all principals | ✓ | | | Audit actions taken on behalf of a specific user | | ✓ | | Revoke one user's or agents access without touching others | | ✓ | An [agentic identity](/agentic-identity-hub/core-components/agents) represents a software agent that acts autonomously or on behalf of a user or tenant. Unlike a service account, it is designed for a system that makes runtime decisions: - Authentication is non-interactive and machine-to-machine - Authorization is capability-based and evaluated per request - Credentials are short-lived by default - Delegation is explicit and traceable - Creation and decommissioning happen just in time Agents are first-class identities in Descope. They operate independently or act under explicit delegation from a human user. Either way, there is a clear chain of accountability from every agent action back to an owner. ## Credential Issuance Patterns Token refresh returns the same scopes that were granted when the refresh token was issued. Downscoping during refresh is not currently supported. Descope supports three patterns, available across both [direct agent integration](/agentic-identity-hub/auth-patterns#direct-agent-integration) and [MCP server-based integration](/agentic-identity-hub/auth-patterns#mcp-server-based-integration). ### User Delegation An agent acts on behalf of an authenticated user. The user authenticates through the [User Consent Flow](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow), which can include SSO, MFA, and consent screens customized in Descope Flows. Descope returns an access token, a refresh token, and an ID token when `openid` scope is requested. Implemented as **OAuth 2.1 authorization code flow with PKCE**. Confidential clients authenticate with client credentials at the code exchange; public clients (CLIs, native apps, MCP clients) rely on PKCE alone. ### Autonomous Access The agent acts on its own behalf with no user involved. Used for service-to-service, machine-to-machine, and agent-to-agent calls: background workers, scheduled jobs, inter-agent calls in multi-agent pipelines. Implemented as **OAuth 2.1 client credentials flow**. The agent authenticates with its client credentials and receives a token scoped to its permitted resources. [Policies](/agentic-identity-hub/policies) work differently depending on grant type: For `client_credentials` flows, policies control which scopes the client is permitted to request at the token endpoint. The agent cannot obtain a scope that policy does not allow, regardless of what it asks for. For authorization code flows, policies control which scopes a user can consent to in the consent screen. Scopes excluded by policy do not appear as options for the user. Configure scope grants directly on the client record to constrain what an autonomous agent can request. ### Token Exchange Used for two scenarios that share the same mechanism: **Delegation chaining**: when an agent calls another agent, an MCP server, or a downstream service that accesses further resources, each hop needs its own credential while preserving the original user's identity. The intermediate service presents a previously issued token alongside the target resource and receives a new token scoped to that resource. **Federation across identity systems**: when an agent presents a token from another identity provider (a workforce IdP, a partner's AS, another Descope project, or a cloud workload identity provider like AWS or GCP), Descope validates the token against the configured trust relationship and issues an equivalent Descope token. No additional authentication redirects are required. See [Using Descope with Workloads](/agentic-identity-hub/core-components/clients/workloads) for AWS and GCP setup. Both are implemented as **OAuth 2.0 Token Exchange (RFC 8693)**. The exchanged token preserves the delegation chain, so policy evaluation and audit logs can trace the full path from the original user through every intermediary. For the server-side implementation, validating the inbound token, calling the STS, and choosing a Resource or Connection target, see [Downstream Credential Access](/agentic-identity-hub/auth-patterns/downstream-credential-access). ## Integration Patterns Choose a pattern based on how your agent calls resources. Both run on the same Descope endpoints, tokens, and policy engine; switching between them, or running both simultaneously, requires no changes to your identity configuration. ### Direct Agent Integration The agent authenticates directly to Descope and uses the resulting token against APIs, databases, or other resources. No MCP server in the path. The [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk), in Python and TypeScript, implements this pattern for you. It signs the agent in to Descope and fetches the Resource and Connection tokens its tools need, with no MCP server in between. This pattern fits when: - The agent calls REST APIs, internal services, or databases directly from agent code - The agent runs as a background process, CI/CD job, or scheduled task that needs scoped credentials per run - You're integrating with an agent framework (LangChain, LangGraph, or a custom agent) that manages its own tool dispatch ### MCP Server-based Integration The agent connects to one or more MCP servers, which handle token validation, scope enforcement, and credential brokering between the agent and downstream resources. The [MCP Auth SDKs](/mcp/sdks), in Python and TypeScript (Express), implement this pattern. They protect your MCP server, validate incoming access tokens, and fetch Connection tokens from inside your tool handlers. This pattern fits when: - You expose tools to agents over the Model Context Protocol - You want resource access logic kept out of agent code entirely - You support MCP-aware clients like Claude, Cursor, or VS Code When a tool calls a **different** downstream API (another [Resource](/resources) with a different audience, a third-party service in [Connections](/agentic-identity-hub/core-components/connections), or a narrower scope set), the server exchanges the inbound MCP token at the [STS](/api/third-party-apps/token-endpoint) rather than forwarding it. Forwarding the token directly (passthrough) only works when the downstream service validates the same audience and scopes, and is generally not recommended for tool handlers. For the exchange request, the Resource-vs-Connection paths, and why passthrough falls short, see [Downstream Credential Access](/agentic-identity-hub/auth-patterns/downstream-credential-access). # Agentic Identity (/agentic-identity-hub/core-components/agents) Learn how to view, manage, and revoke access for AI agents in the Agentic Identity Hub. # Agentic Identity The Agentic Identity view provides an overview of all agentic identities in your project. From this page, you can view agent details, filter agents, and manage their access. ## Viewing Agentic Identity Navigate to the [Agentic Identity](https://app.descope.com/agentic-hub/identities) section in the Descope Console to see an overview of all your agentic identities and their details. You can filter, select, and revoke access for agents from this page. You can also [search agentic identities](/api/management/agentic-identity-hub/search-agentic-identities) and [revoke access](/api/management/agentic-identity-hub/revoke-agentic-identities) programmatically with our [Management API](/management). Agentic identities populate this table in two ways: - **Automatically**, when a client with the **Client Credentials** grant type is created. Descope creates an agentic identity for that client immediately, since no user principal is involved. The agent appears in the table as soon as the client exists. - **On first authorization**, for delegated agents. A new row is created each time a different user or tenant authorizes the client, with the principal recorded on that row. ![Agentic Identity](/assets/agent-view.webp) ### Agent Information Each agent in the list displays the following information: #### Agent ID Every agentic identity has a unique **Agent ID**. It also appears under each client, in the [clients page](/agentic-identity-hub/core-components/clients) as the **ID**. It adjacent to the client name in the Agentic Identity view and is the stable reference point for all audit events and activity logs associated with that agent. When a log entry records a token issuance, policy decision, or credential access, the Agent ID is what ties it back to the specific authorization record — not just the client. #### Agent Name The **agent name** is a human-readable identifier for the agent. This name helps you quickly identify and distinguish between different agents in your system. #### Associated User Agents can optionally have an **associated user**. When an agent is linked to a user, it operates on behalf of that user with delegated permissions. If no user is associated, the agent operates independently as a machine-to-machine client. #### Tenant Name Agents can optionally have an associated tenant. The **tenant name** indicates which tenant the agent is associated with. This is important for multi-tenant applications where agents need to be isolated per tenant. #### IP Address The **IP address** field shows the IP address associated with the agent's authentication or activity. This can be useful for security monitoring and access control. #### Tags **Tags** are optional labels you can assign to agents for organization and categorization. Tags help you group and filter agents based on custom criteria such as environment, purpose, or team. ### Consents Consents are only available for agents that have been authorized by a user. Agents that have been authorized by a user will have a consent record associated with them. Underneath the main identity information, you will see the latest consent that has been granted to the agent. ![Consents](/assets/agent-consents.webp) The consent information includes: - **Consent ID**: A unique identifier for the consent. - **Scopes**: The scopes that have been granted to the agent. - **Created Time**: The time when the consent was granted. - **Modified Time**: The time when the consent was last updated. - **Expiry Time**: The time when the consent will expire. #### Scopes The **scopes** column shows all the permissions that have been granted to the agent. These scopes can come from: - **User consent**: When a user authorizes an agent to act on their behalf, the scopes they consent to are displayed here Scopes define what actions the agent can perform and what resources it can access. Scopes are often tied to [MCP tools](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes), where each scope corresponds to a specific tool or group of tools that the agent is authorized to use. #### Created Time The **created time** indicates when the user consent was granted or when the agent identity was first created. This timestamp helps track when an agent was provisioned or when a user authorized access. #### Modified Time The **modified time** shows when the agent was last updated. This includes changes to scopes, associated user, tags, or any other agent properties. #### Expiry Time The **expiry time** shows when the agent's access token will expire. After this time, the agent will need to re-authenticate or refresh its token to continue operating. #### OAuth Client ID The **OAuth client ID** is the identifier for the OAuth client associated with this agentic identity. This client ID is used during the OAuth authentication flow to identify which application or agent is requesting access. ## Managing Agentic Identity ### Filtering Agentic Identity You can filter the agent list to find specific agents based on various criteria. The following table shows all available filter operators, their descriptions, which columns they apply to, and whether they require a value: | Operator | Description | Applicable Columns | Requires Value | |----------|-------------|-------------------|----------------| | **Contains** | Determine if the target contains the predicate (supports both lists and strings) | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **Equals** | Determine if two values are equivalent | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **Doesn't Equal** | Determine if two values are not equivalent | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **In** | Determine if value is in the array | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **Not In** | Determine if value is not in the array | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **Is Empty** | Determine if value is empty | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | No | | **Is Not Empty** | Determine if value is not empty | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | No | | **Matches** | Determine if the target matches the regex predicate | Agent Name, Agent ID, Client ID, Associated User, Tenant Name, Tags, MCP Server | Yes | | **Is** | Determine if timestamp matches exactly | Created Time, Modified Time | Yes | | **Is After** | Determine if timestamp is after the specified time | Created Time, Modified Time | Yes | | **Is Before** | Determine if timestamp is before the specified time | Created Time, Modified Time | Yes | | **IP Address In** | Determine if an IP address is within a list of ranges | IP Address | Yes | You can combine multiple filters to create complex queries that help you find exactly the agents you're looking for. ![Filtering Agentic Identity](/assets/agent-filtering.webp) ### Selecting Agentic Identity Select one or more agents from the list to perform bulk operations such as: - Manage Tags - Revoke Access ![Selecting Agentic Identity](/assets/agent-selecting.webp) #### MCP Server The **MCP server** field indicates which MCP (Model Context Protocol) server the agent is associated with, if any. This helps track which agents are connected to specific MCP resources. #### Managing Tags You can manage [tags](#tags) for individual agents or multiple agents at once. This involves adding or removing tags from the agent(s) you have selected. ![Managing Tags](/assets/agent-managing-tags.webp) #### Revoking Access Revoking access is immediate and cannot be undone. The agent will need to go through the authentication flow again to regain access. You can also revoke programmatically with the [agentic identity revocation endpoint](/api/management/agentic-identity-hub/revoke-agentic-identities) Management API endpoint. You can revoke access for individual agents or multiple agents at once. When you revoke access: - The agent's current tokens become invalid - The agent will need to re-authenticate to regain access - All associated sessions are terminated Revoking access is **not** the same as deleting a [client](/agentic-identity-hub/core-components/mcp-servers#clients) under MCP Servers. The client ID will not be invalidated when you revoke access, you are simply invalidating the previously granted user consent. ![Revoking Access](/assets/agent-revoking.webp) # How XAA and ID-JAG Work (/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works) How Cross App Access mints and redeems XAA tokens (ID-JAGs) for MCP and APIs. # How XAA and ID-JAG Work This page explains the **protocol**: what Cross App Access (XAA) is, what an XAA token (ID-JAG) contains, and how the two OAuth grants fit together. When pre-built agents in your enterprise reach third-party tools (via XAA or a gateway), see [Manage agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags). If you host an MCP server for customers and accept their XAA tokens, see [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags) (also covered under [SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration) and [Cross App Access for B2B MCP Servers](/mcp#cross-app-access-xaa)). ## The problem XAA solves Without XAA, an agent that already signed in through the company IdP still has to run a separate OAuth consent flow for every MCP server or API it wants to use. Each product becomes its own authorization island. Enterprise IT cannot answer "which agents can reach which tools for which users?" in one place. **Cross App Access (XAA)** reuses the SSO trust the enterprise already has. The IdP that signed the user in also vouches, in a short-lived JWT, that *this user*, through *this client*, may reach *this resource*. The client presents that JWT to the resource's authorization server and receives a normal access token. There is no second browser redirect and no per-tool consent screen on the resource side. The IETF draft calls this pattern XAA and defines the JWT as an **Identity Assertion JWT Authorization Grant (ID-JAG)**. In Descope docs, **XAA token** and **ID-JAG** refer to the same credential. ## Parties | Role | What it is | Example | | --- | --- | --- | | **User** | The human (or service identity) the agent acts for | An employee in your Okta / Entra / Descope tenant | | **Requesting application (client)** | The agent or app that already has a session with the IdP | Claude Code, VS Code, your internal agent | | **Identity provider (IdP)** | Issues the XAA token after checking enterprise policy | Descope (when you manage your agents) or the customer's Okta / Entra (when they manage theirs) | | **Resource authorization server** | Trusts the IdP, accepts the XAA token, issues its own access token | Descope protecting an MCP server you sell, or a third-party MCP vendor's AS | | **Resource** | The API or MCP server the access token is for | Linear MCP, your product's MCP server | The IdP and the resource authorization server are often **different organizations**. That split is the point: the IdP decides *whether* the hop is allowed; the resource AS decides *what access token* the resource gets. ## How the two Descope use cases map onto the protocol Descope actually plays both roles with XAA: when you manage your agents, it is the IdP; when customers manage their agents, it is the resource authorization server. | Your job | Descope's role in the diagram | Who mints the XAA token | Who redeems it | | --- | --- | --- | --- | | [Manage agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags) | **IdP** | Descope | Third-party MCP / API authorization server | | [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags) | **Resource authorization server** | Customer's Okta, Entra, or Descope | Descope (for your MCP Resource) | ## End-to-end flow Two OAuth profiles, in sequence: 1. **Token exchange (RFC 8693)** at the IdP: the client trades a token it already holds (typically an ID token or access token from SSO) for an **XAA token (ID-JAG)**. 2. **JWT Bearer grant (RFC 7523)** at the resource authorization server: the client presents that XAA token as an assertion and receives the resource's **access token**. The user is not redirected to the resource authorization server's `/authorize` endpoint on this path. Consent and access decisions already happened at the IdP (and in any policies you configure there). ## What an XAA token (ID-JAG) is An XAA token is a signed JWT. It is **not** a bearer access token for the MCP server. It is an **assertion**: a statement the IdP makes that the resource authorization server can verify and then exchange for its own access token. Typical header: ```json { "alg": "RS256", "typ": "oauth-id-jag+jwt", "kid": "..." } ``` The `typ` (or equivalent profile marker) distinguishes an ID-JAG from a normal access token so a random JWT cannot be replayed as a grant. ### Claims that matter Exact claim names follow the IETF draft and the IdP's profile. Conceptually the assertion binds: | Concept | Purpose | | --- | --- | | **Issuer (`iss`)** | Which IdP signed the assertion. The resource AS must trust this issuer and fetch its JWKs. | | **Audience (`aud`)** | The resource authorization server that is allowed to redeem the assertion. | | **Resource** | The concrete API or MCP server URL the access is for. | | **Subject** | The user (or identity) the agent acts for. | | **Client** | Which client requested the grant, so the AS can bind the grant to the authenticated client. | | **Scopes** | What the IdP is willing to allow for this hop (the resource AS may narrow further). | | **Expiry (`exp`) / `jti`** | Short lifetime; optional single-use tracking. | After a successful JWT Bearer exchange, the resource AS returns an ordinary access token whose audience is the resource. From that point on, the MCP server or API validates that access token the way it always does. ## Minting the XAA token (IdP side) At the IdP token endpoint, the client sends a token exchange roughly like: ```bash curl -X POST https://idp.example.com/oauth2/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ -d "requested_token_type=urn:ietf:params:oauth:token-type:id-jag" \ -d "subject_token=$EXISTING_IDP_TOKEN" \ -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ -d "resource=https://mcp.example.com" \ -d "scope=tools:read" ``` The IdP: 1. Authenticates the client (confidential clients are strongly preferred). 2. Validates the subject token. 3. Checks enterprise policy (which clients, users, and resources are allowed). 4. Returns the XAA token (ID-JAG), often with a very short lifetime. ### When Descope is the IdP When [you manage agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags), Descope is this IdP. The agent never talks to Descope and the downstream server in one step: Descope mints an XAA token (ID-JAG), then the downstream authorization server trades that assertion for its own access token. #### Exchange a Descope token for an XAA token The agent calls Descope's [token endpoint](/api/third-party-apps/token-endpoint) with the [token exchange](/identity-federation/inbound-apps/authorization-server#token-exchange) grant ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693)), asking for an ID-JAG as the requested token type. The **subject token** is a credential Descope already issued for your project. Descope accepts three kinds: | Subject token | `subject_token_type` | | --- | --- | | A SAML assertion issued by Descope | `urn:ietf:params:oauth:token-type:saml2` | | A Descope refresh token | `urn:ietf:params:oauth:token-type:refresh_token` | | A Descope ID token | `urn:ietf:params:oauth:token-type:id_token` | The token has to come from your own Descope project. That ties the assertion to a real user in your tenant, which is what policy is then evaluated against. ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ -d "requested_token_type=urn:ietf:params:oauth:token-type:id-jag" \ -d "subject_token=" \ -d "subject_token_type=urn:ietf:params:oauth:token-type:id_token" \ -d "resource=https://mcp.asana.com/v2/mcp" \ -d "client_id=" \ -d "client_secret=" ``` `resource` is the downstream service the agent wants to reach. Descope matches it to the [Resource](/resources) you registered, checks your token exchange policy, and mints the assertion only for what that policy allows. For Console setup (Resource, scopes, policy), see [Manage agents → XAA setup](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#xaa-setup). #### What the XAA token contains The response carries a signed JWT. Its header marks it as an ID-JAG rather than an ordinary access token: ```json { "alg": "RS256", "kid": "SK3ICInXRuWz7Dw0BAvLObJktyKeK", "typ": "oauth-id-jag+jwt" } ``` ```json { "aud": "https://mcp.asana.com", "client_id": "UDNJQ0luWW5XVE13ZmZXekt4S3QzTGFFUXZvRzpUUEEzSU5nbXRiSlZVN2RpYkVMeTR6WXZDSGlsVUUj", "exp": 1787611589, "iat": 1787611289, "iss": "https://api.descope.com/v1/apps/P3ICInYnWTMwffWzKxKt3LaEQvoG", "jti": "LJ3INiBL8Bwa6RXVs55q0pcoo0e91", "resource": "https://mcp.asana.com/v2/mcp", "scope": "", "sub": "U3ICdOPhMIascVJHWE49w92v8v3R" } ``` `aud` and `resource` are not the same value: | Claim | Meaning | | --- | --- | | **`aud`** | The **authorization server** that will validate this assertion (in the example, Asana's AS at `https://mcp.asana.com`). | | **`resource`** | The **specific protected resource** the agent wants to reach (in the example, `https://mcp.asana.com/v2/mcp`). | | `iss` | Your Descope project as the issuer, `/v1/apps/__ProjectID__`. The downstream AS fetches Descope's JWKs from here. | | `sub` | The Descope user the agent is acting for. | | `exp` / `iat` | Short-lived (five minutes in the example). Minted per request, not stored. | In the request you send only `resource`. Descope derives `aud` from the authorization server that protects that resource. The `resource` value you send and the `resource` claim you get back must both match the downstream server exactly. A trailing slash or path difference produces an assertion the downstream authorization server will reject. #### Redeem the XAA token for an access token The agent presents the assertion to the **downstream** authorization server named in `aud`, using the [JWT Bearer grant](/agentic-identity-hub/core-components/clients#jwt-bearer) ([RFC 7523](https://www.rfc-editor.org/rfc/rfc7523)): ```bash curl -X POST "https://mcp.asana.com/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ -d "assertion=" \ -d "client_id=" ``` That server validates the assertion against Descope's public keys, confirms it trusts your project as an issuer, and returns **its own** access token. Descope's involvement ends when the ID-JAG is minted. #### Client authentication for XAA The ID-JAG is a bearer assertion. Anyone holding one can present it to the downstream authorization server and receive an access token, so Descope requires the client requesting an ID-JAG to **authenticate as a confidential client** (client ID and secret, or [private key JWT](/identity-federation/inbound-apps/authorization-server#client-authentication)). | Client type | Can request an ID-JAG | Notes | | --- | --- | --- | | **Confidential** (recommended) | Yes | Authenticates with a client secret or private key JWT. | | **Public** | Possible, but not the default | Cannot hold a secret, so the assertion is harder to protect against replay. | Signing a client assertion with your own private key avoids distributing a shared secret. It is behind a feature flag — contact Descope to enable it. See [Client authentication](/identity-federation/inbound-apps/authorization-server#client-authentication). #### Registering the client with the downstream authorization server Descope being a trusted issuer is not enough on its own. The downstream AS must also recognize your client: - **[CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)** — the client presents a metadata URL; the downstream server registers it on the spot (usual path). - **Manual registration** — you create the client at the downstream server and configure its ID (and secret) in your agent. This is separate from registering the client in Descope. Clients that arrive in Descope through [CIMD or DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods) get access to the Resource they registered with automatically. A client you **create by hand** in Descope needs a [token exchange policy](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#write-a-token-exchange-policy) or the exchange fails. ## Redeeming the XAA token (resource AS side) At the resource authorization server: ```bash curl -X POST https://resource-as.example.com/oauth2/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ -d "assertion=$XAA_TOKEN" ``` The resource AS: 1. Authenticates the client. 2. Verifies the JWT signature against the IdP's JWKs. 3. Checks `iss`, `aud`, `exp`, resource, and client binding. 4. Issues an access token for that resource (and may map the subject into a local user or tenant). In Descope, when **customers** manage their agents against an MCP server you host for them, Descope is this resource authorization server: it accepts the customer's XAA token and returns a Descope access token. See [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). ## Other Notes on XAA and ID-JAG - **No second consent UI** on the resource, because the enterprise already authorized the hop at the IdP. - **Short-lived assertion**: the XAA token is meant to be exchanged quickly, not stored as a long-lived API key. - **Audience-restricted access token**: the token the resource AS issues should be bound to the resource named in the assertion. - **Confidential clients**: anyone holding a valid XAA token can attempt redemption, so Descope expects authenticated clients when minting ID-JAGs. - **Per-tenant trust** on the validation side: each customer's issuer is registered on their tenant so one customer's assertions cannot buy access for another. # Enterprise-Managed Authorization (/agentic-identity-hub/enterprise-managed-authorization) Govern pre-built agents reaching third-party MCP servers with XAA or a gateway. # Enterprise-Managed Authorization **Enterprise-Managed Authorization** answers *which agents may reach which tools, for which users* in one place: the enterprise identity provider. Without it, every MCP server becomes its own authorization island with its own consent screen. The primary scenario is **pre-built clients you do not control** (Claude Code, VS Code, Cursor, and similar) talking to **third-party MCP servers and APIs you do not protect with Descope** (HubSpot, Asana, Linear, Canva, and so on), over a standard protocol such as MCP. Descope is the IdP those clients sign into. That is not the only way to use the Hub. For building MCP servers (MCP Auth), building agents (Agent Auth SDK), and governing agents internally (XAA or gateway), see [Use cases](/agentic-identity-hub/use-cases). ## Connecting to third-party servers For that pre-built-client → third-party-server case, there are **two main ways** to connect: } title="Cross App Access (XAA)" href="/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-xaa" description="Descope mints a short-lived XAA token (ID-JAG). The third-party authorization server validates it. Nothing sits between the agent and the tool." /> } title="Gateway" href="/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-a-gateway" description="A gateway sits in the request path. Descope governs authorization; Connections hold credentials. Use when the target lacks XAA — or when you want gateway security controls XAA does not provide." /> ### Cross App Access (XAA) **Cross App Access (XAA)** reuses the SSO trust the enterprise already has. The identity provider that signed the user in also vouches, in a short-lived signed JWT, that this user, through this client, may reach this resource. The client presents that JWT to the resource's authorization server and receives a normal access token in return. That JWT is the **XAA token**, formally an [Identity Assertion JWT Authorization Grant (ID-JAG)](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/). XAA also applies when the client is Claude (or similar) and the MCP server is **yours** — Descope can be both issuer and validator. See [Use cases → Governing agents internally](/agentic-identity-hub/use-cases#governing-agents-internally). - Protocol: [How XAA and ID-JAG work](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works) - Setup: [Manage agents in your enterprise → Enforce with XAA](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-xaa) ### Gateway This feature is in early access, and must be enabled by Descope Support for your project. A **gateway** sits between the pre-built agent and third-party tools. Descope remains the IdP and policy decision point; [Connections](/agentic-identity-hub/core-components/connections) hold the downstream credentials. Choose a gateway when: - The third-party does **not** support Cross App Access (most servers today), or - You want capabilities XAA alone does not give you — for example prompt-injection detection, centralized inspection of tool calls, unified audit, or tenant-scoped credential routing through one entrypoint. We work out of the box with [agentgateway](https://agentgateway.dev/), or you can bring your own gateway and configure it to use Descope as a PDP. - Overview: [Manage agents → Enforce with a Gateway](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags#enforce-with-a-gateway) - Build guide: [MCP Gateways](/mcp/gateways) ## Hosting an MCP server for your customers? If you host an MCP server that other enterprises connect to, each customer manages *their* agents with *their* XAA-compatible workforce IdP, and Descope accepts those customers' XAA tokens for your server. That setup lives with tenant SSO and the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration). See [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags) and [Cross App Access for B2B MCP Servers](/mcp#cross-app-access-xaa). # Let Customers Manage Their Agents (/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags) Let enterprise customers govern agents that reach your MCP server using their own XAA-compatible workforce IdP, such as Okta or Ping. # Let Customers Manage Their Agents If **you** instead run the agents and need them to call third-party MCP servers, see [Manage agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags). Use this guide when **you host an MCP server for your customers** (the [B2B MCP](/mcp#b2c-and-b2b-companies) case) and each enterprise customer should govern **their own** users and agents with **their** XAA-compatible workforce IdP. Customers keep using Okta, Ping, or their own [Descope project](/agentic-identity-hub), as their XAA-enabled workforce IdP. Their IT decides which agents may reach your server. Descope, as the authorization server for the MCP server you protect with [Descope MCP Auth](/agentic-identity-hub/core-components/mcp-servers), accepts that customer's **XAA token** (ID-JAG), validates it against the issuer you trusted for that tenant, and mints an ordinary Descope access token for your server. Your MCP server does not need to understand ID-JAG. You do not provision the customer's workforce users for this path; their admin controls access in their IdP, and your MCP server honors what it decides. For the protocol, see [How XAA and ID-JAG work](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works). ## How It Works The premise is that two organizations each decide the half of the question they actually own. The customer's IdP decides **whether** one of their agents may reach your MCP server at all, because only they know their own employees and their own policies. Descope, sitting in front of that server, decides **what** that agent gets once it arrives, because only you know your server's scopes. Cross App Access is the handoff between those two decisions. The customer's IdP writes its decision into a short-lived signed assertion, the **ID-JAG**, and Descope verifies that assertion before honoring it. However the customer's agent obtained its XAA token, the part you configure is the same. The agent presents that token to your Descope-protected MCP server. Descope matches the assertion's **subject** to the user in the matching [tenant](/management/tenant-management), and mints an access token for your server. This works the same whether the XAA token came from Okta, Ping, or any other workforce IdP the customer uses that supports Cross App Access / ID-JAG. The customer's workforce IdP owns the first authorization decision: whether a given client may request cross-app access to your MCP server at all, through its own Cross App Access configuration. Descope owns the second: which scopes the resulting Descope token carries. See [Agent Authorization](/agentic-identity-hub/policies) for how those scopes are resolved. ### Your MCP server is the Resource The target being protected is your own MCP server, registered in Descope as an [MCP Server Resource](/resources). Its URL is the audience the customer's IdP names when it mints an ID-JAG, and it is what Descope issues the final access token for. Your server validates that token exactly as it does for any other request. ## Enable Cross-App Access per Tenant Each enterprise customer is a [tenant](/management/tenant-management) in your Descope project. Under a tenant, you register that customer's workforce IdP as a **trusted issuer** whose ID-JAGs Descope will accept for your MCP server. Cross-App Access is part of the tenant's **SSO configuration**, because the assertions you accept come from the same workforce IdP the tenant already signs in with. It reuses the [JWT Bearer grant](/agentic-identity-hub/core-components/clients#jwt-bearer): you add the issuer's URL and keys, and Descope accepts assertions signed by it. Because trust is scoped to the tenant, each customer's users flow through their own IdP, and an ID-JAG minted for one customer can never buy access for another. ### Two Ways to Configure XAA The configuration can be written either by you or by the customer, and both produce the same tenant settings: | Path | Who does it | Where | | --- | --- | --- | | **Console** | You, on the customer's behalf | Your tenant, then **Authentication Methods → SSO → Cross-App Access** | | **[SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration)** | The customer's IT admin, self-service | The **Cross App Access** section, alongside SSO and SCIM | The Setup Suite path is usually what you want for a B2B product, since the customer configures Cross-App Access for their own organization in the same session where they set up SSO, without a support ticket. Whether the Cross App Access (XAA) section appears in the SSO Setup Suite, is controlled by the [SSO Suite Features](/auth-methods/sso/settings#sso-suite-features) setting. ### Turning it on In the Console, open the tenant, go to **Authentication Methods → SSO**, and select the **Cross-App Access** tab. Enable **Allow Cross-App Access (ID-JAG)**, which is off by default. Turning it on reveals the sections below. ![allow cross-app access (xaa) toggle off](/assets/tenant-xaa-enable.webp) The tab has three parts, and they run in order. - **Resource Server Details** gives you values to hand to the customer. - **Trusted Issuer** is where you accept their IdP. - **JIT Provisioning** decides how their users become users in your project. ### Resource Server Details This section is read-only. The customer's IdP must send the tenant ID in the **`aud_tenant`** claim. Descope uses it to work out which tenant's Cross-App Access configuration an incoming assertion should be evaluated against. If `aud_tenant` is missing, or carries a value different from the one shown here, token validation fails. This is the most common reason a correctly signed ID-JAG is still rejected. It shows the two values that identify your resource server to the customer's IdP, and the customer copies both into the cross-app access configuration on their side. | Value | Example | Where it lands | | --- | --- | --- | | **Audience** | `__BaseURL__/v1/apps/__ProjectID__` | The `aud` claim of the ID-JAGs their IdP mints | | **Tenant ID** | `T2fmCOB4Ps5bBPnN4EVAb4cNdjPX` | The `aud_tenant` claim of those same ID-JAGs | Together the two values are what keep customers separated. `aud` says the assertion is for your project, and `aud_tenant` says which customer inside it. An ID-JAG minted for one tenant cannot be redeemed against another. ![Resource Server Details section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-resource-server-details.webp) ### Trusted Issuer Which IdP Descope accepts assertions from, and how it verifies their signatures. An ID-JAG is only worth as much as the issuer behind it, so this is the setting that carries the trust. The customer gives you their IdP's issuer URL, and Descope uses it two ways. It is the value every incoming assertion's `iss` claim has to match, and it is where Descope fetches the public keys that verify the assertion's signature. On each incoming assertion Descope then checks that the signature verifies against those keys, that `iss` is the issuer registered on this tenant, that `aud` matches the [Audience](#resource-server-details) you configured, and that the assertion has not expired. An assertion that fails any of these is rejected and no access token is issued. Registering the issuer on a specific tenant is what keeps customers apart. An assertion is only ever evaluated against the issuer configured for the tenant it targets, so an ID-JAG minted by one customer's Okta cannot buy access anywhere else in your project no matter how valid its signature is. | Field | Required | Description | | --- | --- | --- | | **Issuer URL** | Yes | The expected `iss` claim on incoming ID-JAGs. If the issuer is discoverable, saving the configuration fetches the JWKs URL and the rest of the metadata automatically. | | **JWKs URL** | Only if the issuer is not discoverable | The issuer's JSON Web Key Set, used to fetch the public keys that verify the JWT signature. You can set it manually, otherwise Descope attempts to discover it from the Issuer URL. | | **Sign Algorithm** | No | The algorithm used to verify the JWT signature, for example `RS256` or `ES256`. Left blank, the algorithm is read from the token header. | | **User Information Endpoint URL** | No | An endpoint called after the token validates, to fetch additional attributes about the subject user that are not carried in the JWT. | A tenant can trust more than one issuer. The **+ Add issuer** button at the bottom of the section adds another set of these fields, which is what you would use when a customer runs more than one IdP, or is midway through moving between them. Descope accepts an assertion if it matches any issuer configured on the tenant. ![The Trusted Issuer section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-trusted-issuer.webp) ### JIT Provisioning The assertion names a subject, but that subject is a user in the customer's directory, not yet a user in yours. Something has to create the Descope user record that the access token is eventually issued for, and you have two ways to do it. **Enable JIT Provisioning** is a toggle that creates *and updates* that user from the claims in the ID-JAG, every time one validates. The first time a given employee's agent calls your server the user appears in the tenant, and later assertions keep their attributes current. Nothing has to be set up in advance, which is why JIT is the quicker path to a working integration. With the toggle off, provisioning and mapping have to happen some other way, in practice through [SCIM](/management/tenant-management/scim). Leaving it off means the exchange only succeeds for users who already exist in the tenant. ![The JIT Provisioning section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-jit.webp) #### Choosing between JIT and SCIM The tradeoff is not about how users get created. It is about what happens when they leave. | | JIT provisioning | SCIM provisioning | | --- | --- | --- | | **User created** | On the first ID-JAG exchange | Pushed by the customer's IdP ahead of time | | **Setup required** | None | The customer configures SCIM on their tenant | | **Offboarding** | Nothing tells Descope the user is gone | The IdP pushes the deactivation to Descope | JIT only ever hears about a user when that user shows up. It has no channel for the opposite event. When an employee leaves the company and their IdP account is disabled, their agent stops being able to obtain new ID-JAGs, but the Descope user record you created from an earlier assertion stays active, and so does any Descope session already issued for it. SCIM is the channel for that event. The customer's IdP pushes the deactivation, Descope marks the user inactive, and the next time that user's session tries to refresh, the refresh fails and access ends. That is what makes SCIM the right answer for enterprise customers who expect offboarding in their directory to actually revoke access to your product. A deactivation takes effect on the next login or token refresh, so an `access_token` already in an agent's hands remains valid until it expires. Shorten your [session and refresh token durations](/management/project-settings#session-management) if you need a tighter window, and revoke the session directly for an immediate cutoff. See [SCIM session behavior](/management/tenant-management/scim#scim-session-behavior), for more details. So: enable JIT when you do not manage that tenant's users with SCIM, and it is the reasonable default for getting a customer running. When SCIM provisions the tenant's users, leave JIT off and let SCIM own the user record, so that the lifecycle events you care about arrive through the same channel that created the user. Entra pushes SCIM changes on a roughly 40 minute cycle, and disables in particular often wait for a provisioning job that includes that user. If a customer needs a deactivation reflected immediately, they can run **Provision on Demand** in Entra. See [SCIM best practices](/management/tenant-management/scim/scim-best-practices#provisioning-timing). #### User Attribute Mapping This is the same attribute mapping you already know from SSO, with one difference in where the values come from. Instead of reading a SAML assertion or an `id_token`, you are mapping claims carried on the **ID-JAG** onto Descope user attributes. It matters most when the customer's IdP sends custom claims in the assertion that you want reflected on the user profile, since JIT builds the user from exactly these claims. ![The User Attribute Mapping section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-user-attribute-mapping.webp) SCIM provisions users according to the existing SCIM and SSO configuration and mapping. It does not read the ID-JAG claims mapped here, so these mappings only take effect on the JIT path. #### Group Attribute Mapping Here you enter the **name of the claim** that carries group association, commonly `groups`. Descope reads group names out of that claim on the ID-JAG and maps them onto the tenant's groups. This lets a customer's existing IdP group membership drive access without a separate assignment step in your product. ![The Group Attribute Mapping section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-group-attribute-mapping.webp) Not every IdP sends groups in an ID-JAG. Okta, for example, does support sending group values in the assertion, so check what your customer's IdP emits before relying on this mapping. ## Setup Instructions ### Protect your MCP server Create the [MCP Server Resource](/resources) and stand up [Descope MCP Auth](/mcp/mcp-server) as you would for any MCP server you operate. ### Add a tenant per customer Represent each enterprise customer as a [tenant](/management/tenant-management). ### Turn on Cross-App Access and register the issuer On that tenant, go to **Authentication Methods → SSO → Cross-App Access** and enable **Allow Cross-App Access (ID-JAG)**. Under **Trusted Issuer**, add the customer IdP's **Issuer URL** so Descope accepts its assertions, using **+ Add issuer** if they run more than one. Add a **JWKs URL** as well if the issuer is not discoverable. See [Enable Cross-App Access per Tenant](#enable-cross-app-access-per-tenant) for every field. ### Decide how the tenant's users are provisioned Turn on **JIT Provisioning** if SCIM is not managing that tenant's users, which is the quickest way to get a customer working. If the customer provisions users with [SCIM](/management/tenant-management/scim), leave JIT off so deactivations in their directory also reach Descope and end access to your product. See [Choosing between JIT and SCIM](#choosing-between-jit-and-scim) for what that choice costs you at offboarding time. ### Hand the resource server details to the customer Copy the **Audience** and **Tenant ID** from [Resource Server Details](#resource-server-details) and give both to the customer, who enters them in the cross-app access configuration of their IdP. Their IdP has to send the tenant ID in the **`aud_tenant`** claim. A missing or mismatched `aud_tenant` causes validation to fail even when everything else is correct. The customer's IT admin can do all of this themselves through the [Cross App Access](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration) section of the SSO Setup Suite. ### Write policies Use [Policies](/agentic-identity-hub/policies) to decide which scopes the exchanged token carries, keyed on `user.roles`, `user.tenantIds`, and claims carried over from the assertion. # Descopers with SDKs (/management/company-settings/descopers-sdks) Learn how to create and update Descopers using the Descope backend SDKs. # Descopers with SDKs You can use the Descope management SDK to create, update, delete, or load Descopers (Descope console users). The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). If you wish to learn more about Descopers in general, see the [Company Settings](/management/company-settings#descopers) page. ## Descoper management using the management SDK ### Load All Descopers You can also use our [List Descopers API](/api/management/descopers/list-descopers) to list all Descopers. This operation lists all Descopers in your company and returns both the Descoper details and the total count. ```javascript const resp = await descopeClient.management.descoper.list(); if (!resp.ok) { console.log("Failed to load descopers.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded descopers.") console.log("Total: " + resp.data.total) console.log(resp.data.descopers) } ``` ```python try: resp = descope_client.mgmt.descoper.list() print ("Successfully loaded descopers.") print ("Total: " + str(resp["total"])) print(json.dumps(resp["descopers"], indent=2)) except AuthException as error: print ("Unable to load descopers.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // options (*descope.DescoperLoadOptions): Optional load options. Currently unused; nil is allowed. var options *descope.DescoperLoadOptions = nil descopers, total, err := descopeClient.Management.Descoper().List(ctx, options) if (err != nil){ fmt.Println("Unable to load descopers: ", err) } else { fmt.Println("Successfully loaded descopers.") fmt.Println("Total: ", total) fmt.Println(descopers) } ``` ### Load Descoper by ID You can also use our [Get Descoper API](/api/management/descopers/get-descoper) to load a specific Descoper. This operation loads an existing Descoper by ID, including their attributes, status, and RBAC configuration. ```javascript // Args: // id (str): The Descoper ID. const id = "descoper-id"; const resp = await descopeClient.management.descoper.load(id); if (!resp.ok) { console.log("Failed to load descoper.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded descoper.") console.log(resp.data) } ``` ```python # Args: # id (str): The Descoper ID. id = "descoper-id" try: resp = descope_client.mgmt.descoper.load(id) print ("Successfully loaded descoper.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load descoper.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (string): The Descoper ID. id := "descoper-id" descoper, err := descopeClient.Management.Descoper().Get(ctx, id) if (err != nil){ fmt.Println("Unable to load descoper: ", err) } else { fmt.Println("Successfully loaded descoper.") fmt.Println(descoper) } ``` ### Create Descoper You can also use our [Create Descopers API](/api/management/descopers/create-descopers) to create a Descoper. This operation creates one or more Descopers within the company. Each Descoper requires a login ID. You can optionally set attributes (display name, email, phone), send an invitation email, and configure [Descoper roles](/management/company-settings#descoper-roles) via RBAC — typically as company admin (`isCompanyAdmin`), or with granular access scoped to projects or tags. ```javascript // Args: // descopers (List[DescoperCreate]): An array of Descoper creation objects. Each Descoper must have a loginId. // loginId (str): The login ID for the Descoper. // attributes (DescoperAttributes): Optional attributes (displayName, email, phone). // sendInvite (bool): Optional. Set to true to send an invitation email. // rbac (DescoperRBAC): Optional RBAC configuration. Typically use exactly one of isCompanyAdmin, projects, or tags. const descopers = [ { loginId: "user@example.com", attributes: { displayName: "Test User", email: "user@example.com", phone: "+1234567890", }, sendInvite: true, rbac: { // exactly one of isCompanyAdmin, projects, or tags projects: [ { projectIds: ["project-id"], role: "admin", // 'admin' | 'developer' | 'support' }, ], }, }, ]; const resp = await descopeClient.management.descoper.create(descopers); if (!resp.ok) { console.log("Failed to create descoper.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created descoper.") console.log("Total: " + resp.data.total) console.log(resp.data.descopers) } ``` ```python # Args: # descopers (List[DescoperCreate]): A list of Descoper creation objects. Each Descoper must have a login_id. # login_id (str): The login ID for the Descoper. # attributes (DescoperAttributes): Optional attributes (display_name, email, phone). # send_invite (bool): Optional. Set to True to send an invitation email. # rbac (DescoperRBAC): Optional RBAC configuration. Typically use exactly one of is_company_admin, projects, or tags. descopers = [ DescoperCreate( login_id="user@example.com", attributes=DescoperAttributes( display_name="Test User", email="user@example.com", phone="+1234567890", ), send_invite=True, rbac=DescoperRBAC( # exactly one of is_company_admin, projects, or tags projects=[ DescoperProjectRole( project_ids=["project-id"], role=DescoperRole.ADMIN, # ADMIN | DEVELOPER | SUPPORT ) ], ), ) ] try: resp = descope_client.mgmt.descoper.create(descopers=descopers) print ("Successfully created descoper.") print ("Total: " + str(resp["total"])) print(json.dumps(resp["descopers"], indent=2)) except AuthException as error: print ("Unable to create descoper.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // descopers ([]*descope.DescoperCreate): A list of Descoper creation objects. Each Descoper must have a LoginID. // LoginID (string): The login ID for the Descoper. // Attributes (*descope.DescoperAttributes): Optional attributes (DisplayName, Email, Phone). // SendInvite (bool): Optional. Set to true to send an invitation email. // ReBac (*descope.DescoperRBAC): Optional RBAC configuration. Typically use exactly one of IsCompanyAdmin, Projects, or Tags. descopers := []*descope.DescoperCreate{ { LoginID: "user@example.com", Attributes: &descope.DescoperAttributes{ DisplayName: "Test User", Email: "user@example.com", Phone: "+1234567890", }, SendInvite: true, ReBac: &descope.DescoperRBAC{ // exactly one of IsCompanyAdmin, Projects, or Tags Projects: []*descope.DescoperProjectRole{ { ProjectIDs: []string{"project-id"}, Role: descope.DescoperRoleAdmin, // Admin | Developer | Support }, }, }, }, } created, total, err := descopeClient.Management.Descoper().Create(ctx, descopers) if (err != nil){ fmt.Println("Unable to create descoper: ", err) } else { fmt.Println("Successfully created descoper.") fmt.Println("Total: ", total) fmt.Println(created) } ``` ### Update Descoper You can also use our [Update Descopers API](/api/management/descopers/update-descoper) to update a Descoper. This operation updates an existing Descoper. You can update attributes (display name, email, phone) and/or RBAC configuration (company admin, or granular project/tag roles). ```javascript // Args: // id (str): The Descoper ID. const id = "descoper-id"; // attributes (DescoperAttributes): Optional attributes to update (displayName, email, phone). const attributes = { displayName: "Updated Name", }; // rbac (DescoperRBAC): Optional RBAC configuration to update. Typically use exactly one of isCompanyAdmin, projects, or tags. const rbac = { isCompanyAdmin: true, }; const resp = await descopeClient.management.descoper.update(id, attributes, rbac); if (!resp.ok) { console.log("Failed to update descoper.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated descoper.") console.log(resp.data) } ``` ```python # Args: # id (str): The Descoper ID. id = "descoper-id" # attributes (DescoperAttributes): Optional attributes to update (display_name, email, phone). attributes = DescoperAttributes( display_name="Updated Name", ) # rbac (DescoperRBAC): Optional RBAC configuration to update. Typically use exactly one of is_company_admin, projects, or tags. rbac = DescoperRBAC( is_company_admin=True, ) try: resp = descope_client.mgmt.descoper.update(id=id, attributes=attributes, rbac=rbac) print ("Successfully updated descoper.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update descoper.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (string): The Descoper ID. id := "descoper-id" // attributes (*descope.DescoperAttributes): Optional attributes to update (DisplayName, Email, Phone). attributes := &descope.DescoperAttributes{ DisplayName: "Updated Name", } // rbac (*descope.DescoperRBAC): Optional RBAC configuration to update. Typically use exactly one of IsCompanyAdmin, Projects, or Tags. rbac := &descope.DescoperRBAC{ IsCompanyAdmin: true, } descoper, err := descopeClient.Management.Descoper().Update(ctx, id, attributes, rbac) if (err != nil){ fmt.Println("Unable to update descoper: ", err) } else { fmt.Println("Successfully updated descoper.") fmt.Println(descoper) } ``` ### Delete Descoper You can also use our [Delete Descopers API](/api/management/descopers/delete-descoper) to delete a Descoper. This operation deletes an existing Descoper by ID. It is important to note that this operation is irreversible and the Descoper will be removed and will not be able to be added back without recreation. ```javascript // Args: // id (str): The Descoper ID. const id = "descoper-id"; const resp = await descopeClient.management.descoper.delete(id); if (!resp.ok) { console.log("Failed to delete descoper.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted descoper.") } ``` ```python # Args: # id (str): The Descoper ID. id = "descoper-id" try: descope_client.mgmt.descoper.delete(id) print ("Successfully deleted descoper.") except AuthException as error: print ("Unable to delete descoper.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (string): The Descoper ID. id := "descoper-id" err := descopeClient.Management.Descoper().Delete(ctx, id) if (err != nil){ fmt.Println("Unable to delete descoper: ", err) } else { fmt.Println("Successfully deleted descoper.") } ``` # Company Settings (/management/company-settings) Learn how to customize your Descope company settings. # Company Settings On the [company page](https://app.descope.com/settings/company) of the Descope console, you can manage company-level settings, Descopers and their permissions, management keys, projects, and usage. Actions taken on this page — such as creating or deleting projects, creating or modifying management keys, and changing company settings — are recorded as company-level audit events. To review them, see [Company-level Auditing](/audit-trails-and-integrations#company-level-auditing) and the [Company-level Audit Events](/audit-trails-and-integrations/audit-events#company-level-audit-events) reference. ## Settings Within the [Settings tab](https://app.descope.com/settings/company/settings), the following can be configured: ### General - The `Company Name`. - The `Company ID`, which is used to identity your Descope company. ### Console Access - **Enforce SSO** - This will be visible once SSO is configured for the console. Will oblige Descopers to log in through SSO. - **SSO Exclusions** - This will be visible once SSO is configured for the console. Will allow you to specify login IDs that can bypass SSO authentication. - **Force MFA** - Will force Descopers to go through MFA, while allowing the Descoper to choose their preferred MFA method from the following options: Passkeys, OTP via SMS, or TOTP. ![Descope Company Settings console access](/assets/descope-console-access-1.webp) #### SSO Configurations Enabling Cross App Access (XAA) for your company will allow you to use XAA with the [Descope MCP Server](/mcp/mcp-server#cross-app-access-xaa). This section gives you your link to configure SSO for your Descope Company, using our [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). You can configure multiple SSO configurations per Company, which can be distinguished by the unique [SSO domains](/auth-methods/sso/sso-setup-suite#sso-domains) you set in each configuration. ![Descope Company Settings SSO configurations](/assets/sso-configurations-company.webp) Within the SSO Setup Suite, you can also configure [SCIM provisioning](/auth-methods/sso/sso-setup-suite#scim-configuration) for your Descope Company. This enables you to manage Descope console access through your identity provider's SCIM provider, rather than relying on JIT (Just-In-Time) provisioning. Group mapping for Descoper permissions is done in the [Role Mapping](#role-mapping) section below. #### Session Settings You can configure session settings for your Descope Company, including session (access) token duration, session inactivity timeout, and refresh token duration. The time units allowed for these settings are: minutes, hours, days, and weeks. #### Role Mapping At the bottom of the widget, there is a section where you can configure role mapping for your Descope Company. You can choose to either provide full access, or configure granular permissions for users within specific groups from your SSO provider. You can also map SSO groups to specific [project-level tags](/management/project-settings#general), which can be associated with specific [project-level roles](/management/company-settings#descoper-roles). You can associate the "Company Admin" with an SSO group that will provide administrative access to all projects and company settings. ![Descope Company Settings granular permissions](/assets/descope-company-settings-granular-permissions.webp) #### Permissions - `Allow Developer Success to access my data for troubleshooting purposes`: Optional. Allows the Descope Customer Success team to capture further information within the project for troubleshooting purposes. - `Enable AI-powered features and insights`: Enables built-in AI analysis of troubleshooting logs, flow activity, flow diffs, and related data right from the Descope console. Enabled by default. Turn this toggle off if you want to disable AI features for your company to satisfy internal governance and legal requirements. ![Descope Company Settings permissions](/assets/descope-company-settings-troubleshooting-ai-toggles.webp) ## Descopers Users who have access to your Descope Console are known as Descopers. You can manage these from the [Descopers](https://app.descope.com/settings/company/admins) tab in the Console. Here you can create, delete, and manage your Descopers. When creating a new Descoper you can choose whether to send the invitation via email, and can also configure the [Descoper role](/management/company-settings#descoper-roles) for the user. ### Descoper Roles Descopers can be associated to specific projects, tags, and roles. When inviting a Descoper or editing their roles, you can select `Granular permissions` instead of `Full access` and configure the Descoper's roles based on project or tag. This allows you to grant a Descoper unique access or roles for any project in the company or for any tag associated with a group of projects. ![Granular permissions when inviting Descopers](/assets/invite-descopers-granular-permissions.webp) | Role | Description | | ------------- | ------------------- | | Company Admin | Company admins have full read/write access across the company and all projects. | | Project Admin | Descopers associated to project(s) with the Admin role have full read/write access across the projects they are associated with. | | Project Developer | Descopers associated to project(s) with the Developer role have read/write access across all of the projects they are associated, including the Project-level settings. However, they will not have read/write access to Company-level settings. | | Project Support | Descopers associated to project(s) with the Support role have access to read the following: **Authentication Methods**, **Flows**, **Connectors**, **IdP Apps**, **Authorization**, and **Project Settings**. These users have full read/write access within the users, access keys, tenants, and audit pages. | #### Custom Descoper Roles In addition to the predefined roles, you can create **custom Descoper roles** with granular permissions tailored to your organization's needs from the [custom roles tab](https://app.descope.com/settings/company/customroles) of the Descope console. Custom roles allow you to control **Edit or View** access to specific console sections. For example, you can create a role that allows editing flows and widgets but restricts access to user management, or a read-only role that provides visibility across all projects without modification rights. Descopers can also control [Sidebar Preferences](/sidebar-preferences), to hide or shows specific pages within their own sidebar. However, control of what is shown is not controlled by an Descope administrator. | Role | Description | | ------------- | ------------------- | | Access Keys Edit | Manage access keys for the company. Includes view and edit access to access keys, authorization, projects, and tenants. | | Access Keys View | View access key configurations. Includes view access to access keys, authorization, projects, and tenants. | | Agentic Hub Edit | Manage agentic identities, MCP servers, and connections. Includes view and edit access to agentic identities, MCP servers, MCP server clients, and third-party apps. | | Agentic Hub View | View agentic identities, MCP servers, and connections. Includes view access to agentic identities, MCP servers, MCP server clients, third-party apps, audits, authorization, flows, projects, tenants, and users. | | Audits View | View audit logs and activity history. Includes view access to audit logs and tenants. | | Authentication Methods Edit | Configure authentication methods. Includes view and edit access to authentication methods and connectors, plus view access to authorization, projects, users, tenants, styles, audits, and flows. | | Authentication Methods View | View authentication method configurations. Includes view access to authentication methods, authorization, projects, users, tenants, styles, audits, connectors, and flows. | | Authorization Edit | Manage roles, permissions, and FGA configuration. Includes view access to authorization and edit access to auth roles and permissions. | | Authorization View | View roles, permissions, and FGA configuration. Includes view access to authorization settings. | | Connectors Edit | Configure and manage connectors. Includes view and edit access to connectors, plus view access to audits, authentication methods, flows, tenants, and projects. | | Connectors View | View connector configurations. Includes view access to connectors, audits, authentication methods, flows, tenants, and projects. | | Flows Edit | Create and modify authentication flows. Includes view and edit access to flows, plus view access to authentication methods, connectors, projects, users, authorization, Federated Apps, styles, and tenants. | | Flows View | View authentication and authorization flows. Includes view access to flows, authentication methods, connectors, projects, users, authorization, Federated Apps, styles, and tenants. | | Getting Started | Run the "Getting Started" setup wizard. Includes access to wizard views and edits, plus view and edit access to flows, projects, tenants, and styles. | | Home View | View the project home dashboard and overview. Includes view access to home dashboard, audits, connectors, flows, projects, tenants, and third-party apps. | | Inbound Apps Edit | Configure inbound applications. Includes view and edit access to third-party apps, plus view access to authorization, flows, projects, users, and tenants. | | Inbound Apps View | View inbound application configurations. Includes view access to third-party apps, authorization, flows, projects, users, and tenants. | | Localization Edit | Manage localization and translations. Includes view and edit access to authentication methods, flows, and third-party apps, plus view access to connectors. | | Localization View | View localization and translations. Includes view access to authentication methods, connectors, flows, and third-party apps. | | Outbound Apps Edit | Configure outbound applications. Includes view and edit access to Federated Apps and third-party apps, plus view access to tenants. | | Outbound Apps View | View outbound application configurations. Includes view access to Federated Apps, third-party apps, and tenants. | | Project Settings Edit | Manage project settings. Includes view and edit access to project settings, connectors, plus view access to tenants, authentication methods, audits, flows, and users. | | Project Settings View | View project settings and configuration. Includes view access to project settings, tenants, authentication methods, connectors, audits, flows, and users. | | Federated Applications Edit | Configure federated applications. Includes view and edit access to Federated Apps, plus view access to audits, authorization, flows, users, and authentication methods. | | Federated Applications View | View federated application configurations. Includes view access to Federated Apps, audits, authorization, flows, users, and authentication methods. | | Style Editor Edit | Customize flow styles and branding. Includes view and edit access to styles, plus view access to authentication methods. | | Style Editor View | View flow styles and branding. Includes view access to styles and authentication methods. | | Tenants Edit | Manage tenants and their settings. Includes view and edit access to tenants, plus view access to authentication methods, authorization, connectors, projects, Federated Apps, users, and styles. | | Tenants View | View tenant configurations. Includes view access to tenants, authentication methods, authorization, connectors, projects, Federated Apps, users, and styles. | | Users Edit | Manage user accounts. Includes view and edit access to users and user CA settings, plus view access to authorization, projects, Federated Apps, tenants, and connectors. | | Users View | View user accounts and details. Includes view access to users, authorization, projects, Federated Apps, tenants, and connectors. | | Widgets Edit | Configure and customize widgets. Includes view and edit access to styles and flows, plus view access to authentication methods, connectors, projects, users, tenants, authorization, and Federated Apps. | | Widgets View | View widget configurations. Includes view access to widgets, styles, authentication methods, connectors, projects, users, tenants, flows, authorization, and Federated Apps. | ## Management Keys Within the [Management Keys tab](https://app.descope.com/settings/company/managementkeys) you can create, delete, and manage Management keys within your company. Review the [Management Keys](/management#management-keys) documentation for more information about how they can be used. ## Projects On the [Projects](https://app.descope.com/settings/company/projects) tab you see every project in your company. Selecting a project opens its project page so you can edit that project's settings. The settings icon at the end of each row opens more actions: Edit, Clone, Export (Pro plan only), and Delete. The projects table includes the following columns. Click the arrow next to any column header to sort in ascending or descending order. Optional columns can be shown or hidden from the column picker. | Column | Description | | --- | --- | | Name | The display name of the project. | | ID | The unique project identifier. | | Environment | The project environment (for example, development, staging, or production). | | Region | The geographic region where the project is deployed. | | Created Time | When the project was created. | | App URL | The application URL which your application resides on. | | Tags | Custom labels used to organize and filter projects. | | Block Sign Up Config | Whether sign-up is blocked for the project. (Optional column) | | Custom Domain | The custom domain configured for the project. (Optional column) | | Approved Domain | Domains allowed for redirect and verification URLs across authentication methods. (Optional column) | Use the search filter to narrow the project list by different fields when you have many projects. To create a new project, click the `+Project` button on the right. ![Descope Project Overview](/assets/descope-company-settings-projects.webp) ## Usage The [Usage tab](https://app.descope.com/settings/company/usage) on Company Settings summarizes usage metrics for your company. For each metric you can see totals for the **current month** and the **previous month**: - Monthly Active Users - Monthly Active Tenants - SSO Connections - Monthly M2M Exchanges - Monthly Active Consents - Monthly Active Tokens For plan allowances, how overages work, and explanations of this terminology, see [Descope pricing](https://www.descope.com/pricing). ![Company Usage](/assets/company_usage.webp) # Management Keys with SDKs (/management/company-settings/mgmt-keys-sdks) Learn how to create and update Management Keys using the Descope backend SDKs. # Management Keys with SDKs You can use the Descope management SDK to create, update, delete, load, or search Management Keys. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). If you wish to learn more about how Management Keys work, see [Company Settings](/management/company-settings#management-keys). ## Management Keys using the management SDK ### Search All Management Keys You can also use our [Search Management Keys API](/api/management/management-keys/search-management-keys) to list all Management Keys. This operation returns all Management Keys in your company. ```javascript const resp = await descopeClient.management.managementKey.search(); if (!resp.ok) { console.log("Failed to search management keys.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully searched management keys.") console.log(resp.data) } ``` ```python try: resp = descope_client.mgmt.management_key.search() print ("Successfully searched management keys.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to search management keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // options (*descope.MgmtKeySearchOptions): Required search options. Currently unused; pass an empty struct (nil is not allowed). options := &descope.MgmtKeySearchOptions{} keys, err := descopeClient.Management.ManagementKey().Search(ctx, options) if (err != nil){ fmt.Println("Unable to search management keys: ", err) } else { fmt.Println("Successfully searched management keys.") fmt.Println(keys) } ``` ```csharp try { var keyRes = await descopeClient.Mgmt.V1.Managementkey.Search.GetAsync(); } catch (DescopeException ex) { // Handle the error } ``` ### Load Management Key by ID You can also use our [Get Management Key API](/api/management/management-keys/get-management-key) to load a specific Management Key. This operation loads an existing Management Key by ID, including its name, description, status, expiration, permitted IPs, and role configuration. The key secret (cleartext) is not returned. ```javascript // Args: // id (str): The Management Key ID. const id = "key-id"; const resp = await descopeClient.management.managementKey.load(id); if (!resp.ok) { console.log("Failed to load management key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded management key.") console.log(resp.data) } ``` ```python # Args: # id (str): The Management Key ID. try: resp = descope_client.mgmt.management_key.load("key-id") print ("Successfully loaded management key.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load management key.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (string): The Management Key ID. id := "key-id" key, err := descopeClient.Management.ManagementKey().Get(ctx, id) if (err != nil){ fmt.Println("Unable to load management key: ", err) } else { fmt.Println("Successfully loaded management key.") fmt.Println(key) } ``` ```csharp // Args: // id (string): The Management Key ID. var id = "key-id"; try { var keyRes = await descopeClient.Mgmt.V1.Managementkey.GetAsync(config => { config.QueryParameters.Id = id; }); } catch (DescopeException ex) { // Handle the error } ``` ### Create Management Key You can also use our [Create Management Key API](/api/management/management-keys/create-management-key) to create a Management Key. This operation creates a new Management Key. A name and role configuration (`reBac`) are required; description, expiration (`expiresIn` in seconds; `0` for no expiration), and permitted IPs are optional. Roles can be set at the company, project, or tag level and cannot be changed after creation. The response includes the key details and the cleartext secret — store the secret securely, as it is only returned once. ```javascript // Args: // name (str): Required name for the management key. const name = "my-key-name"; // reBac (MgmtKeyReBac): Role-based access control configuration for the key. const reBac = { companyRoles: ["company-fga-read-write"] }; // description (str): Optional description. const description = "Optional description"; // expiresIn (number): Optional expiration time in seconds (0 for no expiration). const expiresIn = 3600; // permittedIps (List[str]): Optional list of IP addresses or CIDR ranges that are allowed to use this key. const permittedIps = ["10.0.0.1/24"]; const resp = await descopeClient.management.managementKey.create( name, description, expiresIn, permittedIps, reBac, ); if (!resp.ok) { console.log("Failed to create management key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created management key.") console.log(resp.data.key) console.log("Key secret (save this!): " + resp.data.cleartext) } ``` ```python # Args: # name (str): Required name for the management key. # rebac (MgmtKeyReBac): Role-based access control configuration for the key. # description (str): Optional description. # expires_in (int): Optional expiration time in seconds (0 for no expiration). # permitted_ips (List[str]): Optional list of IP addresses or CIDR ranges that are allowed to use this key. try: resp = descope_client.mgmt.management_key.create( name="my-key-name", rebac=MgmtKeyReBac(company_roles=["company-fga-read-write"]), description="Optional description", expires_in=3600, permitted_ips=["10.0.0.1/24"], ) print ("Successfully created management key.") print(json.dumps(resp["key"], indent=2)) print ("Key secret (save this!): " + resp["cleartext"]) except AuthException as error: print ("Unable to create management key.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (string): Required name for the management key. name := "my-key-name" // description (string): Optional description. description := "Optional description" // expiresIn (uint64): Optional expiration time in seconds (0 for no expiration). expiresIn := uint64(3600) // permittedIPs ([]string): Optional list of IP addresses or CIDR ranges that are allowed to use this key. permittedIPs := []string{"10.0.0.1/24"} // reBac (*descope.MgmtKeyReBac): Role-based access control configuration for the key. reBac := &descope.MgmtKeyReBac{CompanyRoles: []string{"company-fga-read-write"}} key, cleartext, err := descopeClient.Management.ManagementKey().Create( ctx, name, description, expiresIn, permittedIPs, reBac, ) if (err != nil){ fmt.Println("Unable to create management key: ", err) } else { fmt.Println("Successfully created management key.") fmt.Println(key) fmt.Println("Key secret (save this!): ", cleartext) } ``` ```csharp // Args: // name (string): Required name for the management key. var name = "my-key-name"; // reBac (ManagementKeyReBac): Role-based access control configuration for the key. var reBac = new ManagementKeyReBac { CompanyRoles = new List { "company-fga-read-write" }, }; // description (string): Optional description. var description = "Optional description"; // expiresIn (string?): Optional expiration time in seconds ("0" for no expiration). var expiresIn = "3600"; // permittedIps (List?): Optional list of IP addresses or CIDR ranges that are allowed to use this key. var permittedIps = new List { "10.0.0.1/24" }; var createRequest = new CreateManagementKeyRequest { Name = name, ReBac = reBac, Description = description, ExpiresIn = expiresIn, PermittedIps = permittedIps, }; try { var keyRes = await descopeClient.Mgmt.V1.Managementkey.PutAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update Management Key You can also use our [Update Management Key API](/api/management/management-keys/update-management-key) to update a Management Key. This operation updates an existing Management Key's name, description, status (`active` or `inactive`), and permitted IPs. All provided fields override the current values, and fields will be reset if not provided. Role and project associations cannot be changed after creation — deactivate or delete the key and create a new one if those need to change. ```javascript // Args: // id (str): The Management Key ID. const id = "key-id"; // name (str): The updated name for the management key. const name = "updated-key-name"; // description (str): The updated description for the management key. const description = "Updated description"; // status (MgmtKeyStatus): The status of the management key ('active' or 'inactive'). const status = "active"; // permittedIps (List[str]): Optional list of IP addresses or CIDR ranges that are allowed to use this key. const permittedIps = ["1.2.3.4"]; const resp = await descopeClient.management.managementKey.update( id, name, description, status, permittedIps, ); if (!resp.ok) { console.log("Failed to update management key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated management key.") console.log(resp.data) } ``` ```python # Args: # id (str): The Management Key ID. # name (str): The updated name for the management key. # description (str): The updated description for the management key. # permitted_ips (List[str]): List of IP addresses or CIDR ranges that are allowed to use this key. # status (MgmtKeyStatus): The status of the management key (MgmtKeyStatus.ACTIVE or MgmtKeyStatus.INACTIVE). try: resp = descope_client.mgmt.management_key.update( id="key-id", name="updated-key-name", description="Updated description", permitted_ips=["1.2.3.4"], status=MgmtKeyStatus.ACTIVE, ) print ("Successfully updated management key.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update management key.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (string): The Management Key ID. id := "key-id" // name (string): The updated name for the management key. name := "updated-key-name" // description (string): The updated description for the management key. description := "Updated description" // permittedIPs ([]string): Optional list of IP addresses or CIDR ranges that are allowed to use this key. permittedIPs := []string{"1.2.3.4"} // status (descope.MgmtKeyStatus): The status of the management key (descope.MgmtKeyActive or descope.MgmtKeyInactive). status := descope.MgmtKeyActive key, err := descopeClient.Management.ManagementKey().Update( ctx, id, name, description, permittedIPs, status, ) if (err != nil){ fmt.Println("Unable to update management key: ", err) } else { fmt.Println("Successfully updated management key.") fmt.Println(key) } ``` ```csharp // Args: // id (string): The Management Key ID. var id = "key-id"; // name (string): The updated name for the management key. var name = "updated-key-name"; // description (string): The updated description for the management key. var description = "Updated description"; // status (string): The status of the management key ("active" or "inactive"). var status = "active"; // permittedIps (List?): Optional list of IP addresses or CIDR ranges that are allowed to use this key. var permittedIps = new List { "1.2.3.4" }; var updateRequest = new UpdateManagementKeyRequest { Id = id, Name = name, Description = description, Status = status, PermittedIps = permittedIps, }; try { var keyRes = await descopeClient.Mgmt.V1.Managementkey.PatchAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Management Key(s) You can also use our [Delete Management Keys API](/api/management/management-keys/delete-management-keys) to delete a Management Key. This operation deletes one or more existing Management Keys by ID. This action is irreversible — deleted keys are removed and can no longer be used or reactivated. ```javascript // Args: // ids (List[str]): The IDs of the Management Keys to delete. const ids = ["key-id-1", "key-id-2"]; const resp = await descopeClient.management.managementKey.delete(ids); if (!resp.ok) { console.log("Failed to delete management keys.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted management keys.") } ``` ```python # Args: # ids (List[str]): The IDs of the Management Keys to delete. try: resp = descope_client.mgmt.management_key.delete(["key-id-1", "key-id-2"]) print ("Successfully deleted management keys.") print ("Total deleted: " + str(resp["total"])) except AuthException as error: print ("Unable to delete management keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // ids ([]string): The IDs of the Management Keys to delete. ids := []string{"key-id-1", "key-id-2"} total, err := descopeClient.Management.ManagementKey().Delete(ctx, ids) if (err != nil){ fmt.Println("Unable to delete management keys: ", err) } else { fmt.Println("Successfully deleted management keys.") fmt.Println("Total deleted: ", total) } ``` ```csharp // Args: // ids (List): The IDs of the Management Keys to delete. var ids = new List { "key-id-1", "key-id-2" }; try { var keyRes = await descopeClient.Mgmt.V1.Managementkey.DeletePath.PostAsync( new DeleteManagementKeysRequest { Ids = ids } ); } catch (DescopeException ex) { // Handle the error } ``` # Flows (/management/flows) Learn about Managing Descope Flows within your project # Managing Descope Flows This page is an overview of the helpful items that you may need when working with Descope flows. Details about keyboard shortcuts, importing and exporting flows, creating custom flows, and deleting flows will be covered here. ## Flow Shortcuts The details of available keyboard shortcuts within the flow editor are covered here. ### Group Select To group select items within the flow editor, hold `shift` and drag your cursor to capture the items you'd like. You can then move the flow items, or delete them by pressing the `delete` button. ### Undo Action To undo an action within the flow editor, press `ctrl/cmd + z`. ### Redo Action To redo an action within the flow editor, press `ctrl/cmd + shift + z`. ### Reset Changes To revert all changes within the flow editor, press `ctrl/cmd + shift + l`. ### Delete Action To delete a node within the descope flow, click on it, then press the `delete` key. ### Save Changes To save changes to a flow, press `ctrl/cmd + s`. ### Copy Flow Screens and Actions Between Flows Descope allows you to copy and paste actions, screens, etc between flows. You can select an item or [group select](/management/flows#group-select) multiple items via `ctrl/cmd + c` within one flow and then `ctrl/cmd + v` within another flow on another tab or window. It is also possible to copy and paste components within your screens by selecting an item and using `ctrl/cmd + c` within one screen and `ctrl/cmd + v` within another screen in the same or another tab. ## Creating Custom Flows Descope allows you to create custom flows to meet your needs. You can create flows within the [Descope console](https://app.descope.com/flows) by clicking the `+ Flow` button at the top right. You will then give the flow a name, and an ID. The ID will be used when referencing the flow from your frontend code. ## Duplicating Flows You can duplicate existing flows within the [Descope console](https://app.descope.com/flows) by clicking the three dots on the right side of a flow, then selecting `Duplicate`. This creates a copy of the flow with identical screens, actions, and configurations. When you duplicate a flow, **event triggers are automatically disabled** in the new flow. This is a safety measure to prevent unintended automatic executions of the new flow. If your flow uses event triggers (such as ones in [Management Flows](/flows/management-flows) that are triggered by system events), you will need to manually re-enable them after duplication. ## Flow Deletion Descope allows you to delete flows from your project. This can be done by selecting the flow(s) within the [Descope console](https://app.descope.com/flows) by the checkboxes on the left then the `delete` button at the top of the table, or by clicking the three dots on the right then selecting `delete`. ## Disabling and Activating Flows When you are not actively using a Flow, you can disable it from the Console. This can be done by selecting the flow(s) within the [Descope console](https://app.descope.com/flows) by the checkboxes on the left, then the **Disable** button at the top of the table, or by clicking the three dots on the right selecting **Disable**. The same process applies when you want to re-enable the flows, but select **Activate**. ## Showing Flow Code Within the [Flows page](https://app.descope.com/flows) of the Descope Console, you can generate backend and frontend code snippets by clicking the three dots at the right of the flow then selecting `Show Code`. You can select between various frontend frameworks and backend SDKs to integrate the selected flow into your app. ![flow show code](/assets/show-code-flows.webp) ## Exporting and Importing Flows Descope allows you to export and import flows between projects. This feature allows you to backup your current flows, or migrate them between your projects. ### Export Flow(s) from the Console Within the [Descope console](https://app.descope.com/flows) you can export a single flow by selecting the checkboxes on the left then selecting `export` at the top right of the table, or by clicking the three dots on the right selecting `export`. The same can also be done for multiple flows when you select multiple checkboxes before clicking export. This will export the flows within a zip file. You can also export a flow when you are in the flow editor by clicking the down arrow on the top right. ### Import Flow(s) from the Console Within the [Descope console](https://app.descope.com/flows) you can import a single flow by selecting the `import` button at the top left and selecting the flow's json file to import. You can also import multiple flows by using a zip file containing the flows you would like to upload. Do note that if the IDs are the same as the flows currently in the system, the existing flows will be overridden. You can also import a flow when you are in the flow editor by clicking the up arrow on the top right. # Flows with SDKs (/management/flows/with-sdks) Learn about Managing Descope Flows with backend SDKs # Descope Flows with SDKs You can use the Descope management SDKs for common flow management operations like list/search, delete, import, and export. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). ### List Flows You can also use our [List/Search Flows API](/api/management/flows/list-flows) to list or search Flows in your project. This operation lists flows in the project. Optionally filter by flow IDs, or send an empty request body to list all flows. ```javascript // Args: // None const resp = await descopeClient.management.flow.list() if (!resp.ok) { console.log("Failed to list flows.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully listed flows.") console.log(resp.data) } ``` ```python # Args: # None try: resp = descope_client.mgmt.flow.list_flows() print("Successfully listed flows.") print(resp) except AuthException as error: print("Failed to list flows.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() res, err := descopeClient.Management.Flow().ListFlows(ctx) if err != nil { fmt.Println("Failed to list flows: ", err) } else { fmt.Println("Successfully listed flows.") fmt.Println(res.Total) for _, f := range res.Flows { fmt.Printf("ID: %s, Name: %s\n", f.ID, f.Name) } } ``` ```java // Args: // None FlowService fs = descopeClient.getManagementServices().getFlowService(); try { FlowsResponse resp = fs.listFlows(); for (FlowMetadata f : resp.getFlows()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # ids (Array): Optional list of flow IDs to filter by. Omit or pass [] to list all flows. ids = [] # To filter by IDs: # ids = %w[flow-1 flow-2] begin resp = descope_client.list_or_search_flows(ids) puts 'Successfully listed flows.' puts resp rescue Descope::AuthException => e puts "Failed to list flows. Error: #{e.message}" end ``` ```csharp // Args: // request (SearchFlowsRequest): Optional filter. // Ids (List?): Optional list of flow IDs to filter by. Omit or leave empty to list all flows. var request = new SearchFlowsRequest(); // To filter by IDs: // var request = new SearchFlowsRequest { Ids = new List { "flow-1", "flow-2" } }; try { var response = await descopeClient.Mgmt.V1.Flow.List.PostAsync(request); foreach (var flow in response!.Flows!) { // Do something } } catch (DescopeException ex) { // Handle the error } ``` ### Delete Flows This operation deletes one or more flows by ID. This action is irreversible. ```javascript // Args: // flowIds (List[str]): The flow IDs to delete. const flowIds = ["flow-1", "flow-2"]; const resp = await descopeClient.management.flow.delete(flowIds) if (!resp.ok) { console.log("Failed to delete flows.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted flows.") } ``` ```python # Args: # flow_ids (List[str]): The flow IDs to delete. try: descope_client.mgmt.flow.delete_flows(flow_ids=["flow-1", "flow-2"]) print("Successfully deleted flows.") except AuthException as error: print("Failed to delete flows.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() // flowIDs ([]string): The flow IDs to delete. flowIDs := []string{"flow-1", "flow-2"} err := descopeClient.Management.Flow().DeleteFlows(ctx, flowIDs) if err != nil { fmt.Println("Failed to delete flows: ", err) } else { fmt.Println("Successfully deleted flows.") } ``` ```java // Args: // flowIds (List): The flow IDs to delete. List flowIds = Arrays.asList("flow-1", "flow-2"); FlowService fs = descopeClient.getManagementServices().getFlowService(); try { fs.deleteFlows(flowIds); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // request (DeleteFlowsRequest): Request containing the flow IDs to delete. // Ids (List): The flow IDs to delete. var request = new DeleteFlowsRequest { Ids = new List { "flow-1", "flow-2" } }; try { await descopeClient.Mgmt.V1.Flow.DeletePath.PostAsync(request); } catch (DescopeException ex) { // Handle the error } ``` ### Export Flow You can also use our [Export Flow API](/api/management/flows/export-flow) to export a Flow. This operation exports a flow (and its screens) by flow ID. ```javascript // Args: // flowId (str): The flow ID to export. const flowId = "sign-up-or-in"; const resp = await descopeClient.management.flow.export(flowId) if (!resp.ok) { console.log("Failed to export flow.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully exported flow.") console.log(resp.data) } ``` ```python # Args: # flow_id (str): The flow ID to export. try: resp = descope_client.mgmt.flow.export_flow(flow_id="sign-up-or-in") print("Successfully exported flow.") print(resp) except AuthException as error: print("Failed to export flow.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() // flowID (str): The flow ID to export. flowID := "sign-up-or-in" res, err := descopeClient.Management.Flow().ExportFlow(ctx, flowID) if err != nil { fmt.Println("Failed to export flow: ", err) } else { fmt.Println("Successfully exported flow.") fmt.Println(res) } ``` ```java // Args: // flowID (String): The flow ID to export. String flowID = "sign-up-or-in"; FlowService fs = descopeClient.getManagementServices().getFlowService(); try { FlowResponse resp = fs.exportFlow(flowID); Flow flow = resp.getFlow(); List screens = resp.getScreens(); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # flow_id (str): The flow ID to export. flow_id = 'sign-up-or-in' begin resp = descope_client.export_flow(flow_id) puts 'Successfully exported flow.' puts resp rescue Descope::AuthException => e puts "Failed to export flow. Error: #{e.message}" end ``` ```csharp // Args: // request (ExportFlowRequest): Request containing the flow ID to export. // FlowId (string): The flow ID to export. var request = new ExportFlowRequest { FlowId = "sign-up-or-in" }; try { var response = await descopeClient.Mgmt.V2.Flow.Export.PostAsync(request); var exportedFlow = response!.Flow; } catch (DescopeException ex) { // Handle the error } ``` ### Import Flow You can also use our [Import Flow API](/api/management/flows/import-flow) to import a Flow. This operation imports a flow. This overrides the existing flow for the given ID. ```javascript // Args: // flowId (str): The flow ID to import as. // flow (object): The flow definition to import. // screens (list): Optional screens to import with the flow. const flowId = "sign-up-or-in"; const flow = { name: "Sign Up or In", description: "Sign up or in flow", disabled: false, }; const screens = []; const resp = await descopeClient.management.flow.import(flowId, flow, screens) if (!resp.ok) { console.log("Failed to import flow.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully imported flow.") console.log(resp.data) } ``` ```python # Args: # flow_id (str): The flow ID to import as. # flow (dict): The flow definition to import. # screens (list): Optional screens to import with the flow. try: resp = descope_client.mgmt.flow.import_flow( flow_id="sign-up-or-in", flow={ "name": "Sign Up or In", "description": "Sign up or in flow", "disabled": False, }, screens=[], ) print("Successfully imported flow.") print(resp) except AuthException as error: print("Failed to import flow.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for cancellation and deadlines. ctx := context.Background() // flowID (str): The flow ID to import as. Overrides whatever is set in the flow data. flowID := "sign-up-or-in" // flow (map[string]any): The flow definition to import. flow := map[string]any{ "name": "Sign Up or In", "description": "Sign up or in flow", "disabled": false, } err := descopeClient.Management.Flow().ImportFlow(ctx, flowID, flow) if err != nil { fmt.Println("Failed to import flow: ", err) } else { fmt.Println("Successfully imported flow.") } ``` ```java // Args: // flowID (String): The flow ID to import as. // flow (Flow): The flow definition to import. // screens (List): Optional screens to import with the flow. String flowID = "sign-up-or-in"; Flow flow = Flow.builder() .name("Sign Up or In") .description("Sign up or in flow") .disabled(false) .build(); List screens = Arrays.asList(); FlowService fs = descopeClient.getManagementServices().getFlowService(); try { FlowResponse resp = fs.importFlow(flowID, flow, screens); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # flow_id (str): The flow ID to import as. # flow (Hash): The flow definition to import. # screens (Array): Optional screens to import with the flow. flow_id = 'sign-up-or-in' flow = { 'name' => 'Sign Up or In', 'description' => 'Sign up or in flow', 'disabled' => false, } screens = [] begin resp = descope_client.import_flow( flow_id: flow_id, flow: flow, screens: screens, ) puts 'Successfully imported flow.' puts resp rescue Descope::AuthException => e puts "Failed to import flow. Error: #{e.message}" end ``` ```csharp // Args: // request (ImportFlowRequest): Request containing the exported flow to import. // Flow (ExportedFlow): Typically the Flow object returned from V2 export. // Prefer exporting first, then importing the returned ExportedFlow: var exportRequest = new ExportFlowRequest { FlowId = "sign-up-or-in" }; try { var exported = await descopeClient.Mgmt.V2.Flow.Export.PostAsync(exportRequest); var importRequest = new ImportFlowRequest { Flow = exported!.Flow }; await descopeClient.Mgmt.V2.Flow.Import.PostAsync(importRequest); } catch (DescopeException ex) { // Handle the error } ``` # Access Keys (/management/m2m-access-keys) Learn how to easily implement access key management and authorization for your app with Descope. # Access Keys Access keys enable machine-to-machine authentication for your application. The access keys in Descope behave similarly to users. When users sign in to your application using your application front-end, a JWT token is delivered to the browser. In contrast, for machine-to-machine communication, the machine connecting to your application presents an access key, and a JWT token is returned to the connecting machine. Your application backend can validate the session token as covered in the [session management](/sessions/validation) article, and handle access to resources accordingly. Here's a diagram illustrating how this authentication works: ![Access Key Authentication Diagram](/assets/access-key-auth-diagram.webp) ## Creating an Access Key Head over to the [Access Keys Tab](https://app.descope.com/accessKeys) in the Descope Console, and click on the **+ Access Key** button in the top right corner to create a new access key. You can define the following fields: - **Name (Mandatory)**: The name for your access key - **Description**: The description for your access key - **Expiration**: Used to calculate the key's expiry time. When you exchange the access key for a JWT, the JWT will be valid until the expiry time. You can choose from preset durations (30 days, 60 days, 90 days, 6 months, 1 year, 2 years), or create a key that does not expire automatically. - **Permitted IPs**: If permitted IPs are defined, the access key will only be able to be exchanged for a JWT from IPs in this list of addresses. - **Authorization**: Tenants and/or Roles associated with the Access Key. The Tenants and Roles associations work like how they do for users. ![Create New Access Key](/assets/create-new-access-key.webp) You can also create access keys using our [Management SDK function](/management/m2m-access-keys/sdks#create-access-key). ## Importing Access Keys Please contact [Descope Support](/support) to enable this feature for your project. You can batch import access keys using the import API endpoint. This is useful when you need to migrate existing access keys from another system. To import access keys, make a POST request to `/v1/mgmt/accesskey/import` with a valid management key and the access keys you want to import: ```bash curl --url "${DESCOPE_BASE_URL}/v1/mgmt/accesskey/import" \ --header "Authorization: Bearer ${DESCOPE_PROJECT_ID}:${DESCOPE_MANAGEMENT_KEY}" \ --header "content-type: application/json" \ --data '{ "keys": [ { "name": "My Key", "cleartext": "secretsecretsecret" } ] }' ``` ### Import Requirements - **Cleartext value**: Must be at least 16 characters long - **No duplicates**: Each cleartext value must be unique within your project - **Additional fields**: Apart from `cleartext`, each key object in the request supports the same additional fields as the regular [`/accesskey/create` API](/api/management/access-keys/create-access-key) **Example Response:** ```json { "keys": [ { "id": "K2xiyI1S9jrOGRpp2h36KgIontcK", "name": "My Key", "roleNames": [], "keyTenants": [], "status": "active", "createdTime": 1759926589, "expireTime": 0, "createdBy": "K32Q210klm4NTldDxTgzFr0nt7Zs", "clientId": "UDJ4aXlJMlBWR05WMzZMb2NhbFRlcnJhZm9ybTpLMnhpeUkxUzlqck9HUnBwMmgzNktnSW9udGNL", "boundUserId": "", "customClaims": {}, "description": "", "permittedIps": [] } ] } ``` Once an access key has been imported, you can exchange it for a JWT in the same way you would with a [regularly created access key](/management/m2m-access-keys#exchanging-access-keys-for-jwts). Use the imported key’s cleartext value as the access key when submitting the exchange request. ## Access Key Lifecycle Access keys will continue to function as long as they are active and not expired. Once the access key is expired or deactivated, it will no longer be usable. Within the UI, you can deactivate (revoke) access keys; however, the access key will remain in the Descope project and may be reactivated if you choose to reactivate them. You can also delete access keys. Once an access key is deleted, it will no longer be usable. Access keys set with the **Never** expiration option do not expire automatically and remain active until they are manually deactivated or deleted. ## Associating Access Key to Users Access keys can be created or deleted within the Descope console. While generating the key, you need to provide the name, expiration, tenants, and roles associated with it. Keys created this way, or via the [Create Access Key API](/api/management/access-keys/create-access-key), are **not** bound to a user by default since `boundUserId` is empty. Additionally, you can set the user of the access key with the [management SDK](/management/m2m-access-keys/sdks#create-access-key) or via the user access key management [widget](/widgets/admins#access-key-management-widget). Keys generated through the widget are automatically bound to the user who created them (or, for tenant admins, the user they generated the key on behalf of); see [Configuring Access Keys Widget Usage](/widgets/admins#configuring-access-keys-widget-usage) for how to control who can create keys through it. The rest of the options to edit, delete, or deactivate are provided within the UI. ## Custom Attributes You can define custom attributes to store additional metadata on your access keys, separate from [custom claims](/management/m2m-access-keys#custom-claims). While custom claims are only added to the JWT at exchange time, custom attributes are stored on the access key itself — so they can be searched on, and optionally surfaced in a JWT via a [JWT Template](/management/token/jwt-templates#access-key-jwt-templates). You can define new custom attributes directly in the **Create Access Key** or in the **Edit Access Key** dialog in the [Access Keys Tab](https://app.descope.com/accessKeys) of the Descope console. Custom attributes can be of the following types: - **Text** - Store text-based information - **Numeric** - Store numeric values - **Boolean** - Store true/false values - **Single select** - Choose one option from a list - **Multi select** - Choose multiple options from a list (utilized as an array) - **Date** - Store date values - **Month-Day** - Store the values in the MM-DD format Once defined, you can set custom attribute values via the Console or the [Create Access Key](/api/management/access-keys/create-access-key), [Update Access Key](/api/management/access-keys/update-access-key), and [Import Access Keys](/api/management/access-keys/import-access-keys) APIs, and filter them by using the [Search Access Keys](/api/management/access-keys/search-access-keys) API — see [Search Access Keys](/management/m2m-access-keys/sdks#search-access-keys) for SDK examples. ## Exchanging Access Keys for JWTs If you would prefer to use client credentials flow to exchange an access key for a JWT, we recommend using a [Federated Application](/getting-started/oidc-endpoints#client-credentials-flow) with your access key as a client secret. If you are using Node, Python, Go, or Java SDKs, you can use the [Exchange Access Key function](/management/m2m-access-keys/sdks#exchange-access-keys) to exchange an access key for a JWT token. This JWT token is what will be used with the rest of your application, to validate that you are authenticated. If you are not using any of the SDKs, you can also exchange your access key for a JWT using our [Exchange Key API endpoint](/api/access-keys/exchange-key). ### Custom Claims When exchanging an access key for a JWT, you can include custom claims—such as a user ID—on the token. This can be done in two ways: 1. Include custom claims in the request - Any claims added directly by the M2M client are placed inside the `nsec` claim of the JWT. **Example:** ```json { "exp": 1692304651, "iat": 1692304051, "iss": "P2RFvFexVaxxNFK6rhP0ePtaGfTK", "nsec": { "email": "example@email.com", "name": "Joe Person" } ... } ``` 2. Use a [JWT Template](/management/token/jwt-templates#access-key-jwt-templates) - Claims defined in a template are inserted directly into the JWT payload. Unlike request-based claims, these do **not** appear under the `nsec` field, since they are preconfigured and verified through the Descope Console or management API. If a JWT Template's custom claims reference a `user.*` dynamic value - for example `{{user.userId}}` - that value only resolves for access keys that have a bound user. See [Associating Access Key to Users](#associating-access-key-to-users) above for how to bind one, either explicitly or by having users generate their own keys through the [Access Key Management Widget](/widgets/admins#access-key-management-widget). ## Changing Authorization after Access Key Creation You can change the authorization for an access key even after it's been created. There's no need to generate a new key if you simply want to update what the key is allowed to do. This is useful when the purpose or required access level for a service changes. For example, if a key was originally created with limited permissions, you can later adjust its authorization to match updated requirements. To change the authorization for an access key: - In the Descope Console, navigate to the [Access Keys Tab](https://app.descope.com/accessKeys) - Find the Access Key you want to update - Click the three-dot menu (`:`) and select **Edit** - Update the **Authorization** field to the desired tenants and/or roles - Click **Save** # Access Keys with SDKs (/management/m2m-access-keys/sdks) Learn how to easily implement access key management and authorization for your app via backend SDKs with Descope. # Access Keys with SDKs If you wish to learn more about Access Keys in general, visit our section on [Access Key Management](/management/m2m-access-keys). You can use Descope Management SDK for common access key management operations like create access key, update custom claims, delete access key, etc. ### Create Access Key This operation is used to create an access key. At the time of creation of the access key, you can provide the name, expiration duration, tenants, and roles associated with the key. After the successful creation of the key, the response object contains id of the key and key value. The key value is only delivered at the creation time from Descope service, and your application must store or deliver to the connecting machine based on your use case. Your application can use the key id for updating the name, deleting, etc. An access key must have a name and expiration, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. ```javascript // Args: // name (str): Access key name. const name = "xxxx" // expireTime (int): Access key expiration. Leave at 0 to make it indefinite. const expireTime = 0 // roles (List[str]): An optional list of the access key's roles without tenant association. These roles are mutually exclusive with the `key_tenant` roles, which take precedence over them. const roles = ["TestRole1"] // userId (str): An optional user id to associate to the access key. If the user is disabled or deleted - it will affect the access key accordingly. const userId = 'yyyy' // keyTenants (List[AssociatedTenant[]]): An optional list of the access key's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. const keyTenants = [{tenantId: 'TestTenant'}] // CustomClaims Record: An optional record of custom attributes to add on the top level of the jwt for the access key. const customClaims = {'key':'value'} // Create with associated roles rather than tenants // const resp = await descopeClient.management.accessKey.create(name, expireTime, roles, userId, null, customClaims) // Create with associated tenants rather than roles const resp = await descopeClient.management.accessKey.create(name, expireTime, null, userId, keyTenants, customClaims) if (!resp.ok) { console.log("Failed to create access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created access key.") console.log(resp.data) } ``` ```python # Args: # name (str): Access key name. name = "xxxx" # expire_time (int): Access key expiration. Leave at 0 to make it indefinite. expire_time = 0 # role_names (List[str]): An optional list of the access key's roles without tenant association. These roles are mutually exclusive with the `key_tenant` roles, which take precedence over them. role_names = ["TestRole"] # user_id (str): An optional user id to associate to the access key. If the user is disabled or deleted - it will affect the access key accordingly. user_id = "yyyy" # key_tenants (List[AssociatedTenant]): An optional list of the access key's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. key_tenants = [AssociatedTenant("TestTenant")] #custom_claims (Dict): An optional dictionary of custom attributes to add on the top level of the jwt for the access key. custom_claims = {"key":"value"} try: # Create with associated roles rather than tenants # resp = descope_client.mgmt.access_key.create(name=name, expire_time=expire_time, role_names=role_names, user_id=user_id, custom_claims=custom_claims) # Create with associated tenants rather than roles resp = descope_client.mgmt.access_key.create(name=name, expire_time=expire_time, key_tenants=key_tenants, user_id=user_id, custom_claims=custom_claims) print("Successfully created access key.") print("Access Key info. Save the returned cleartext securely, it will not be returned again.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to create access key.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): Access key name. name := "xxxx" // expireTime (int64): Access key expiration. Leave at 0 to make it indefinite. var expireTime int64 = 0 // userId (str): An optional user id to associate to the access key. If the user is disabled or deleted - it will affect the access key accordingly. const userId := "yyyy" // roles (List[str]): An optional list of the access key's roles without tenant association. These roles are mutually exclusive with the `key_tenant` roles, which take precedence over them. roles := []string{"TestRole1"} // keyTenants (List[AssociatedTenant]): An optional list of the access key's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. keyTenants := []*descope.AssociatedTenant{{TenantID: "TestTenant"}} // customClaims map[string] : An optional dictionary of custom attributes to add on the top level of the jwt for the access key. customClaims := map[string]any{"key": "value"} // Create with associated roles rather than tenants // cleartext, res, err := descopeClient.Management.AccessKey().Create(ctx, name, expireTime, roles, userId, nil, customClaims) // Create with associated tenants rather than roles cleartext, res, err := descopeClient.Management.AccessKey().Create(ctx, name, expireTime, userId, nil, keyTenants, customClaims) if (err != nil){ fmt.Println("Unable to create access key: ", err) } else { fmt.Println("Successfully created access key: ") fmt.Println("Access Key Created with ID: ", res.ID) fmt.Println("Cleartext:", cleartext) } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); try { // custom claims map initialization Map customClaims = new HashMap() { { put("key", "value"); } }; // Create a new access key with a name, delay time, and tenant AccessKeyResponse resp = aks.create("access-key-1", 0, Arrays.asList("Role names"), Arrays.asList( new Tenant("tenant-ID1", "Key Tenant", Arrays.asList(new AssociatedTenant("tenant-ID2", Arrays.asList("Role names"))))), "userId", // add custom claims here if needed customClaims); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // keyName (string): Access key name. var keyName = "my-access-key"; // expireTime (string?): Access key expiration. Leave at 0 to make it indefinite. string? expireTime = null; // roleNames (List?): An optional list of the access key's roles without tenant association. These roles are mutually exclusive with the `key_tenant` roles, which take precedence over them. List? roleNames = new List { "Backend" }; // keyTenants (List?): An optional list of the access key's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. List? keyTenants = new List { new AssociatedTenant { TenantId = "tenant-id1", RoleNames = new List { "Role1" } } }; // bindUserId (string?): An optional user id to associate to the access key. string? bindUserId = null; var createRequest = new CreateAccessKeyRequest { Name = keyName, ExpireTime = expireTime, KeyTenants = keyTenants, UserId = bindUserId, var createRequest = new CreateAccessKeyRequest { Name = keyName, ExpireTime = expireTime, RoleNames = roleNames, KeyTenants = keyTenants, UserId = bindUserId, }; try { var keyRes = await descopeClient.Mgmt.V1.Accesskey.Create.PostAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Load Access Key The Descope SDK allows administrators to use a management key to load details of an existing access key. ```javascript // Args: // id (str): The id of the access key to be loaded. const id = "xxxx" const resp = await descopeClient.management.accessKey.load(id); if (!resp.ok) { console.log("Failed to load access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded access key.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the access key to be loaded. try: resp = descope_client.mgmt.access_key.load(id="xxxx") print("Successfully loaded access key.") print("Access Key info:") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to load access key.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the access key to be loaded. id := "xxxx" res, err := descopeClient.Management.AccessKey().Load(ctx, id) if (err != nil){ fmt.Println("Unable to load access key: ", err) } else { fmt.Println("Successfully loaded access key: ", res) } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Load specific user try { AccessKeyResponse resp = aks.load("access-key-1"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKeyID (string): The ID of the access key to load. var accessKeyID = "access-key-id"; try { var keyRes = await descopeClient.Mgmt.V1.Accesskey.GetWithIdAsync(accessKeyID); } catch (DescopeException ex) { // Handle the error } ``` ### Search Access Keys Search for existing access keys using a management key, optionally filtering by tenant, bound user, creating user, or custom attributes. #### Search filter parameters | Parameter | Type | Match behavior | Description | |---|---|---|---| | `tenantIds` | array of strings | exact match, any | Filter to keys belonging to one or more tenants | | `text` | string | generic text search | Free-text search across key metadata | | `boundUserId` | string | exact match | Filter to keys bound to a specific user ID | | `creatingUser` | string | partial, case-insensitive | Filter by the user who created the key — not the same as `boundUserId`; this reflects who created the key, not who it's scoped to | | `customAttributes` | object | exact match per attribute; multiple values for one attribute are OR'd, multiple attributes are AND'd | Filter by custom attribute values, e.g. `{ "membership_id": "abc123" }`. A `null` value for an attribute matches keys where that attribute is unset | ```javascript // Args: // tenantIds (List[str]): Optional list of tenant IDs to filter by // boundUserId (str): Optional - filter by the user ID a key is scoped to (exact match) // creatingUser (str): Optional - filter by the user who created the key (partial, case-insensitive match) // customAttributes (object): Optional - filter by custom attribute values, e.g. { "membership_id": "abc123" } const tenantIds = ["TestTenant"] // Search all access keys: // const resp = await descopeClient.management.accessKey.searchAll(null) // Search keys based on tenantIds // const resp = await descopeClient.management.accessKey.searchAll(tenantIds) // Search keys based on the bound user, creating user, and/or custom attributes const resp = await descopeClient.management.accessKey.searchAll( tenantIds, "U2Vxxx", // boundUserId - the target user's Descope ID "jane@example.com", // creatingUser - matches against the creator's email/identifier { membership_id: "abc123" }, // customAttributes ) if (!resp.ok) { console.log("Failed to search access keys.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully searched access keys.") console.log(resp.data) } ``` ```python # Args: # tenant_ids (List[str]): Optional list of tenant IDs to filter by # bound_user_id (str): Optional - filter by the user ID a key is scoped to (exact match) # creating_user (str): Optional - filter by the user who created the key (partial, case-insensitive match) # custom_attributes (dict): Optional - filter by custom attribute values, e.g. {"membership_id": "abc123"} tenant_ids = ["TestTenant"] try: # Search all access keys: # resp = descope_client.mgmt.access_key.search_all_access_keys()) # Search keys based on tenant_ids # resp = descope_client.mgmt.access_key.search_all_access_keys(tenant_ids=tenant_ids) # Search keys based on the bound user, creating user, and/or custom attributes resp = descope_client.mgmt.access_key.search_all_access_keys( tenant_ids=tenant_ids, bound_user_id="U2Vxxx", # the target user's Descope ID creating_user="jane@example.com", # matches against the creator's email/identifier custom_attributes={"membership_id": "abc123"}, ) print("Successfully searched access keys.") print("Access Key info:") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to search access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` Starting in go-sdk `v1.7.0`, `AccessKey().SearchAll` no longer accepts a `tenantIDs []string` argument directly. Update to the new `AccessKeysSearchOptions` struct shown below when upgrading. ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // Search all access keys: // res, err := descopeClient.Management.AccessKey().SearchAll(ctx, nil) // Search keys based on tenantIDs, boundUserID, creatingUser, and/or customAttributes res, err := descopeClient.Management.AccessKey().SearchAll(ctx, &descope.AccessKeysSearchOptions{ TenantIDs: []string{"TestTenant"}, BoundUserID: "U2Vxxx", // the target user's Descope ID CreatingUser: "jane@example.com", // matches against the creator's email/identifier CustomAttributes: map[string]any{"membership_id": "abc123"}, }) if (err != nil){ fmt.Println("Unable to search access keys: ", err) } else { fmt.Println("Successfully searched access key:") for _, u := range res { fmt.Println(u) } } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Search all access keys, optionally filtered by tenant try { AccessKeyResponseList resp = aks.searchAll(Arrays.asList("Tenant IDs")); for (AccessKeyResponse r : aks.getKeys()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // tenantIds (List?): Optional list of tenant IDs to filter by. Pass null or an empty list for an unfiltered search. List? tenantIds = new List { "tenant-id1", "tenant-id2" }; try { // Search all access keys: // var searchRequest = new SearchAccessKeysRequest(); // var keyRes = await descopeClient.Mgmt.V1.Accesskey.Search.PostAsync(searchRequest); // Search keys based on tenantIds var searchRequest = new SearchAccessKeysRequest { TenantIds = tenantIds, }; var keyRes = await descopeClient.Mgmt.V1.Accesskey.Search.PostAsync(searchRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update Access Key The Descope SDK allows administrators to use a management key to update the name of an existing access key. It is important to note that all parameters are used as overrides to the existing access key; empty fields will override populated fields. ```javascript // Args: // id (str): The id of the access key to update. const id = "xxxx" // name (str): The updated access key name. const name = "xxxx" const resp = await descopeClient.management.accessKey.update(id, name); if (!resp.ok) { console.log("Failed to update access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated access key.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the access key to update. # name (str): The updated access key name. try: resp = descope_client.mgmt.access_key.update(id="xxxx", name="xxxx") print("Successfully updated access key.") except AuthException as error: print ("Unable to update access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the access key to update. id := "xxxx" // name (str): The updated access key name. name := "xxxx" _, err := descopeClient.Management.AccessKey().Update(ctx, id, name) if (err != nil){ fmt.Println("Unable to update access key: ", err) } else { fmt.Println("Successfully updated access key.") } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Update will override all fields as is. Use carefully. try { AccessKeyResponse resp = aks.update("access-key-1", "updated-name"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKeyID (string): The ID of the access key to update. var accessKeyID = "access-key-id"; // newName (string): The new name to assign to the key. var newName = "updated-key-name"; try { var updateRequest = new UpdateAccessKeyRequest { Id = accessKeyID, Name = newName, }; var keyRes = await descopeClient.Mgmt.V1.Accesskey.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Activate Access Key The Descope SDK allows administrators to use a management key to activate an existing access key that is currently deactivated. ```javascript // Args: // id (str): The id of the access key to be activated. const id = "xxxx" const resp = await descopeClient.management.accessKey.activate(id); if (!resp.ok) { console.log("Failed to activate access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully activated access key.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the access key to be activated. try: resp = descope_client.mgmt.access_key.activate(id="xxxx") print("Successfully activated access key.") except AuthException as error: print ("Unable to activate access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the access key to be activated. id := "xxxx" err := descopeClient.Management.AccessKey().Activate(ctx, id) if (err != nil){ fmt.Println("Unable to activate access key: ", err) } else { fmt.Println("Successfully activated access key.") } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Access keys can be deactivated to prevent usage. This can be undone using "activate". try { AccessKeyResponse resp = aks.activate("access-key-1"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKeyID (string): The ID of the access key to activate. var accessKeyID = "access-key-id"; try { await descopeClient.Mgmt.V1.Accesskey.Activate.PostAsync(new AccessKeyRequest { Id = accessKeyID }); } catch (DescopeException ex) { // Handle the error } ``` ### Deactivate Access Key The Descope SDK allows administrators to use a management key to deactivate an existing access key. After deactivating an access key, it will no longer be usable. The key will persist within the project, and can be activated again if needed. ```javascript // Args: // id (str): The id of the access key to be deactivated. const id = "xxxx" const resp = await descopeClient.management.accessKey.deactivate(id); if (!resp.ok) { console.log("Failed to deactivate access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deactivated access key.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the access key to be deactivated. try: resp = descope_client.mgmt.access_key.deactivate(id="xxxx") print("Successfully deactivated access key.") except AuthException as error: print ("Unable to deactivate access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the access key to be deactivated. id := "xxxx" err := descopeClient.Management.AccessKey().Deactivate(ctx, id) if (err != nil){ fmt.Println("Unable to deactivate access key: ", err) } else { fmt.Println("Successfully deactivated access key.") } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Disabled access keys can be activated once again. try { AccessKeyResponse resp = aks.deactivate("access-key-1"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKeyID (string): The ID of the access key to deactivate. var accessKeyID = "access-key-id"; try { await descopeClient.Mgmt.V1.Accesskey.Deactivate.PostAsync(new AccessKeyRequest { Id = accessKeyID }); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Access Key The Descope SDK allows administrators to use a management key to delete an existing access key. Once an access key is deleted, it is removed from the project and no longer usable. This action is irreversible. ```javascript // Args: // id (str): The id of the access key to be deleted. const id = "xxxx" const resp = await descopeClient.management.accessKey.delete(id); if (!resp.ok) { console.log("Failed to delete access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted access key.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the access key to be deleted. try: resp = descope_client.mgmt.access_key.delete(id="xxxx") print("Successfully deleted access key.") except AuthException as error: print ("Unable to delete access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the access key to be deleted. id := "xxxx" err := descopeClient.Management.AccessKey().Delete(ctx, id) if (err != nil){ fmt.Println("Unable to delete access key: ", err) } else { fmt.Println("Successfully deleted access key.") } ``` ```java // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AccessKeyService aks = descopeClient.getManagementServices().getAccessKeyService(); // Access key deletion cannot be undone. Use carefully. try { aks.delete("access-key-1"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKeyID (string): The ID of the access key to delete (irreversible). var accessKeyID = "access-key-id"; try { await descopeClient.Mgmt.V1.Accesskey.DeletePath.PostAsync(new AccessKeyRequest { Id = accessKeyID }); } catch (DescopeException ex) { // Handle the error } ``` ### Exchange Access Keys You can also provide this method with a custom claims object, that will nest the custom claims inside the `nsec` key. If you use a [JWT template](/management/token/jwt-templates#access-key-jwt-templates) they will be flattened into the JWT payload. Read more in our [Access Keys Overview Doc](/management/m2m-access-keys#custom-claims). The Descope SDK allows for exchanging access keys for a JWT token. For machine-to-machine communication, a machine (M2M client) presents an access key, and a JWT token is returned to the client. The client can then use this JWT token to make API calls to your M2M server or external application. ```javascript // Args: // accessKey (str): The access key const accessKey = "xxxx" // loginOptions: Optional advanced controls over login parameters, e.g. custom claims: const loginOptions = {customClaims: {"key":"value"}} try { const authInfo = await descopeClient.exchangeAccessKey(accessKey, loginOptions); console.log(`Exchanged access key for JWT: ${authInfo.jwt}`); } catch (err) { console.log(`Failed to exchange access key: ${err}`); } ``` ```python # Args: # access_key (str): The access key # login_options: Optional advanced controls over login parameters, e.g. custom claims: try: resp = descope_client.exchange_access_key(access_key="xxxxxx", login_options={"custom_claims": {"key":"value"}}) print("Successfully exchanged access key. Below is the session info.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to exchange access keys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // accessKey (str): The access key accessKey := "xxxxxx" // loginOptions (AccessKeyLoginOptions): Optional advanced controls over login parameters, e.g. custom claims: loginOptions := &descope.AccessKeyLoginOptions{ CustomClaims: map[string]any{"k1": "v1"} } authenticated, token, err := descopeClient.Auth.ExchangeAccessKey(ctx, accessKey, loginOptions) if (err != nil){ fmt.Println("Unable to exchange access keys: ", err) } else { fmt.Println("Successfully exchanged access key. Below is the session info.") fmt.Println("Authenticated: ", authenticated) fmt.Println("Token: ", token) } ``` ```java // Args: // accessKey (str): The access key accessKey := "xxxxxx" // accessKeyLoginOptions: Optional advanced controls over login parameters, e.g. custom claims: Map loginOptions = {"customClaims":{"key":"value"}} // Roles should be set directly if no tenants exist, otherwise set // on a per-tenant basis. AuthenticationServices authService = AuthenticationServiceBuilder.buildServices(client); aks = authService.getAuthService(); try { AuthenticationInfo info = aks.exchangeAccessKey(accessKey, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // accessKey (string): The access key cleartext to exchange. var accessKey = "xxxxxx"; // loginOptions (AccessKeyLoginOptions?): Optional access key login options. var loginOptions = new AccessKeyLoginOptions { CustomClaims = new AccessKeyLoginOptions_customClaims { AdditionalData = new Dictionary { { "k1", "v1" } } } }; try { var token = await descopeClient.Auth.ExchangeAccessKey(accessKey, loginOptions); } catch (DescopeException ex) { // Handle the error } ``` # External Token (/management/project-settings/external-token) Generate a Firebase, Supabase, or custom token at the end of a Descope flow alongside Descope session tokens. # External Token Descope's **External Token** feature supports hybrid authentication: run authentication in Descope Flows, and still return a token in your existing format (Firebase, Supabase, or custom) for backends and services that already validate that format. At the end of a flow, a configured [External Token connector](/connectors/connector-configuration-guides/token) generates the token. It is returned in the authentication response as `externalToken`, alongside Descope's session and refresh JWTs. ![External token swimlane diagram](/assets/external-token.webp) This is **not** the same as [External Token Management](/identity-federation/inbound-apps/using-inbound-apps#external-token-management) on Inbound Apps, which validates an incoming third-party JWT and exchanges it for Descope tokens. ## Setup This section describes how to set up External Token for a project. ### Configuring a External Token Connector To use the External Token feature, you need to configure an External Token connector in the [Descope Console](https://app.descope.com/connectors). The three supported connectors are: - **Firebase** — Firebase-compatible tokens - **Supabase** — Supabase-compatible tokens - **Generic HTTP Token** — Custom format via your own API See the [External Token connector guides](/connectors/connector-configuration-guides/token) for configuration details for each connector. ### Enabling External Token for a Project Configure a connector, then select it under Session Management so all flows within your project will return `externalToken` in the authentication response. 1. Open [Session Management](https://app.descope.com/settings/project/session) in Project Settings. 2. In the **External Token** section, select the connector from Step 1. ![External token connector selection in project settings](/assets/byot-token-enable.webp) That Session Management selection applies to every flow when it finishes. You do not need to configure each flow separately for the connector to run. If one flow should use a different connector than the rest of the project (for example a second Firebase project), set **External Token Connector** on that flow's [End](/flows/actions/end-action#external-token-connector) action. Flows that leave the End field blank keep using this Session Management selection. ## Flow Authentication Response When a flow completes with an External Token connector selected (via Session Management, or via that flow's End action), the authentication response includes `externalToken`: ```json { "cookieDomain": "", "cookieExpiration": 0, "cookieMaxAge": 0, "cookiePath": "/", "externalToken": "EXTERNAL_TOKEN", "firstSeen": false, "idpResponse": null, "refreshJwt": "DESCOPE_REFRESH_TOKEN", "sessionExpiration": 1750879215, "sessionJwt": "DESCOPE_SESSION_TOKEN", "user": {} } ``` `externalToken` is unset / `null` when no connector is enabled for the project or that flow. ## How to Use External Token You can use External Token in your web or mobile application by following the steps below: ### Web ```jsx import { Descope } from '@descope/react-sdk' { const externalToken = e.detail.externalToken if (externalToken) { // pass to the service that expects this token format } }} /> ``` ```jsx import { Descope } from '@descope/nextjs-sdk' { const externalToken = e.detail.externalToken if (externalToken) { // pass to the service that expects this token format } }} /> ``` ```javascript const descopeWcEle = document.getElementsByTagName('descope-wc')[0] descopeWcEle.addEventListener('success', (e) => { const externalToken = e.detail.externalToken if (externalToken) { // pass to the service that expects this token format } }) ``` ```vue ``` ```html ``` ```typescript onSuccess(e: CustomEvent) { const externalToken = e.detail.externalToken if (externalToken) { // pass to the service that expects this token format } } ``` ```html ``` ### Mobile For a specific example of using External Token with Firebase, see our blogs for [iOS](https://www.descope.com/blog/post/external-tokens-ios-firebase) and [Android](https://www.descope.com/blog/post/android-firebase-external-token). With mobile SDKs, you can access the `externalToken` in the `onSuccess` callback. ```swift func flowViewControllerDidFinish(_ controller: DescopeFlowViewController, response: AuthenticationResponse) { if let externalToken = response.externalToken { // pass the token to the service that expects it } } ``` ```kotlin override fun onSuccess(response: AuthenticationResponse) { response.externalToken?.let { externalToken -> // pass the token to the service that expects it } } ``` ```dart onSuccess: (AuthenticationResponse response) { final externalToken = response.externalToken; if (externalToken != null) { // pass the token to the service that expects it } }, ``` ```javascript { const externalToken = jwtResponse.externalToken if (externalToken) { // pass the token to the service that expects it } }} /> ``` # Project Settings (/management/project-settings) Learn how to customize your Descope project settings. # Project Settings This guide covers all the customizable settings for your Descope project. Configure these settings on the [Project Settings page](https://app.descope.com/settings/project) of the Descope Console. ## Project Creation When you create a new Descope Project, you can configure the following settings: - **Project Name**: Name of your project. Can be modified later. - **Region**: We support [multi-region data residency](/management/project-settings/multi-regional). The region (US, EU, AP, CA, UK, SG, or SA) is selected during the project creation and cannot be changed after the project has been created. - **Environment Settings**: Choose whether or not the project has the `production` tag. This is meant primarily to be an internal identifier, but [Static OTP for Test Users](/management/project-settings#test-users) will be disabled in `production` tagged projects. You can also add additional custom tags to the project, discussed in the [General](#general) section below. - **Project Configuration**: Choose whether or not to clone the configurations of an existing project in the same company. This will not clone user data. The `production` tag is meant primarily to be an internal identifier, but [Static OTP for Test Users](/management/project-settings#test-users) will be disabled in projects with the tag. Once a Descope Project has been created, you can edit the following settings from the [Project Settings page](https://app.descope.com/settings/project) of the Descope Console. ## General Settings Under the General tab on the [Project Settings page](https://app.descope.com/settings/project), you can configure the following: ### General The General tab is where you can find your project's ID, which is used to identify the project in the API and SDKs. You can also copy a project's ID or name directly from the project selector dropdown at the top-left of the Console—hover over any project in the list and click the copy icon next to it. This is a quick way to grab another project's ID without switching into it first. You can modify the **Project Name** and **Environment Settings** previously set during [Project Creation](#project-creation). You can add custom descriptive tags to projects. These are maintained on a company level, allowing you to use the same tags across different projects in the same company. This can be used to give [granular access](/management/company-settings#descoper-roles) to Descopers based on tags associated with projects in the company. Additionally, you can configure the URL of your application, and configure a [custom domain](/how-to-deploy-to-production/custom-domain). ![General Project Settings](/assets/general-project-settings.webp) ### Security #### Approved Domains Configure domains allowed for redirect and verification URLs across authentication methods. **Leaving this empty disables validation and creates security vulnerabilities.** When `Apply Trusted Domains on Flow Execution` is enabled, our generic Descope hosted domains (e.g., `api.descope.com`) are not whitelisted automatically. To run [Auth Hosting](/identity-federation/auth-hosting), and other applications with flows hosted by Descope, you must include them manually in the Approved Domains section. ![Approved Domains Project Settings](/assets/approved-domains.webp) - **Web Domains**: Enter domain only, without protocol (e.g., `example.com` not `https://example.com`) - **Mobile App Schemes**: For custom schemes like `descopewebauth://redirect`, enter the identifier (e.g., `redirect`) to enable secure mobile authentication flows. All redirect URIs are validated against this trusted domain list to maintain security. Descope validates a redirect URL built from [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls) after it resolves, so list every domain your dynamic values can produce. [Federated App](/identity-federation/applications) callback URLs are restricted according to this approved domains list. Approved [Inbound App](/identity-federation/inbound-apps) callback URLs are defined in the [Inbound App's settings](/identity-federation/inbound-apps/creating-inbound-apps#connection-information). #### Cross-Company Domain Protection Descope automatically validates that authentication flows are not running on domains belonging to other companies. This security feature prevents flows from one project being executed on custom domains configured in different companies, protecting against potential domain hijacking or cross-company misuse. **How it works:** - When a flow starts or continues, Descope validates the requesting domain against your project's trusted domains (standard validation) - Additionally, Descope checks if the domain belongs to another company's project (cross-company validation) - If the domain is found in another company's trusted domains, the flow is blocked with an error **Example scenario:** - Company A has `auth.companyA.com` in their trusted domains - Company B has `auth.companyB.com` in their trusted domains - If Company A's flow attempts to run on `auth.companyB.com`, it will be automatically blocked This validation happens automatically on all flow endpoints (`/v1/flow/start`, `/v1/flow/next`, etc.) and requires no configuration. #### Federated Apps Define the default access to [Federated Apps](/identity-federation/applications) for new users. Under **Default access for new users**, choose one of the following: - **Give access to all applications** (default): new users can access every Federated App as soon as they are created. - **Do not give access to any applications**: new users start with no access. You then have to manually associate users, roles, or tenants with an application before any applicable user can access it. ![Federated Apps Project Settings](/assets/federated-apps-project-settings.webp) #### Signing Keys Here you can view, manage, and [rotate JWKs](/additional-security-features-in-descope/jwk-rotation), keys used for verification of JWTs by the Authorization Server. #### iframe Embedding Check **iframe Embedding** to enable iframe embedding for your project. This removes the security header Descope adds by default so your Descope-hosted flows can load in an iframe on your site. ![iframe Embedding Project Settings](/assets/iframe-embedding.webp) #### Tenant User Isolation Check **Tenant User Isolation** to isolate users per tenant so the same login ID is treated as a separate identity in each tenant, with independent credentials and MFA state. For more information, refer to [Tenant User Isolation](/management/tenant-management#tenant-user-isolation). ![Security Project Settings](/assets/security-project-settings.webp) ### Sign Ups and User Invitations #### Sign Up Check **Block self-registration sign up** to restrict new user sign up. With this setting enabled, users can only sign in if they have been previously [invited to your project](/management/user-management/invite-users) or by using [SSO](/auth-methods/sso) to sign in. #### User Invitations This is where you can define how invitations are sent to users and customize the invitations. - **User Invitation Redirect URL**: This URL is included in the invite email/SMS sent to the end user. It is typically the login or sign-up page of the application. - **Invite Expiration**: When enabled, the invited user account expires if the invitation is not accepted within the configured period. Set the period in hours, days, or weeks. - **Add a Magic Link token to the Invitation Link**: For a smoother authentication experience, add a Magic Link to your user's invitation email/SMS, so that upon clicking - only verification is needed. You can also define the token expiration time here. To handle a Magic Link token in the flow, refer to the [Verify Token guide](/flows/actions/verify-token). - **Invitation Sending Methods**: Choose to send the invitation via Email and/or Text Message (SMS). For both options, you can configure the messaging connector to use, as well as the template. For more details on configuring messaging templates, refer to our [messaging templates guide](/management/messaging-templates). ![Sign Ups and User Invitations Project Settings](/assets/sign-ups-invitation-project-settings.webp) ### Test Users Define a regex pattern to match Login IDs during sign-up. Users with matching Login IDs are automatically marked as test users. Here, you can also configure a Static OTP Code to be used for the test users' verifiers (email and phone). For complete setup instructions and advanced configuration options, see the [Test Users Guide](/test-users#dynamic-test-user-creation). ![Test Users Project Settings](/assets/test-users-project-settings.webp) ### Project Management You can manage the cloning, export, import, and deletion of projects within this section of Project Settings. Typically, environments for development, staging, and production are considered separate projects in Descope. Each project can have its own settings, as well as its own users and permissions. For details, review our dedicated guide on [Managing Environments](/how-to-deploy-to-production/managing-environments). ![Project Management Project Settings](/assets/project-management-project-settings.webp) To delete a specific project: 1. Open the project you want to delete in the Descope Console. 2. Go to **Project Settings → General** for that project. 3. Scroll to the bottom of the page and click **Delete Project**. 4. Confirm the deletion by typing the project name. ## Session Management Under the Session Management tab on the [Project Settings page](https://app.descope.com/settings/project), you can configure the following: Session management can also be overridden at a tenant level. More information about tenant level session management can be found [here](/management/tenant-management/tenant#session-management). ### Token Format Here you can assign the JWT Templates that JWTs generated by this project will use. Both User and Access Key JWT Templates can be assigned here. Refer to the [JWT Templates Settings documentation](#jwt-templates) to learn how to configure JWT Templates. ![Token Format Project Settings](/assets/token-format-project-settings.webp) ### Token Expiration Define default expiration times for different tokens used for authentication. #### Refresh Token Timeout Expiry time for the refresh token, after which the user must log in again. You can override the default refresh duration during a flow using the `Custom Claims` action. #### Refresh Token Rotation Configuring Refresh Token Rotation is a pro+ tier feature. When enabled, every time the user refreshes their session token (using the refresh token) - the refresh token is also updated to a new one. This method is considered a more secure approach. To learn more, check out our [Refresh Token Rotation guide](/additional-security-features-in-descope/refresh-token-rotation). #### Session Token Timeout Expiry time of the session token, used for accessing the application's resources. Value needs to be at least 3 minutes and can't be longer than the Refresh Token Timeout. #### Step Up Token Timeout Expiry time for the Step Up token, after which the step up token will not be valid, and the user will automatically go back to the Session token. #### Trusted Device Token Timeout Expiry time of the trusted device token. Value needs to be at least 3 minutes. #### Access Key Session Token Timeout Expiry time of the access key session token. Value needs to be at least 3 minutes and can't be longer than 1 month. ![Token Expiration Project Settings](/assets/token-expiration-project-settings.webp) ### Session Inactivity Session inactivity detects idle sessions and closes them on behalf of the user to protect sensitive information. An idle session occurs when there is no user activity within the system for the specified duration. When session inactivity is enabled, client SDKs must periodically refresh the session to prevent it from expiring while the user is still active. To support this efficiently, when the app returns to the foreground and on resume, the SDK refreshes the session if it expired. If the refresh token has expired while the app was in the background, the user will be required to authenticate again. ![Session Inactivity Project Settings](/assets/session-inactivity-project-settings.webp) When session inactivity is enabled, the refresh heartbeat (a periodic background refresh that keeps an active session alive) is scheduled at 10% of the configured session inactivity timeout. When session inactivity is disabled (the default behavior), the refresh is instead based on the length of the session timeout. ### Token Response Methods Configure how tokens are managed by the Descope SDKs. For both the refresh and session token, you can choose to **Manage in response body** or **Manage in cookies**. Refer to our [Session Token Management Guide](/security-best-practices/session-token-storage#session-token-management) for best practices. ![Token Response Methods Project Settings](/assets/token-response-methods-project-settings.webp) ### Session Migration (Beta) Enable seamless migration of existing user sessions from another vendor to Descope. Refer to our [Session Migration doc](/migrate/session-migration) for more details. ## JWT Templates Under the JWT Templates tab on the [Project Settings page](https://app.descope.com/settings/project), you can configure JWT Templates for User and Access Tokens. Refer to our [JWT Templates doc](/management/token/jwt-templates) for more details. ## Lists A List is a project-level object containing a reusable collection of values such as IP addresses, CIDR ranges, phone numbers, email domains, or countries that you can reference directly in flow conditions. **Supported List Types** Descope applies specific matching logic based on the list type: - **Text**: Exact string matching for items like email domains or usernames - **IPs/CIDR**: Matches exact IPs or checks if a user IP falls within a CIDR range Navigate to **Lists** in the [Project Settings page](https://app.descope.com/settings/project/lists) to create and manage lists. Lists can be updated via flow actions, the Console, or managed using CI/CD tools like Terraform. For a practical implementation example, see our guide on [Blocking Users by IP Address](/flows/use-cases/block-users-by-ip-address). # Multi-Region Support (/management/project-settings/multi-regional) Learn how to use Descope with multi-regional support, including Descope base URLs, regional API hosts, and custom domains. # Multi-Region Support Descope allows you to create projects across multiple regions. When creating a Descope Project, user data and all project configurations will only be stored and maintained within the region that the project resides in. Once a region has been selected during project creation, all user and tenant data cannot be moved between regions. ## Currently Supported Regions When on a Descope Growth or Enterprise plan, you can select which region to store the data when creating a new project. Descope currently supports data residency in the following regions: | Region | Location | | :----- | :------- | | US (United States) | Northern Virginia | | EU (European Union) | Frankfurt, Germany | | AP (Asia Pacific) | Sydney, Australia | | CA (Canada) | Montreal | | UK (United Kingdom) | London | | SG (Singapore) | Singapore | | SA (South America) | Brazil | If you are interested in a specific region's data residency and on a Free Forever plan, reach out to [Descope support](mailto:support@descope.com) ### Data Centers For EU users, Descope is compliant with all GDPR regulations Descope leverages AWS data centers across the supported regions for the secure storage and processing of project and user information. Our data practices strictly adhere to all major regulatory requirements, ensuring confidentiality, integrity, and compliance at all times. ## Descope Base URLs A **Descope base URL** is the scheme and host used for API, OAuth, and Flow traffic—for example `https://api.descope.com` or `https://auth.yourcompany.com`. It does not include a path. ### When SDKs Resolve the Host Automatically For standard Descope projects on our public regional hosts, Descope SDKs derive the correct regional API base URL from your [Project ID](/management/project-settings#general). You typically do not need to set `baseUrl` just to hit the right regional endpoint. ### When You Must Set the Base URL Explicitly The base URL **does** matter, and you **must** configure it in your app, when: - You use a **[custom domain](/how-to-deploy-to-production/custom-domain)** for your project (your own hostname replaces the default `api.*.descope.com` host for clients and flows), or - You use a [Private Cloud Descope environment](/how-to-deploy-to-production/private-cloud) (i.e. a "star" environment). In those cases the SDK **cannot** infer the correct host from the Project ID alone. Set **`baseUrl`** (and where applicable **`baseStaticUrl`**, per the [client SDK](/client-sdk/descope-components#base-url-configuration) or your stack’s equivalent) to your custom or environment-specific hostname. Use the hostname from Customer Success, or derive the scheme and host from the OpenID Connect **discovery URL** on the default [Federated application](https://app.descope.com/applications) in the Console. ## Federated Application Issuer and Discovery URLs When configuring an Federated application in the console, issuer and discovery URLs follow your environment's base URL (regional defaults, custom domain, or private cloud). They use the same hosts as described above and can be reviewed in your application [settings](https://app.descope.com/applications). If your project is on a [private cloud](/how-to-deploy-to-production/private-cloud) environment (e.g. `star4`), the SDK cannot auto-detect the base URL. You must pass `baseUrl` explicitly when initializing the SDK: `baseUrl="https://api..descope.app"`. Replace `` with your environment name (e.g. `https://api.star4.descope.app`). #### Descope APIs If you're unsure about the correct base URL to use, you can look at the base URL set for your federated application settings. ## Default Regional API Hosts For projects **without** a custom domain, each region has a fixed API host. Use this host for manual API requests and for understanding where traffic is routed. | Region | API base URL | | :----- | :------- | | US | `https://api.descope.com` | | EU | `https://api.euc1.descope.com` | | AP | `https://api.aps2.descope.com` | | CA | `https://api.cac1.descope.com` | | UK | `https://api.euw2.descope.com` | | SG | `https://api.aps1.descope.com` | | SA | `https://api.sae1.descope.com` | When using Descope Services with our APIs directly, all requests must use the `baseURL` that matches your project's region and deployment type (public regional host, custom domain, or private cloud). For regional **CNAME targets** used when configuring a custom domain in DNS, see [Create a DNS Record](/how-to-deploy-to-production/custom-domain#create-a-dns-record) in the Custom Domain guide. # Project Dashboards (/management/project-settings/project-dashboard) Learn how to use the Descope Project Dashboards to monitor user activity, tenant metrics, operations & security, and flow analytics. # Project Dashboards Descope provides various project dashboards for you to see metrics on your users, tenants, operations, and flows. This data enables you to make informed decisions on how to best leverage Descope. ## User Activity Dashboard The user activity dashboard summarizes the activity of the end users of your Descope project. Here you can track metrics like number of active users over time, authentication methods used, and conversion rates. You can choose to view metrics over either the past week or the past year, including numbers on daily/monthly active users and new users. You can also see a graph of user activity over the time period selected: ![active users over time](/assets/weekly-user-activity.webp) You can also see the following user activity metrics: - **Flows User Conversion**: User conversion is the ratio of completed flows resulting in an authenticated user to incomplete flows. - **Countries**: The distribution of countries from which your users are communicating with your service. - **Authentication Methods**: The distribution of authentication methods with which your users have chosen to authenticate with your service. - **Devices**: The distribution of device types which your users are using when communicating with your service. ![active users metrics](/assets/user-activity.webp) ## Tenant Activity Dashboard The tenant activity dashboard summarizes the activity of the tenants of your Descope project. You can choose to view metrics over either the past week or the past year, including numbers on daily/monthly active tenants and new tenants. You can also see the following metrics: - **Active Tenants Over Time**: The number of active (2 or more MAUs) tenants over time. - **Significant Activity Drops**: Tenants that have suffered a significant drop in user activity, and the percentage of the drop compared to the previous time frame. - **Regular vs SSO Tenants**: The number of tenants with SSO enabled and without. ![tenant activity metrics](/assets/tenant-activity.webp) ## Operations & Security Dashboard The operations & security dashboard provides insights into the security and operational aspects of your Descope project. You can see the following metrics: - **Top IP Addresses**: The IP addresses with the highest number of authentication requests, helping you identify unusual traffic patterns or potential security threats. - **Top Countries**: The countries from which the most authentication requests originate, providing geographic insights into your user base. - **Referers**: The referring sources (URLs or domains) that direct users to your authentication flows, helping you understand how users are accessing your service. - **Email Domains**: The distribution of email domains used by your users during authentication, which can help you identify organizational patterns or potential issues. - **Country Codes**: Here you will be able to see the country codes for phone numbers with which your users are communicating with your service. ![security and operations dashboard](/assets/security-operations-dashboard.webp) ## Flow Analytics Dashboard The flow analytics dashboard allows you to select one of your flows and see detailed metrics on it. This data is based on the current version of the flow, and resets when the flow is modified. - **User visit analytics on your current flow version**: How many users visited each step of the flow. ![analytics of current flow](/assets/flow-analytics.webp) ## Agentic Activity Dashboard The agentic activity dashboard allows you to see the activity of the agents of your Descope project. You can see the following metrics: - **MCP Server Registrations**: Shows how agents authenticate when connecting to your [MCP servers](/agentic-identity-hub/core-components/mcp-servers). Methods include: manual client credentials (pre-configured client ID/secret within an MCP Server), Dynamic Client Registration (DCR), and Client ID Metadata (CIMD) for streamlined OAuth flows. - **Active Connections by Token**: This widget lists the top 5 most commonly used [Connections](/agentic-identity-hub/core-components/connections) by token count. - **Top Policy Violations by MCP server**: [Policy violations](/policies#policy-violations-and-audit-events) from agents connecting to your MCP server will appear here when a client requests Resource scopes that no [Policy](/policies) permits. - **Top Policy Violations by Connection**: [Policy violations](/policies#policy-violations-and-audit-events) within Connections will appear here when a client or Resource requests Connection scopes that no policy permits. - **Policy Violations**: All [policy violation](/policies#policy-violations-and-audit-events) audit events detected in your project. See [Audit Events → Policy violations](/audit-trails-and-integrations/audit-events#policy-violations) for the event names and fields logged for each denial. ![agentic activity dashboard](/assets/agentic-activity-dashboard.webp) # Project Versioning (/management/project-settings/project-versioning) Learn how to use the Descope Project Versioning to manage your project versions using project tags. # Project Versioning In Descope, you can manage project versioning using project tags. Tags help you label and track different versions of your project, especially when working across multiple environments like development, staging, and production. They make it easier to see which version is deployed and maintain a clean version history. This guide explains how project tags work and how you can use them to manage versioning across your Descope projects. ## Using Project Tags for Versioning Project tags are simple labels that you attach to a project to help you keep track of its versions. They make it easy to see which version is running in each environment, and understand when configurations or authentication flows have changed. For example, you can use tags like `version-2` or `branch-feat-add-new-flows` to label and identify specific releases or flow updates. Tags also make troubleshooting easier by helping you identify when you need to roll back to an earlier version, and they let you quickly compare environments to ensure they're staying aligned. When you [export your project](/how-to-deploy-to-production/managing-environments#exporting-projects), you can add `projectTags` field to the `project.json` file. This field is an array of strings, and you can define multiple project tags as strings. ![project tags in project.json](/assets/project-json-tags.webp) When you [import the project](/how-to-deploy-to-production/managing-environments#importing-projects) with any tags defined in the `projectTags` field, the new project tags will override any existing tags and be displayed in the [Project page](https://app.descope.com/settings/project) of the Descope Console. ![project version tags](/assets/project-version-tags.webp) # Refresh Token Storage (/management/project-settings/refresh-token-storage) Learn how to properly store refresh tokens with Descope project setting configurations. # Refresh Token Storage When configuring your project settings, it's important to understand the differences in how refresh tokens can be stored with Descope, and what to do to ensure they are stored properly. The [refresh token expiry time](https://app.descope.com/settings/project) should be decided based on the requirements of your application (tradeoff between higher security and user experience). A shorter expiration time means that the user will need to authenticate frequently. If you are using Client SDK (including Flows), Descope manages the refresh token storage for your application client. Depending on the [project configuration](https://app.descope.com/settings/project), the Descope service can return the refresh token in two different ways - "manage in cookies" and "manage in response body". ## Manage in Cookies (Recommended for Production) Descope Client SDK automatically sets the refresh token as a cookie. For this option to work, you must also configure a [custom domain](/how-to-deploy-to-production/custom-domain) record in your DNS and the custom domain setting in Descope console, which will securely restrict access to the stored refresh token. **_Strict Cookies:_** Descope utilizes strict cookies; this means that after successful authentication, Descope's response will set `SameSite=Strict` within the header of the cookie. The browser will only be able to send cookies to hosts with that custom domain (specified in the project settings) and subdomains of the set custom domain. You should set the custom domain setting to something like `app.example.com`, and the CNAME record in your DNS for `auth.app.example.com` (subdomain of custom domain) should point to `cname.descope.com` (US) / `CNAME.euc1.descope.com` (EU). This implementation will set the refresh token as a cookie on `auth.app.example.com`. See the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide for a step by step guide for managing sessions within cookies. ### Sample Custom Domain Setting ![Descope custom domain example](/assets/descope-custom-domain-example.webp) ### Sample CNAME Setting ![Descope custom domain example](/assets/descope-custom-cname-example.webp) ## Manage in Response Body Descope Client SDK returns the refresh token in the body and stores it in browser local storage. This option does not require configuring a custom domain. You can chose to handle the refresh token as per your needs in your application client. # Projects with SDKs (/management/project-settings/sdks) Learn how to easily implement project management with Descope using the Descope backend SDKs. # Projects with SDKs You can use the Descope management SDK for common project management operations like cloning project, exporting project, importing project, etc. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope client, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) management_key = "xxxx" try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key) except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Update Project Name This endpoint allows you to update the current project's name. ```javascript // Args // name (str): The new name for the project. const name = "New Project Name" const resp = await descopeClient.management.project.updateName(name) if (!resp.ok) { console.log(resp) console.log("Failed to update project name.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated project name.") } ``` ```python # Args # name (str): The new name for the project. name = "New Project Name" try: resp = descope_client.mgmt.project.update_name(name=name) print ("Successfully updated project name.") except AuthException as error: print ("Failed to update project name.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): The new name for the project. name := "New Project Name" err := descopeClient.Management.Project().UpdateName(name, nil) if err != nil { fmt.Print("Failed to update project name.", err) } else { fmt.Println("Successfully updated project name.") } ``` ```java ProjectService ps = descopeClient.getManagementServices().getProjectService(); try { ps.updateName("New Project Name"); } catch (DescopeException de) { // Handle the error } ``` ### Update Project Tags This endpoint allows you to update the tags associated with the current project. Tags are free-text labels you can use to categorize projects (e.g. `production`, `team-a`). ```python # Args # tags (List[str]): Array of free-text tags to set on the project. tags = ["production", "team-a"] try: descope_client.mgmt.project.update_tags(tags=tags) print ("Successfully updated project tags.") except AuthException as error: print ("Failed to update project tags.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` The Python SDK also supports renaming a project with `update_name` (see [Update Project Name](#update-project-name)) and duplicating a project with `clone` (see [Clone Project](#clone-project)): ```python # Rename the current project descope_client.mgmt.project.update_name(name="new-project-name") # Clone the current project (Pro license required; users/tenants/access keys are not cloned) clone_resp = descope_client.mgmt.project.clone(name="cloned-project-name") ``` ### Export Project This endpoint allows you to export the current project. ```javascript // Args // None const resp = await descopeClient.management.project.export() if (!resp.ok) { console.log(resp) console.log("Failed to export project.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully exported project.") console.log(resp.data) } ``` ```python # Args # None try: resp = descope_client.mgmt.project.export_project() print ("Successfully exported project") print (resp) except AuthException as error: print ("Failed to export project") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := descopeClient.Management.Project().Export(ctx) if err != nil { fmt.Print("Failed to export project", err) } else { fmt.Println("Successfully exported project", res) } ``` ```java ProjectService ps = descopeClient.getManagementServices().getProjectService(); try { ExportProjectResponse resp = ps.exportProject(); } catch (DescopeException de) { // Handle the error } ``` ### Import Project This endpoint allows you to import all settings and configurations into the current project. Use with caution, this endpoint overrides any current configuration. ```javascript // Args // files (): The raw JSON dictionary of files, in the same format as the one returned by calls to export. const files = { "files": { //exportRes.data.files ... } } const resp = await descopeClient.management.project.import(files) if (!resp.ok) { console.log(resp) console.log("Failed to import project.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully imported project.") } ``` ```python # Args # files (dict): The raw JSON dictionary of files, in the same format as the one returned by calls to export. files = { "files": { ... } } try: resp = descope_client.mgmt.project.import_project(name=name) print ("Successfully imported project") except AuthException as error: print ("Failed to import project") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // req (ImportProjectRequest): The raw JSON dictionary of files, in the same format as the one returned by calls to export. req = := &descope.ImportProjectRequest{} req.Files map[string]any{ "files": { ... } } err := descopeClient.Management.Project().Import(req) if err != nil { fmt.Print("Failed to import project", err) } else { fmt.Println("Successfully imported project") } ``` ```java ProjectService ps = descopeClient.getManagementServices().getProjectService(); try { ps.importProject({ "files": { ... } }); } catch (DescopeException de) { // Handle the error } ``` ### Clone Project This function allows you to clone the current project, including its settings and configurations. Users, tenants and access keys are not cloned. ```javascript // Args // name (str): The new name for the project. const name = "New Project" // tag(str): Optional tag for the project. Currently, only the "production" tag is supported. const resp = await descopeClient.management.project.clone(name, null) if (!resp.ok) { console.log(resp) console.log("Failed to clone project.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully cloned project.") console.log(resp.data) } ``` ```python # Args # name (str): The new name for the project. name = "New Project" # tag(str): Optional tag for the project. Currently, only the "production" tag is supported. try: resp = descope_client.mgmt.project.clone(name=name) print ("Successfully cloned project") print (resp) except AuthException as error: print ("Failed to clone project") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // name (str): The new name for the project. name := "New Project" // tag(str): Optional tag for the project. Currently, only the "production" tag is supported. res, err := descopeClient.Management.Project().Clone(ctx, name, nil) if err != nil { fmt.Print("Failed to clone project", err) } else { fmt.Println("Successfully cloned project", res) } ``` ```java ProjectService ps = descopeClient.getManagementServices().getProjectService(); // Clone the current project to a new one // Note that this action is supported only with a pro license or above. try { NewProjectResponse resp = ps.clone("New Project Name", ProjectTag.None); } catch (DescopeException de) { // Handle the error } ``` # Advanced Styling Examples (/management/styles/advanced-styling-examples) This guide will cover common advanced styling options. # Advanced Styling Examples The names of most fields inside Code Mode are self-explanatory, and those fields can be tested right away in the flow builder. Some best practices and odd features are documented here. For the full set of available `globals` and `components` keys, see the [Globals Reference](/management/styles/code-mode/globals-reference) and [Components Reference](/management/styles/code-mode/components-reference). ## Components - **Badge**: Is the styling of the component of the options you get inside a multi-select field: ![Code Mode badge component explanation](/assets/screen-body-component-styles.webp) - **Logo**: Contains the values of the URL's for the company logo that will be used, including the fallback URL: ```json "logo": { "--descope-logo-fallback-url": "url(https://imgs.descope.com/components/no-logo-placeholder.svg)", "--descope-logo-url": "https://static.descope.com/pages/__ProjectID__/v2-alpha/light/logo.png" } ``` - **TOTP Image**: Holds the placeholder image for TOTP button ```json "totpImage": { "--descope-totp-image-fallback-url": "url(https://imgs.descope.com/components/totp-placeholder.svg)" } ``` - **Required Attribute**: If a component supports using the required boolean, the `_required` attribute will appear in code mode, stating which component will it use to indicate it: ```json "_required": { "--descope-upload-file-required-indicator": "var(--descope-input-wrapper-required-indicator)" } ``` - **Notification Card**: can alter the style of the notification toast you and other Descopers will get, also how different types of modes will be displayed, such as errors and success messages for actions performed in Descope: ```json "mode": { "error": { "--descope-notification-card-background-color": "var(--descope-colors-error-main)", "--descope-notification-card-border-color": "var(--descope-colors-error-light)", "--descope-notification-card-text-color": "var(--descope-colors-error-contrast)" }, "primary": { "--descope-notification-card-background-color": "var(--descope-colors-primary-main)", "--descope-notification-card-border-color": "var(--descope-colors-primary-light)", "--descope-notification-card-text-color": "var(--descope-colors-primary-contrast)" }, "success": { "--descope-notification-card-background-color": "var(--descope-colors-success-main)", "--descope-notification-card-border-color": "var(--descope-colors-success-light)", "--descope-notification-card-text-color": "var(--descope-colors-success-contrast)" } }, ``` - **Alert Error Icon**: can add an icon to the Alert component's error mode for example an exclamation point icon to display errors for better accessibility purpose in Descope: ```json { "components": { "alert": { "mode": { "error": { "--descope-alert-icon": "url(data:image/svg+xml;base64,PHN2ZyBmaWxsPSIjZTIxZDEyIiB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIA0KCSB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAzMC4zMzQgMzAuMzM0Ig0KCSB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxwYXRoIGQ9Ik0xNS4xNjcsMEM2LjgwNSwwLDAuMDAxLDYuODA0LDAuMDAxLDE1LjE2N2MwLDguMzYyLDYuODA0LDE1LjE2NywxNS4xNjYsMTUuMTY3YzguMzYxLDAsMTUuMTY2LTYuODA1LDE1LjE2Ni0xNS4xNjcNCgkJQzMwLjMzMyw2LjgwNCwyMy41MjgsMCwxNS4xNjcsMHogTTE3LjE2NywyNS42NjdoLTR2LTQuNWg0VjI1LjY2N3ogTTE3LjE2NywxOS41NDJoLTRWNS4xNjdoNFYxOS41NDJ6Ii8+DQo8L2c+DQo8L3N2Zz4=)" } } } } } ``` - **Mandatory Behavior for Inputs**: Descope has a mandatory behavior setting for inputs. When enabled, it adds an asterisk (*) to the placeholder text. You can customize the appearance of the asterisk by editing the required indicator: ```json { "components": { "emailField": { "--descope-email-field-label-required-indicator": "none" } } } ``` - **Special Attributes**: Some components hold styling states for states of the components. Altering the attributes listed below will change how the component will look in different states: ```json { "_checked": { ... }, "_disabled": { ... }, "_invalid": { ... }, "_readonly":{ ... }, "_focused":{ ... }, "_vertical":{ ... }, "_fullWidth":{ }, "_loading":{ ... }, "_square":{ ... }, "_bordered":{ ... }, "_hidden":{ ... }, "_hideCursor":{ ... } // special for text component: "_italic":{ ... }, "_lowercase":{ ... }, "uppercase":{ ... } } ``` # Styling and Theming (/management/styles) Learn how to use CSS code mode within Descope styling and themes # Styles Styles help you create and scale user-facing screens while adhering to your brand's guidelines. You can add your brand's logo, colors, fonts, and other design elements in a central location through the [Styles Page](https://app.descope.com/styles) of the Descope Console. Any screens you create with the [Screen Builder](/flows/screens#screen-builder) will take on the brand elements from your pre-defined styles. ## Creating and Managing Styles To manage styles: 1. Navigate to the [Styles tab](https://app.descope.com/styles) in your Descope Console. 2. Click the dropdown and select **`+ New styles file`** to create a new style. ### Creating a New Style - Give your style a unique name—this automatically generates a corresponding Style ID. - Click **`Add`** to create the new style in your project. ![Create New Styles File](/assets/style-create.webp) You can now customize the style using the **GUI** or **Code Mode**, adjusting any UI component's appearance. Based on your primary and secondary color selections, Descope automatically generates a full color palette. ![Configure New Styles File](/assets/style-configure.webp) ### Deleting a Style If a style is no longer needed, simply delete it using the delete icon next to the file. ![Styles File Delete](/assets/style-file-delete.webp) ### Styles Per Application If you want to customize the styling for the same flow based on the application rendering the flow, you can do that in one of two ways: #### Using the Descope SDKs Please refer to our [Descope SDK documentation](/client-sdk/descope-components#descope-flow-component) for more details on other frameworks. You can pass the `styleId` parameter into the component like this: ```javascript ``` This allows you to use a single flow across multiple branded applications, with a custom style in each. To override only primary and secondary colors at runtime (without a separate style file), use [`themeOverride`](/client-sdk/descope-components#theme-override) on the flow component. #### Using Auth Hosting This is typically relevant for customers using Descope with [Federated Apps](/identity-federation/applications), [Inbound Apps](/identity-federation/inbound-apps), or [MCP Servers](/agentic-identity-hub/core-components/mcp-servers). You can read more about how to configure a custom style using Auth Hosting, on our docs [here](/identity-federation/auth-hosting#hosted-by-descope). ## Within the Console Within the Console, Descope has Styles divided into two tabs. Under the **Theme** tab, you'll see all the global styling options that affect the overall screens and individual components. Under the **Components** tab, you can configure each individual component type in detail. ### Styling Themes Under the **Theme** tab, we have three sections that we can customize according to our brand. Each of these sections are available for both Light and Dark themes. #### Logo You can upload your company's logo under this field with a simple drag and drop. Same goes for the dark theme option as well. ![GUI screen Logo](/assets/theme-logo-styles.webp) #### Favicon This option is only available for SSO Setup Suite themes. You can upload your company's favicon under this field with a simple drag and drop. Same goes for the dark theme option as well. ![GUI screen Favicon](/assets/theme-favicon-styles.webp) #### Colors In this section, you can select your brand colors to match brand consistency. You have options of designing Primary, Secondary, Greys, Success, Warning, and Error color palettes. When you select a primary color for example, Descope will create the rest of the palette for you. ![GUI screen Colors](/assets/colors-styles-theme.webp) #### Typography In this section, you choose primary and secondary font families, then define styling for text-typed components used in Flow screens. Under **Font 1** and **Font 2** (optional), select a font family from the dropdown. You can also add a custom font; see [Custom Fonts](#custom-fonts) below. ![GUI screen Fonts](/assets/styles-typography.webp) Below the font family selectors, you'll see pre-configured text styles: - Heading 1, Heading 2, Heading 3 - Subtitle 1, Subtitle 2 - Body 1, Body 2 These are the stylized text options available in Flow screens—for example, when using the text component. On the left, you see a preview of each style. On the right, you can override any pre-configured setting with your own **Font Family**, **Size (px)**, and **Font Weight**. Font Weight offers nine named increments, each mapped to a standard CSS `font-weight` value: | Label | CSS `font-weight` | |---|---| | Thin | 100 | | Extra Light | 200 | | Light | 300 | | Regular | 400 | | Medium | 500 | | Semi Bold | 600 | | Bold | 700 | | Extra Bold | 800 | | Black | 900 | ##### Custom Fonts Custom fonts must be hosted on a publicly accessible URL to be used in your Descope styles. Each font family allows one custom font setting. To configure a custom font, click `+ Add Font` from the font dropdown, then provide a label name (for example, "My Custom Font") and a URL to a CSS file that contains font file URLs. You can edit the custom font after it has been set. ![GUI custom Fonts](/assets/typography-custom-font.webp) The uploaded file must be a CSS file that defines `@font-face` rules. Each font face should specify the font's name, weight, source files, and any other attributes it needs. The CSS file can include multiple font faces, such as variations of the same font with different weights. The `font-family` value **must** match the custom font name you configured in Descope. Any valid [MDN `@font-face`](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face) declaration is supported. You can use one or multiple CSS files for font face declarations. Shown below is an example `style.css` for a custom font named 'Roboto Mono' with regular and bold+italic styles: ``` @font-face { font-family: 'Roboto Mono', monospace; src: url(fonts/roboto-mono-v12-latin-regular.woff2) format('woff2'), url(fonts/roboto-mono-v12-latin-regular.woff) format('woff'), url(fonts/roboto-mono-v12-latin-regular.ttf) format('truetype'); } @font-face { font-family: 'Roboto Mono', monospace; src: url(fonts/roboto-mono-v12-latin-700italic.woff2) format('woff2'), url(fonts/roboto-mono-v12-latin-700italic.woff) format('woff'), url(fonts/roboto-mono-v12-latin-700italic.ttf) format('truetype'); font-weight: 700; font-style: italic; } ``` ### Styling Components Under the **Components** tab, you can style individual UI components in more detail. Every component has editable options on the right side. ![GUI screen buttons components](/assets/component-styles-fw-update.webp) Many components group their options into a **General** section (options that always apply) and a **By Variant** or **By State** section (options you can override for a specific variant or state—for example, styling only the `Enabled` state of a component, or only the `Primary Text` variant). Select the variant or state you want to customize from its dropdown before editing the fields below it. Once all the changes are made, click **Apply**, then use **Preview** to review your changes. #### Root Container **Style**: - **Background Color**: The fill color of the screen's outer container. - **Border Radius**: The roundness of the container's corners. - **Border**: Toggle whether the container has a border. When enabled, you can set the border's color and width; when disabled (the default), the container has no border. #### Primary Buttons **General**: - **Font Family**: The font used for the button's text. - **Font Weight**: The weight (boldness) of the button's text. - **Border Radius**: The roundness of the button's corners. - **Border Width**: The thickness of the button's border. **By Variant**, configured per **Variant** (e.g. `Contained`) and **State** (e.g. `Enabled`)—select a variant and state, then set: - **Background Color**: The fill color of the button for the selected variant and state. - **Border Color**: The border color of the button for the selected variant and state. - **Text Color**: The text color of the button for the selected variant and state. Secondary Buttons share the same set of style options as Primary Buttons. #### OAuth Buttons You can customize each default OAuth sign-in button independently of one another. As an example, for just the `Apple` OAuth login button, you can override the button styling or leave the defaults: ![GUI screen oauth button components](/assets/oauth-buttons-edit.webp) **By Variant**: - **Providers**: The OAuth provider whose button you want to style (e.g. `Apple`, `Google`, `Facebook`). - **Preset**: Choose a predefined style, or `Custom` to override the button's styling for the selected provider. **General**: - **Font Family**: The font used for the button's text. - **Font Weight**: The weight (boldness) of the button's text. - **Border Radius**: The roundness of the button's corners. - **Border Width**: The thickness of the button's border. **By State** (e.g. `Enabled`)—select a state, then set: - **Logo Color**: The color of the provider's logo icon on the button. - **Background Color**: The fill color of the button for the selected state. - **Border Color**: The border color of the button for the selected state. - **Text Color**: The text color of the button for the selected state. #### Input - **Font Family**: The font used for the input's label and value text. - **Font Weight**: The weight (boldness) of the input's text. - **Border Radius**: The roundness of the input's corners. - **Border Width**: The thickness of the input's border. - **Background Color**: The fill color of the input field. - **Label Color**: The color of the input's label. - **Value Color**: The color of the text the user enters into the input. - **Error Message Font Size**: The font size of the validation error message shown below the input. #### Labels **General** (applies to the component regardless of variant or state): - **Font Family**: The font used for the label's text. - **Border Radius**: The roundness of the label's corners. - **Border Width**: The thickness of the label's border. **By Variant**, configured per **State** (e.g. `Enabled`)—select a state, then set: - **Background Color**: The fill color of the label for the selected state. - **Border Color**: The border color of the label for the selected state. - **Text Color**: The text color of the label for the selected state. #### Radio Group **General**: - **Font Family**: The font used for the option labels. - **Font Weight**: The weight (boldness) of the option labels. - **Label Color**: The color of the text label next to each radio option. - **Option Color**: The color of the radio button's outer circle (the unselected ring). - **Background Radio Color**: The fill color behind the radio button. **By State** (e.g. `Default`): select a state to override any of the options above just for that state. #### Text **By Variant** (e.g. `Primary Text`)—select which text variant you want to style, then set: - **Text Color**: The color of the text for the selected variant. #### Tooltip - **Font Family**: The font used for the tooltip's text. - **Size (px)**: The font size of the tooltip's text, in pixels. - **Font Color**: The color of the tooltip's text. - **Font Weight**: The weight (boldness) of the tooltip's text. - **Border Radius**: The roundness of the tooltip's corners. - **Border Width**: The thickness of the tooltip's border. - **Padding**: The internal spacing between the tooltip's border and its text, set independently for horizontal and vertical spacing. - **Background Color**: The fill color of the tooltip. - **Border Color**: The color of the tooltip's border. - **Shadow**: The size of the drop shadow behind the tooltip. #### Last Used Badge - **Font Family**: The font used for the badge's text. - **Font Weight**: The weight (boldness) of the badge's text. - **Size (px)**: The font size of the badge's text, in pixels. - **Text Color**: The color of the badge's text. - **Border Radius**: The roundness of the badge's corners. - **Border Width**: The thickness of the badge's border. - **Padding**: The internal spacing between the badge's border and its content, set independently for horizontal and vertical spacing. - **Background Color**: The fill color of the badge. - **Border Color**: The color of the badge's border. - **Shadow**: The size of the drop shadow behind the badge. ## Code Mode Styles between light and dark theme will not persist, meaning each theme holds different values. Enable code mode editor with a toggle button. Clicking the button will switch the styles page to code mode: ![Code Mode button Descope](/assets/styles-light-theme-off.webp) Code mode allows you to view or edit the styled theme in a code-first approach. For ease of use, code mode will only show the fields that have been changed from Descope's default style. For example, to edit the **Font Family** in Code Mode, we can first change it to **Sans Serif** in the GUI: ![Code Mode Change To Sans Serif](/assets/styles-view-change-sansserif.webp) The keys pertaining to **Font Family** will now be visible in Code Mode, and can be modified directly from there: ![Code Mode Change Reflected](/assets/styles-cm-changes.webp) For the full set of `globals` and `components` keys available in Code Mode, see the [Globals Reference](/management/styles/code-mode/globals-reference) and [Components Reference](/management/styles/code-mode/components-reference). For specific worked examples, refer to our [Advanced Styling Examples Doc](/management/styles/advanced-styling-examples). ## Managing Themes from the Console Once you have defined your styles, you can export or import the styles using the **Export theme** and **Import theme** buttons at the top right of the styles page. This feature allows you to backup your current styles, or migrate them between your projects. ![Descope screen styling example](/assets/styles-brand-colors.webp) You can also revert back to the default Descope styling with the **Reset to default** button. # Themes with SDKs (/management/styles/managing-themes-sdks) This guide will cover how to manage themes with SDKs. # Themes with SDKs Manage your themes by exporting them for backup or migration, and importing them across different projects. You can do this [through the Descope Console](/management/styles#managing-themes-from-the-console) or programmatically using the Management SDK. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```xml // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Import and Initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope client you can configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) management_key = "xxxx" try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key) except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Export Theme Use the code below to export a theme: ```javascript import * as fs from 'fs'; // Args // None const resp = await descopeClient.management.theme.export() if (!resp.ok) { console.log(resp) console.log("Unable to export theme.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully exported theme.") console.log(resp.data) let data = JSON.stringify(resp.data, null, 2); fs.writeFile('theme.json', data, (err) => { if (err) throw err; console.log('Theme written to file'); }); } ``` ```python import json # Args # None try: resp = descope_client.mgmt.flow.export_theme() print ("Successfully exported theme") print (resp) json_object = json.dumps(resp, indent=4) with open("theme.json", "w") as outfile: outfile.write(json_object) except AuthException as error: print ("Failed to export theme") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go import ( "encoding/json" "errors" "fmt" "os" "strings" ) // Args // None res, err := descopeClient.Management.Flow().ExportTheme() if err != nil { fmt.Print("Failed to export theme", err) } else { data, err :=json.Marshal(res) if err == nil { fileName := fmt.Sprintf("theme.json") if err == nil { err = os.WriteFile(fileName, data, 0644) fmt.Println("Successfully exported flow") } } } ``` ### Import Theme Use the code below to import a theme: ```javascript import * as fs from 'fs'; // Args // theme (Theme): the theme to import. dict in the format // {"id": "", "cssTemplate": {} } let data = fs.readFileSync('theme.json'); let jsonData = JSON.parse(data); const theme = jsonData["theme"] const resp = await descopeClient.management.theme.import(theme) if (!resp.ok) { console.log(resp) console.log("Unable to import theme.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully imported theme.") console.log(resp.data) } ``` ```python import json # Args # theme (Theme): the theme to import. dict in the format # {"id": "", "cssTemplate": {} } with open("theme.json", "r") as infile: themeJson = json.load(infile) theme = themeJson["theme"] try: resp = descope_client.mgmt.flow.import_theme(theme=theme) print ("Successfully imported theme") print (resp) except AuthException as error: print ("Failed to import theme") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go import ( "encoding/json" "errors" "fmt" "os" "strings" ) // Args // theme (Theme): the theme to import. dict in the format // {"id": "", "cssTemplate": {} } raw, err := os.ReadFile("theme.json") if err != nil { return err } theme := &descope.Theme{} err = json.Unmarshal(raw, theme) if err != nil { return err } res, err := descopeClient.Management.Flow().ImportTheme(theme) if err != nil { fmt.Print("Failed to import flow", err) } else { fmt.Print("Successfully imported flow", res) } ``` # Tenants (/management/tenant-management) Learn how to create and update tenants for your B2B app using the Descope console or API. # Tenants To learn more about multi-tenancy, check out our [Multi-Tenant vs. Single-Tenant blog](https://www.descope.com/blog/post/single-tenant-vs-multi-tenant). Descope supports a multi-tenancy architecture natively. Each project can have multiple tenants, and the end-users can belong to zero, one, or many tenants. In a multi-tenancy architecture, a single instance of a software application serves multiple customers, known as tenants. Each tenant is isolated and has its own data, configuration, user management, and functionality. This architecture is particularly beneficial for B2B applications where each customer can have multiple users. In Descope, tenants can be used in any situation in which you may need to group users together. For the full B2B model (tenants, users, roles, and JWTs), see the [B2B guide](/b2b). ## Tenant User Isolation By default, users in a Descope project share a single identity record. A user can belong to multiple tenants, but their login ID, credentials, and MFA state are shared across all of them. For example, if `alice@company.com` signs up in Tenant A and is later added to Tenant B, she uses the same password and MFA enrollment in both tenants. Enable **Tenant User Isolation** in [Project Settings](/management/project-settings#tenant-user-isolation) when the same login ID should represent a separate identity in each tenant, with independent credentials and MFA state. This is useful for B2B applications where the same email address may belong to different organizations. For instance, a consultant who uses `alice@company.com` with both Customer A and Customer B should have distinct accounts, passwords, and MFA enrollment in each tenant. ### Authentication with tenant user isolation When tenant user isolation is enabled, you must pass in a tenant ID at authentication time to scope identity resolution to a specific tenant. When using flows, you should pass in the `tenant` as a [flow input](/flows/dynamic-keys/flow-inputs). When using SDKs, pass the tenant directly into the authentication function. Refer to the specific SDK documentation for further details. `tenantId` in login or sign-up options scopes identity at authentication time. This is different from [tenant selection](/flows/screens/inputs/tenantselect-component), which switches the active tenant on an already-authenticated session. ## Managing Tenants Descope admins can create, update, activate, and disable tenants either manually on the [Tenants page](https://app.descope.com/tenants) of the Descope Console, or using the tenant management [SDKs](/management/tenant-management/sdks) and [APIs](/api/management/tenants). To manage an existing tenant in the Console, click the three-dot menu in the tenant's row. From there, you can edit tenant details, activate or disable the tenant, or delete it. Once a tenant exists, see [Configuring a Tenant](/management/tenant-management/tenant) to set up its authentication, authorization, styling, users, and settings, and [Sub Tenants](/management/tenant-management/sub-tenants) to model hierarchy within a tenant. ### Tenant Admins Users assigned to the [Tenant Admin role](/authorization/role-based-access-control#tenant-admin-role) for a specific tenant inherit the required permissions to manage [SSO](/management/tenant-management/sso) and Users for the tenant, and [impersonate users](/user-impersonation) associated with the tenant. ### Custom Tenant Attributes Descope allows you to create custom attributes that can store further details about your tenants. You can manage custom attributes from the [Custom Attributes tab](https://app.descope.com/tenants/attributes) of the Tenants page of the Descope Console. Custom attributes can be of type "text", "numeric", "boolean", "single select", "multi select", "date", or "month-day". You can utilize the "multi select" type as an array. Custom attributes can be used to store any data you want for the tenant. For example, this data could be a tenant's paid tier, geographical location, etc. You can later utilize these attributes within [custom claims](/security-best-practices/custom-claims) or load them for a tenant and display them within your application. ### SSO Settings Any [Project-Level SSO Settings](/auth-methods/sso/settings) (like redirect URLs, mandatory user attributes, SSO Setup Suite configurations, etc) will apply to all tenants in your project. For SSO configuration with the tenant's external IdP, refer to our [Tenant SSO doc](/management/tenant-management/sso). ## Widgets You can use Widgets to delegate tenant management actions to your [Tenant Admins](/management/tenant-management#tenant-admins). Widgets are client-side components that you can embed in your website using our SDKs. To learn more about delegating management using Widgets, refer to our [Admin Widgets doc](/widgets/admins). # Tenants with SDKs (/management/tenant-management/sdks) Learn how to create and update tenants for your B2B app using the Descope backend SDKs. # Tenants with SDKs If you wish to learn more about Tenants in general, see the [Tenants](/management/tenant-management) page. Descopers (Descope admins) can create and update tenants either manually in the [Descope Console](https://app.descope.com/tenants), using the Tenant Management [APIs](/api/management/tenants), or using our Management SDKs as shown below. ## Tenant management using the management SDK ### Load All tenants Use the code below to load all existing tenants within the project ```javascript let resp = await descopeClient.management.tenant.loadAll(); if (!resp.ok) { console.log("Unable to load tenants.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded tenants:") console.log(resp.data) } ``` ```python try: resp = descope_client.mgmt.tenant.load_all() print ("Successfully loaded tenants:") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load tenants.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := descopeClient.Management.Tenant().LoadAll(ctx) if (err != nil){ fmt.Println("Unable to load tenants: ", err) } else { fmt.Println("Successfully loaded tenants: ") for _, t := range res { fmt.Println(t) } } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); // Load all tenants try { List tenants = ts.loadAll(); for (Tenant t : tenants) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp try { var response = await descopeClient.Mgmt.V1.Tenant.All.GetAsync(); foreach (var tenant in response!.Tenants!) { // do something var parent = tenant.Parent; // parent tenant ID (empty if top-level) var successors = tenant.Successors; // direct sub-tenant IDs } } catch (DescopeException ex) { // Handle the error } ``` ### Load Tenant by ID This function allows for you to load a specific tenant based on the tenant's ID. ```javascript // Args: // id (String): The ID of the tenant which you want to load const id = "xxxx" let resp = await descopeClient.management.tenant.load(id); if (!resp.ok) { console.log("Failed to load tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded tenant.") console.log(resp.data) } ``` ```python # Args: # id (String): The ID of the tenant which you want to load try: resp = descope_client.mgmt.tenant.load(id="xxxx") print("Successfully loaded tenant") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to load tenant") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (String): The ID of the tenant which you want to load id := "xxxx" res, err := descopeClient.Management.Tenant().Load(ctx, id) if (err != nil){ fmt.Println("Unable to load tenant: ", err) } else { fmt.Println("Successfully loaded tenant: ") fmt.Println(res) } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); try { ts.load("my-custom-id"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // tenantID (string): The ID of the tenant to load. var tenantID = "tenant-id"; try { var response = await descopeClient.Mgmt.V1.Tenant.GetWithIdAsync(tenantID); var parent = response!.Tenant!.Parent; // parent tenant ID (empty if top-level) var successors = response.Tenant.Successors; // direct sub-tenant IDs } catch (DescopeException ex) { // Handle the error } ``` ### Search Tenants This function allows for you to search Descope tenants by ID, name, self service provisioning domain, and custom attributes. ```javascript // Args: // ids (String[]): Array of tenant IDs to search for. const ids = ["TestTenant"] // names (String[]): Array of tenant names to search for. const names = ["TestTenant"] // selfProvisioningDomains (String[]): Array of self service provisioning domains to search for. const selfProvisioningDomains = ["example.com", "company.com"] // customAttributes (String[]): Array of self service provisioning domains to search for. const customAttributes = {"mycustomattribute": "Test"} // When searching based on one of these items or a few of these items, leave the applicable items you are not searching for to null. let resp = await descopeClient.management.tenant.searchAll(ids, names, selfProvisioningDomains, customAttributes); if (!resp.ok) { console.log("Failed to search tenants.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully searched tenants.") resp.data.forEach((tenant) => { console.log(tenant) }); } ``` ```python # Args: # ids (String[]): Array of tenant IDs to search for. # names (String[]): Array of tenant names to search for. # selfProvisioningDomains (String[]): Array of self service provisioning domains to search for. # customAttributes (String[]): Array of self service provisioning domains to search for. # When searching based on one of these items or a few of these items, leave the applicable items you are not searching for to null. try: resp = descope_client.mgmt.tenant.search_all(ids=["TestTenant"], names=["TestTenant"], self_provisioning_domains=["example.com", "company.com"], custom_attributes={"mycustomattribute": "Test"}) print("Successfully searched tenants") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to search tenants") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // searchOptions (&descope.TenantSearchOptions{}): Search options for your tenant search searchOptions := &descope.TenantSearchOptions{} searchOptions.IDs = []string{"TestTenant"} searchOptions.Names = []string{"TestTenant"} searchOptions.SelfProvisioningDomains = []string{"example.com", "company.com"} searchOptions.CustomAttributes = map[string]any{"mycustomattribute": "Test"} res, err := descopeClient.Management.Tenant().SearchAll(ctx, searchOptions) if (err != nil){ fmt.Println("Unable to search tenants: ", err) } else { fmt.Println("Successfully searched tenants: ") for _, t := range res { fmt.Println(t) } } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); try { List tenants = ts.searchAll(TenantSearchRequest.builder() .ids(Arrays.asList("TestTenant")) .names(Arrays.asList("TestTenant")) .customAttributes(Map.of("mycustomattribute", "Test")) .selfProvisioningDomains(Arrays.asList("example.com", "company.com"))); for (Tenant t : tenants) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // searchRequest (SearchTenantsRequest): Fine-tune filters. var searchRequest = new SearchTenantsRequest { TenantIds = new List { "my-custom-id" }, TenantNames = new List { "My Tenant" }, TenantSelfProvisioningDomains = new List { "domain.com", "company.com" }, CustomAttributes = new SearchTenantsRequest_customAttributes { AdditionalData = new Dictionary { { "mycustomattribute", "Test" } } }, }; try { var response = await descopeClient.Mgmt.V1.Tenant.Search.PostAsync(searchRequest); foreach (var tenant in response!.Tenants!) { // do something } } catch (DescopeException ex) { // Handle the error } ``` ### Create Tenant At the time of creation, the tenant must be given a name and a tenant-id. If you don't provide a tenant-id, a tenant-id is automatically generated. The tenant-id is used for sign-up/sign-in and other management operations later. In addition, you can also set domains for the tenant. The domain is used to automatically assign the end-user to a tenant at the time of sign-up and sign-in. The tenant name must be unique per project. The tenant ID is generated automatically for the tenant when not provided. ```javascript // There are two ways to create a tenant via SDK. createWithId (which will assign the given id) and create (which will automatically generate the id). Examples below: // createWithId: Create a new tenant with a given name and tenant id. // ================================================================== // Args: // name (str): The tenant's name var name = "TestTenantCreateWithId" // id (str): The tenant's id. var id = "TestConfiguredId" // selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. var selfProvisioningDomains = ["TestDomain1.com", "TestDomain2.com"] // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"} let resp = await descopeClient.management.tenant.createWithId(id, name, selfProvisioningDomains, customAttributes) if (!resp.ok) { console.log(resp) console.log("Unable to create tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created tenant.") console.log(resp.data) } // Create: Create a new tenant with the given name. The id will be automatically generated and returned. // ================================================================== // Args: // name (str): The tenant's name name = "TestTenantCreateGeneratedId" // selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. selfProvisioningDomains = ["TestDomain3.com", "TestDomain4.com"] // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"} resp = await descopeClient.management.tenant.create(name, selfProvisioningDomains, customAttributes) if (!resp.ok) { console.log("Unable to create tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created tenant.") console.log(resp.data) } ``` ```python # Create a new tenant with the given name. Tenant IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project. # Args: # name (str): The tenant's name # id (str): Optional tenant ID. If not provided, it will be auto assigned. # self_provisioning_domains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app try: resp = descope_client.mgmt.tenant.create(name="TestTenantCreateWithId", id="TestTenantCreateWithId", self_provisioning_domains=["TestDomain1.com", "TestDomain2.com"], custom_attributes={"attribute1": "Value 1", "attribute2": "Value 2"}) print ("Successfully created tenant.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to create tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // There are two ways to create a tenant via SDK. CreateWithID (which will assign the given id) and Create (which will automatically generate the id). Examples below: // CreateWithID: Create a new tenant with a given name and tenant id. // ================================================================== // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantRequest (&descope.TenantRequest{}): Tenant options for creation tenantRequest := &descope.TenantRequest{} tenantRequest.Name = []string{"TestTenant"} tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"} tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"} // id (str): The tenant's id. id := "TestConfiguredId" err := descopeClient.Management.Tenant().CreateWithID(ctx, id, tenantRequest) if (err != nil){ fmt.Println("Unable to create tenant with specified ID: ", err) } else { fmt.Println("Successfully created tenant with specified ID") } // Create: Create a new tenant with the given name. The id will be automatically generated and returned. // ================================================================== // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantRequest (&descope.TenantRequest{}): Tenant options for creation tenantRequest := &descope.TenantRequest{} tenantRequest.Name = []string{"TestTenant"} tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"} tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"} tenantID, err := descopeClient.Management.Tenant().Create(ctx, tenantRequest) if (err != nil){ fmt.Println("Unable to create tenant: ", err) } else { fmt.Println("Successfully created tenant. The automatically generated ID is: ", tenantID) } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); // The self provisioning domains or optional. If given they'll be used to associate // Users logging in to this tenant try { ts.create("My Tenant", Arrays.asList("domain.com"), new HashMap() {{ put("custom-attribute-1", "custom-value1"); put("custom-attribute-2", "custom-value2"); }}); } catch (DescopeException de) { // Handle the error } // You can optionally set your own ID when creating a tenant try { ts.createWithId("my-custom-id", "My Tenant", Arrays.asList("domain.com"), new HashMap() {{ put("custom-attribute-1", "custom-value1"); put("custom-attribute-2", "custom-value2"); }}); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // createRequest (CreateTenantRequest): Configuration for the new tenant (name required). var createRequest = new CreateTenantRequest { Name = "name", Id = "my-tenant-id", // optional — omit to auto-generate SelfProvisioningDomains = new List { "domain" }, CustomAttributes = new CreateTenantRequest_customAttributes { AdditionalData = new Dictionary { { "mycustomattribute", "test" } } }, }; try { var createResponse = await descopeClient.Mgmt.V1.Tenant.Create.PostAsync(createRequest); var newTenantId = createResponse!.Id; } catch (DescopeException ex) { // Handle the error } ``` ### Update Tenant Use the code below to update an existing tenant with the given name and domains. All parameters are used as overrides to the existing tenant. Empty fields will override populated fields. ```javascript // Args: // id (str): The tenant's id. var id = "xxxxxx" // name (str): The tenant's name var name = "Test Updated Name" // selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. If changed, it will be the new list of self provisioned domains. var selfProvisioningDomains = ["TestUpdatedDomain1.com", "TestUpdatedDomain2.com"] // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"} const resp = await descopeClient.management.tenant.update(id, name, selfProvisioningDomains, customAttributes); if (!resp.ok) { console.log(resp) console.log("Failed to update tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully update tenant.") console.log(resp.data) } ``` ```python # Args: # id (str): The ID of the tenant to update. # name (str): The tenant's name, if changed, it will be the new name. # self_provisioning_domains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. If changed, it will be the new list of self provisioned domains. # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app try: resp = descope_client.mgmt.tenant.update(id="xxxxxx", name="Test Updated Name", self_provisioning_domains=["TestUpdatedDomain1.com", "TestUpdatedDomain2.com"], custom_attributes={"attribute1": "Value 1", "attribute2": "Value 2"}) print ("Successfully updated tenant.") except AuthException as error: print ("Unable to update tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the tenant you want to update. id := "xxxxxx" // tenantRequest (&descope.TenantRequest{}): Tenant options for update tenantRequest := &descope.TenantRequest{} tenantRequest.Name = []string{"TestTenant"} tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"} tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"} err := descopeClient.Management.Tenant().Update(ctx, id, tenantRequest) if (err != nil){ fmt.Println("Unable to update tenant: ", err) } else { fmt.Println("Successfully updated tenant") } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); // Update will override all fields as is. Use carefully. try { ts.update("my-custom-id", "My Tenant", Arrays.asList("domain.com", "another-domain.com"), new HashMap() {{ put("custom-attribute-1", "custom-value1"); put("custom-attribute-2", "custom-value2"); }}); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // tenantID (string): The ID of the tenant to update. var tenantID = "tenant-id"; // updateRequest (UpdateTenantRequest): New details to set (all fields override existing). var updateRequest = new UpdateTenantRequest { Id = tenantID, Name = "Updated Name", SelfProvisioningDomains = new List { "domain" }, CustomAttributes = new UpdateTenantRequest_customAttributes { AdditionalData = new Dictionary { { "mycustomattribute", "test" } } }, }; try { await descopeClient.Mgmt.V1.Tenant.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Tenant Use the code below to delete an existing tenant. Please note that this action is irreversible. ```javascript // Args: // id (str): The tenant's id. var id = "xxxxxx" // cascade (boolean): Pass true to cascade value, in case you want to delete all users/keys associated only with this tenant var cascade = false let resp = await descopeClient.management.tenant.delete(id, cascade); if (!resp.ok) { console.log(resp) console.log("Unable to delete tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted tenant.") console.log(resp.data) } ``` ```python # Args: # id (str): The id of the tenant to be deleted. # cascade (boolean): Pass true to cascade value, in case you want to delete all users/keys associated only with this tenant try: resp = descope_client.mgmt.tenant.delete(id="xxxxxx", cascade=False) print("Successfully deleted tenant.") except AuthException as error: print ("Unable to delete tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (str): The id of the tenant you want to delete. id := "xxxxxx" // cascade (boolean): Pass true to cascade value, in case you want to delete all users/keys associated only with this tenant cascade := false err := descopeClient.Management.Tenant().Delete(ctx, id, cascade) if (err != nil){ fmt.Println("Unable to delete tenant: ", err) } else { fmt.Println("Successfully deleted tenant") } ``` ```java TenantService ts = descopeClient.getManagementServices().getTenantService(); // Tenant deletion cannot be undone. Use carefully. try { ts.delete("my-custom-id"); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // tenantID (string): The ID of the tenant to delete (irreversible). var tenantID = "tenant-id"; // cascade (bool): Pass true to delete users/keys associated only with this tenant. var cascade = false; try { await descopeClient.Mgmt.V1.Tenant.DeletePath.PostAsync(new DeleteTenantRequest { Id = tenantID, Cascade = cascade, }); } catch (DescopeException ex) { // Handle the error } ``` ### Update Tenant Password Policy Use the code below to update password policy for your tenant. ```javascript // Args: //tenantId (str): The tenant Id whose password policy has to be updated. //enabled (boolean): To enable //minLength (int) : minimum passwords length //lowercase (boolean): Requires atleast one lowercase letter //uppercase (boolean): Requires atleast one uppercase letter //number (boolean): Requires atleast one number //nonAlphaNumeric (boolean) : Requies at least one non-alphanumeric character //expiration (boolean): Enable password expiration //expirationWeeks (int): Password expiration period in weeks //reuse (boolean): Enable/Disable prevent password reuse //reuseAmount (int): Number of passwords to remember //lock (boolean) : Enable/Disable account locking //lockAttempts (int) : Lock account after this number of attempts const tenantId = "xxxx" const policysetting = { enabled: true, minLength: 8, expiration: true, expirationWeeks: 4, lock: true, lockAttempts: 5, reuse: true, reuseAmount: 6, lowercase: true, uppercase: false, number: true, nonAlphaNumeric: false, } const resp = await descopeClient.management.password.configureSettings(tenantId, policysetting); if (!resp.ok) { console.log("Failed to update access key.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated password policy.") console.log(resp.data) } ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() tenantId = "xxxx" // policy (descope.PasswordSettings): Policy object to update password policy. policy := &descope.PasswordSettings{ Enabled: true, MinLength: 8, Lowercase: true, Uppercase: true, Number: true, NonAlphanumeric: true, Expiration: true, ExpirationWeeks: 3, Reuse: true, ReuseAmount: 3, Lock: true, LockAttempts: 4, } resp, err := descopeClient.Management.Password().ConfigureSettings(ctx, tenantId, policy) if (err != nil){ fmt.Println("Failed to update password policy: ", err) } else { fmt.Println("Successfully updated password policy", resp) } ``` ```ruby # settings is a hash storing key value pair for password policy settings for the required tenant ID settings = {"minLength" => 10} descope_client.update_password_settings(settings) ``` ```csharp // Args: // tenantID (string?): Optional tenant ID. Omit/null uses project‑level settings. string? tenantID = "tenant-id"; try { var currSettings = tenantID != null ? await descopeClient.Mgmt.V1.Password.Settings.GetWithTenantIdAsync(tenantID) : await descopeClient.Mgmt.V1.Password.Settings.GetForProjectAsync(); currSettings!.Enabled = true; currSettings.MinLength = 8; currSettings.Lowercase = true; currSettings.Uppercase = true; currSettings.Number = true; currSettings.NonAlphanumeric = true; currSettings.Expiration = true; currSettings.ExpirationWeeks = 3; currSettings.Reuse = true; currSettings.ReuseAmount = 3; currSettings.Lock = true; currSettings.LockAttempts = 5; await descopeClient.Mgmt.V1.Password.Settings.PostWithSettingsResponseAsync(currSettings); } catch (DescopeException ex) { // Handle the error } ``` ### Get and Update Tenant Settings Use the `getSettings` and `configureSettings` functions as shown below to access and update settings for your tenant, based on the tenant ID. You can also configure the tenant's [SSO Setup Suite](/auth-methods/sso/settings#sso-setup-suite-settings) through `ssoSetupSuiteSettings`. You can find the full set of configurable `disabledFeatures` keys like `saml`, `oidc`, `scim`, `ssoDomains` and others [here](/auth-methods/sso/settings#sso-setup-suite-settings). ```javascript // Args: // id (String): The ID of the tenant for which you want to access and update settings const id = "xxxx" // Load tenant settings by id const tenantSettings = await descopeClient.management.tenant.getSettings(id); // Update will override all fields as is. Use carefully. await descopeClient.management.tenant.configureSettings(id, { domains: ['domain1.com'], selfProvisioningDomains: ['domain1.com'], enabled: true, refreshTokenExpiration: 12, refreshTokenExpirationUnit: 'days', // 'minutes' | 'hours' | 'days' | 'weeks' sessionTokenExpiration: 10, sessionTokenExpirationUnit: 'minutes', // 'minutes' | 'hours' | 'days' | 'weeks' enableInactivity: true, JITDisabled: false, InactivityTime: 10, InactivityTimeUnit: 'minutes', // 'minutes' | 'hours' | 'days' | 'weeks' // Configure the tenant's SSO Setup Suite (requires @descope/node-sdk v2.5.0+) ssoSetupSuiteSettings: { enabled: true, styleId: 'my-style-id', disabledFeatures: { scim: true, // hide the SCIM tab in the Setup Suite groupMapping: false, // legacy combined toggle — disabling this hides BOTH Role Mapping and FGA Mapping }, }, }); ``` ```python from descope.management.common import SSOSetupSuiteSettings, SSOSetupSuiteSettingsDisabledFeatures # Args: # id (str): The ID of the tenant for which you want to access and update settings id = "xxxx" # Load tenant settings by id try: tenant_settings = descope_client.mgmt.tenant.load_settings(id=id) print("Successfully loaded tenant settings:") print(json.dumps(tenant_settings, indent=2)) except AuthException as error: print("Failed to load tenant settings") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) # Update will override all fields as is. Use carefully. # self_provisioning_domains is required; every other field is optional. try: descope_client.mgmt.tenant.update_settings( id=id, self_provisioning_domains=["domain1.com"], session_settings_enabled=True, refresh_token_expiration=12, refresh_token_expiration_unit="days", # "minutes" | "hours" | "days" | "weeks" session_token_expiration=10, session_token_expiration_unit="minutes", # "minutes" | "hours" | "days" | "weeks" enable_inactivity=True, inactivity_time=10, inactivity_time_unit="minutes", # "minutes" | "hours" | "days" | "weeks" JITDisabled=False, # Configure the tenant's SSO Setup Suite sso_setup_suite_settings=SSOSetupSuiteSettings( enabled=True, style_id="my-style-id", disabled_features=SSOSetupSuiteSettingsDisabledFeatures( scim=True, # hide the SCIM tab in the Setup Suite group_mapping=False, # legacy combined toggle — disabling this hides BOTH Role Mapping and FGA Mapping ), ), ) print("Successfully updated tenant settings.") except AuthException as error: print("Failed to update tenant settings") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // id (String): The ID of the tenant for which you want to access and update settings id := "xxxx" // Load tenant settings by a tenant id settings, err := descopeClient.Management.Tenant().GetSettings(ctx) // Configure desired settings settingsRequest := &descope.TenantSettings{} settingsRequest.SelfProvisioningDomains = []string{"domain.com", "company.com"} settingsRequest.RefreshTokenExpiration = 30 settingsRequest.RefreshTokenExpirationUnit = "days" settingsRequest.SessionTokenExpiration = 30 settingsRequest.SessionTokenExpirationUnit = "minutes" settingsRequest.EnableInactivity = true settingsRequest.InactivityTime = 2 settingsRequest.InactivityTimeUnit = "days" // Update the tenant settings err := descopeClient.Management.Tenant().ConfigureSettings(ctx, id, settingsRequest) ``` # Sub-tenants (/management/tenant-management/sub-tenants) Learn about Descope's sub-tenant hierarchy system and management. # Sub-tenants Sub-tenants allow you to create multi-level hierarchical structures within your tenant organization. This guide explains how sub-tenants work and how to manage them effectively: - A tenant can have multiple sub-tenants - Sub-tenants can have their own sub-tenants (nested hierarchy) - Each sub-tenant has exactly one parent (either a tenant or another sub-tenant) - Sub-tenants inherit certain configurations while maintaining independent control over others ## Creating Sub-tenants Currently, the creation of sub-tenants is only supported through the [Tenants page](https://app.descope.com/tenants) of the Descope Console or through our [API](https://docs.descope.com/api/management/tenants/create-tenant). You can create a sub-tenant in either of these places: 1. **Inside a tenant**: open the parent tenant, go to **Settings → Sub-tenants**, and create a child tenant there. 2. **From the main Tenants page**: create a new tenant, toggle on **Create as sub-tenant**, and select an existing tenant or sub-tenant as the parent. When you create a sub-tenant, you can set **Role Inheritance**, which controls how the sub-tenant handles users and roles from its parent tenant: | Option | Behavior | | ------ | -------- | | **Full Inheritance** | Sync all users and assigned roles from the parent tenant | | **User only** | Sync users from the parent; roles are assigned locally on the sub-tenant | | **None** | Disable inheritance for both users and roles | Inheritance only flows downward: roles or membership on a sub-tenant do not grant access to the parent. ![Role Inheritance when creating from Settings → Sub-tenants](/assets/sub-tenant-role-inheritance-settings.webp) ![Role Inheritance when creating from the Tenants page](/assets/sub-tenant-role-inheritance-tenants-page.webp) You can also manage existing sub-tenants from the **Sub-tenants** tab in the parent tenant's Settings. To see a complete hierarchy, including nested sub-tenants, you can select the **Show all descendants** checkbox. ![Sub-tenant list](/assets/tenant-subtenant-view.webp) ### Sub-tenant Configurable Features Sub-tenants support independent configuration of many features. See the relevant documentation for details: - [Tenant Details](/management/tenant-management/tenant#tenant-settings) - [Custom attributes](/management/tenant-management#custom-tenant-attributes) - [RBAC Authorization Settings](/authorization/role-based-access-control#tenants-and-roles) - [SSO Setup Suite Configuration](/management/tenant-management/tenant#sso-setup-suite-configuration) ### Authentication and Session Settings Authentication settings follow specific inheritance rules: - **[SSO Configuration](/management/tenant-management/sso)**: Can either be - Inherited from the parent tenant (only group mapping and SSO domain are editable) - Created independently for the sub-tenant - **[Password Settings](/management/tenant-management/tenant#passwords)**: Always inherited from the parent tenant - **[Session Management](/management/tenant-management/tenant#session-management)**: Always inherited from the parent tenant ### JWT Structure Sub-tenants appear in the session JWT under the same `tenants` map as any other tenant. Each key is a tenant ID with that tenant's `roles` and `permissions`. Parent and sub-tenant IDs sit at the same level — the claim does not encode hierarchy. For example, a user has the **Partner** role only on **Tenant 1**, and **Tenant 1** has one sub-tenant (**Tenant 1 Sub-tenant**): ![Parent tenant assignment](/assets/parent-tenant-assignment.webp) ![Sub-tenant assignment](/assets/tenant-subtenant-console.webp) ```json { "tenants": { "": { "permissions": [], "roles": ["Partner"] }, "": { "permissions": null, "roles": ["Partner"] } } } ``` Both IDs appear the same way. If your app needs to tell parents from sub-tenants: - **Add the `tenant.subtenant` claim** with a [JWT Template](/management/token/jwt-templates) or [Custom Claims](/flows/actions/custom-claims) action. This [dynamic value](/flows/dynamic-keys#tenant) returns `true` if the tenant has a parent, `false` otherwise. - **Look up the tenant** with [Load Tenant by ID](/api/management/tenants/load-tenant-by-id) or [Search Tenants](/api/management/tenants/search-tenants) if you need the full hierarchy. Each tenant includes `parent` (empty for top-level) and `successors` (direct sub-tenant IDs), so you can rebuild the hierarchy server-side. Claims set from `tenant.subtenant` or a custom tenant attribute only reflect the currently selected tenant (`dct`). They do not describe hierarchy for every tenant the user belongs to. # Exporting Tenants (/management/tenant-management/tenant-exporting) This guide will cover the fundamentals of tenant exporting with Descope. # Exporting Tenants There are two main methods for exporting tenants from Descope: 1. **API/SDK Methods**: For migrating tenants between projects or to external systems, use the [Load All Tenants API](/api/management/tenants/load-all-tenants) or the SDK methods to export tenant data programmatically. This is the recommended approach for project migrations and external system integrations. 2. **CSV Export**: If you need to export tenants as a CSV file (useful when dealing with large batches of tenants), you can use the Descope Console's CSV export feature. ## Using the Load All Tenants API or SDK For scenarios where you need to migrate tenant configurations between projects or to a different system, you can use the [Load All Tenants API](/api/management/tenants/load-all-tenants) or the SDK methods to export tenant data and then recreate the tenants using the [Create Tenant API](/api/management/tenants/create-tenant). You can export tenant data securely by utilizing the [Search Tenants](/api/management/tenants/search-tenants) endpoint of the Descope Backend API. This endpoint allows for the programmatic extraction of tenant information. Alternatively, the [loadAll()](/management/tenant-management/sdks#load-all-tenants) function available in the Descope Backend SDKs can be employed to retrieve a comprehensive list of tenants. Submitting an empty request payload to the Search Tenants endpoint will return all tenants. Here's an example `curl` command to do so: ```bash curl -i -X POST \ __BaseURL__/v1/mgmt/tenant/search \ -H 'Authorization: Bearer __ProjectID__:' \ -H 'Content-Type: application/json' \ -d '{}' ``` You can also use the Load All Tenants endpoint without a request body: ```bash curl -i -X GET \ __BaseURL__/v1/mgmt/tenant/all \ -H 'Authorization: Bearer __ProjectID__:' ``` ### Example Output The output of the above curl commands will be a JSON object containing tenant details. Below is an example of what the response might look like: ```json { "tenants": [ { "id": "T2xBhVWosRC2ZeCV0qri3gosE1ZW", "name": "Example", "selfProvisioningDomains": [ "example.com" ], "customAttributes": {}, "authType": "saml", "domains": [ "example.com" ], "createdTime": 1747417398, "disabled": false, "enforceSSO": false, "enforceSSOExclusions": [], "federatedAppIds": [ "" ], "parent": "", "successors": [ "T33kYuODI4fPWvymbOcTUELEGbiW" ], "defaultRoles": [], "roleInheritance": "" }, { "id": "T33kYuODI4fPWvymbOcTUELEGbiW", "name": "example-subtenant", "selfProvisioningDomains": [], "customAttributes": {}, "authType": "saml", "domains": [], "createdTime": 1759862758, "disabled": false, "enforceSSO": false, "enforceSSOExclusions": [], "federatedAppIds": [], "parent": "T2xBhVWosRC2ZeCV0qri3gosE1ZW", "successors": [], "defaultRoles": [], "roleInheritance": "" } ] } ``` For complex migration scenarios involving SSO configurations, custom attributes, or tenant hierarchies, please contact our [Support team](/support) for assistance. ## Exporting Tenants as CSV This method of export is not meant to transfer tenants between different projects. If you need to export tenants as a CSV file (useful when dealing with large batches of tenants), you can use the Descope Console. Head over to the [Tenants page](https://app.descope.com/tenants), select the required tenants for export, and the "Export CSV" button will appear. ![Export Tenants CSV](/assets/export-tenant.webp) By pressing the button, you should be prompted to download the file. # Configuring a Tenant (/management/tenant-management/tenant) Configure a tenant's authentication, authorization, styling, users, and settings in Descope, matching the Build, Manage, and Settings areas of the Console. # Configuring a Tenant Open a tenant from the [Tenants page](https://app.descope.com/tenants) of the Descope Console. Each tenant is organized into three areas, and this page follows the same structure so you can find whatever you see in the Console: - **Build**: how users authenticate and what they can do ([Authentication Methods](#authentication-methods), [Authorization](#authorization), and [Styles](#styles)). - **Manage**: the [Users](#users) associated with the tenant. - **Settings**: [tenant details](#tenant-settings), [session policy](#session-management), [SSO Setup Suite](#sso-setup-suite-configuration), and [sub-tenants](#sub-tenants). You can configure everything below in the Console, or with the Tenant Management [APIs](/api/management/tenants) and [SDKs](/management/tenant-management/sdks). ## Build ### Authentication Methods For security reasons, a custom tenant policy for authentication methods can only be **more restrictive** than the project policy. You cannot loosen requirements at the tenant level. This section allows you to configure specific tenant-level settings for different authentication methods, as well as set up SSO configurations for the tenant. #### SSO You can configure [SSO connections](/management/tenant-management/sso) for the tenant, underneath this section. For more information on SSO in general, see our docs on [SSO](/auth-methods/sso). The **Cross-App Access** tab under SSO is where you accept ID-JAG assertions from a tenant's workforce IdP. For every field, see [Cross-App Access](/management/tenant-management/sso/cross-app-access). For the product story, see [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). #### Passwords You can override the project-level [password policy](/auth-methods/passwords/settings) for a tenant. By default the tenant inherits the project policy. Select **Custom** under **Password Policy** to set a tenant-specific policy. Tenant admins can set the same policy themselves, without Console access, from the [Tenant Profile Widget](/widgets/admins#tenant-profile-widget). If a user belongs to multiple tenants, the stricter elements of each tenant's password policy apply. | Setting | Details | Default | | ------- | ------- | ------- | | Minimum password length | Require passwords at least this many characters long. | `8` (range: 5-64) | | Require at least one letter (any case) | Require at least one alphabetic character (a-z or A-Z). | Unchecked | | Require at least one lowercase letter | Require at least one lowercase letter (a-z). | Checked | | Require at least one uppercase letter | Require at least one uppercase letter (A-Z). | Checked | | Require at least one number | Require at least one digit (0-9). | Checked | | Require at least one non-alphanumeric character | Require at least one special / non-alphanumeric character. | Checked | | Disallowed characters | Characters that must not appear in the password. Leave empty to allow any character otherwise permitted by the policy. | Empty | | Disallow passwords identical to the user's email | Block passwords that match the user's full email address, or the portion before `@` (case-insensitive). | Unchecked | | Enable password expiration | When enabled, passwords expire after the configured number of weeks and the user must choose a new one. | Unchecked; duration `26` weeks (range: 1-999) | | Prevent password reuse | Remember the last N passwords and reject them when the user sets a new password (for example after reset or expiration). | Unchecked; remember last `10` (range: 10-50) | | Lock account after x attempts | After x incorrect password attempts, lock the account so the user cannot sign in until an admin unlocks it. | Unchecked; after `5` attempts (range: 2-10) | | Temporary lock after x attempts, for y minutes | After x incorrect attempts, lock the account temporarily for y minutes. The user can try again when the lock expires. | Unchecked; `3` attempts (range: 1-10) after `5` minutes (range: 1-999) | | Enable password strength enforcement | Require a minimum password strength score. Passwords must meet or exceed the configured minimum to be accepted. | Average (other options: Strong, Very Strong, Very Weak, Weak) | When a user belongs to more than one tenant, Descope applies the most restrictive combination of policies. How "more restrictive" is decided: | Setting | How the stricter value is chosen | | ------- | --------| | Minimum password length | Highest number across tenants. | | Require at least one letter / lowercase / uppercase / number / non-alphanumeric | Enforced if any tenant has it enabled. | | Disallowed characters | Union of all disallowed characters across tenants. | | Disallow passwords identical to the user's email | Enforced if any tenant has it enabled. | | Enable password expiration | Enforced if any tenant has it enabled; uses the shortest expiration period. | | Prevent password reuse | Enforced if any tenant has it enabled; remembers the largest number of previous passwords. | | Lock account after x attempts | Enforced if any tenant has it enabled; uses the lowest attempt threshold. | | Temporary lock after x attempts, for y minutes | Enforced if any tenant has it enabled; uses the lowest attempt threshold and lock duration. | | Enable password strength enforcement | Enforced if any tenant has it enabled; uses the highest minimum strength. | For more information on Passwords in general, see our docs on [Passwords](/auth-methods/passwords). #### One-time Password (OTP) By default, the tenant inherits the project-level [OTP settings](/auth-methods/otp/settings). Select **Custom** under **One-time Password** on the tenant to override them for that tenant only. | Setting | Details | | ------- | --------| | Domain | URL domain used in the email or text message sent to the end user. | | Expiration Time | How long the OTP code remains valid. A shorter window reduces the time available for guessing or replay. | | Number of Retries and Attempts Timeframe | Cap how many OTP messages a recipient can receive within the configured timeframe. Further sends are blocked until the window resets. | For more information on OTP in general, see our docs on [OTP](/auth-methods/otp). #### Magic Link By default, the tenant inherits the project-level [Magic Link settings](/auth-methods/magic-link/settings). Select **Custom** under **Magic Link** on the tenant to override them for that tenant only. | Setting | Details | | ------- | --------| | Redirect URL | Where the user is sent after a successful Magic Link login. Can still be overridden by the SDK or API call. | | Expiration Time | How long the Magic Link remains valid. A shorter window reduces the time available for misuse of a leaked link. | | Number of Retries and Attempts Timeframe | Cap how many Magic Link messages a recipient can receive within the configured timeframe. Further sends are blocked until the window resets. | For more information on Magic Link in general, see our docs on [Magic Links](/auth-methods/magic-link). ### Authorization Each tenant has an **Authorization** tab where you can create and manage **tenant-level** roles and permissions for that organization. Project-level roles will still apply across tenants, but tenant-level roles will be scoped to this tenant only. You can also mark project-level roles as default for the tenant so new users in that tenant receive them automatically. For the full model (project vs tenant roles, default/hidden roles, JWT claims, and assignment), see our docs on tenant-level roles in [Role-Based Access Control](/authorization/role-based-access-control#tenants-and-roles). You can also read more about our authorization model in general with the following docs: - [Authorization overview](/authorization) (RBAC and Fine-Grained Authorization) - [Authorization with SSO providers](/management/tenant-management/sso/how-authorization-works-with-sso-providers) (mapping IdP groups to roles) - [Tenant Admin role](/authorization/role-based-access-control#tenant-admin-role) ### Styles This will only apply once the flow knows which tenant is being used. Therefore, it's only recommended to use this feature, when you are using a [tenant slug](/flows/dynamic-keys/flow-inputs) to hardcode the tenant in your flow. You can adjust the styling of the tenant's login page with the following settings: | Setting | Details | | ------- | --------| | Logo | You can upload a logo for the tenant's login page. | | Favicon | You can upload a favicon for the tenant's login page. | | Colors | You can set the colors for the tenant's login page, similar to how you configure a [custom theme](/management/styles) for your project. | ![tenant settings styles](/assets/tenant-settings-styles.webp) You can also configure a [custom theme](/management/styles) and just harcode the theme, like you do the tenant. However, if you do not want to have to create individual styles for each tenant and map them accordingly in the code of your application, you can rely on this feature to do it for you. ## Manage ### Users The **Users** tab shows every user associated with this tenant. It's a filtered view of your project's users, scoped to this tenant, so you can see who belongs to the organization and manage their tenant roles. By default, user identities exist at a Project level. This means that a user has a single identity across every tenant they belong to, so the same login ID, credentials, and MFA state are shared. If the same login ID needs to be treated as a completely separate identity in each tenant, with independent credentials and MFA, enable [Tenant User Isolation](/management/tenant-management#tenant-user-isolation). ![tenant settings users](/assets/tenant-settings-users.webp) The UI is very similar to the main [Users](https://app.descope.com/users) page in the Console, where you can create, edit, and delete users. To learn how to add users to a tenant, read our docs on [User Management](/management/user-management). ![tenant settings adding users](/assets/tenant-settings-adding-users.webp) You can also do this with the [Users API](/api/management/users) and [SDKs](/management/user-management/sdks), or add users to a tenant [within a flow](/management/tenant-management/handling-tenants-in-flows/add-user-to-tenant). ## Settings **Settings** in the tenant sidebar has two sections: - **[Sub-tenants](#sub-tenants)**: child tenants under this organization - **[Tenant Settings](#tenant-settings)**: [details](#tenant-name) such as name and email domain, [session policy](#session-management), and [SSO Setup Suite configuration](#sso-setup-suite-configuration) ### Sub-tenants Sub-tenants let you model hierarchy inside a tenant, such as departments or business units within a customer organization. See [Sub-tenants](/management/tenant-management/sub-tenants) for details. ![tenant settings sub-tenants](/assets/tenant-settings-sub-tenants.webp) ### Tenant Settings #### Tenant Name When creating your tenant, you can set the tenant name. This can also be modified later. To enable the ability to give multiple tenants the same name on your project, reach out to our support (support@descope.com). #### Tenant ID When creating a tenant, you can set the Tenant ID. However, if not provided during tenant creation, it will be autogenerated. The Tenant ID is not modifiable after tenant creation. #### Email Domain This is only necessary for tenants that **don't use SSO**. If you are using SSO, you need to make sure you've configured [SSO domains](/auth-methods/sso#option-1-sso-domains) for your SSO configuration instead, in order to associate the right users with the right SSO provider. Users with email domains matching this configuration will automatically be associated with the tenant when they sign up or in. #### SSO Enabled This is different than the `ssoEnabled` flow condition, which simply checks if SSO is enabled for the user's associated tenant. When this feature is enabled, under **Tenant access**, SSO authentication will be enforced for all users within the tenant. Users will be required to authenticate through the configured SSO provider (SAML or OIDC) and will not be able to use other authentication methods like passwords or magic links. Even if the flow is incorrectly designed, there will be an error when signing in with any other authentication method in the flow. ![tenant settings enforce SSO](/assets/tenant-settings-enforce-sso.webp) You are also able to specify an set of login ID's in an **SSO exclusion list** that can bypass this SSO enforcement. This can be useful in cases when you may need a "break glass" account configured for a particular tenant. You can optionally set an SSO Exclusion List if you would like to specify login IDs that can bypass SSO authentication. This can be useful in cases when you may need a "break glass" account. Refer to [our Enforcing SSO guide](/flows/conditions/sso-enforced) to learn how to effectively use this setting in a flow. Enabling **SSO Enforced** without an SSO domain configured for the tenant means Descope can't route users to their SSO connection. To require an SSO domain on every tenant SSO connection in your project, add **SSO Domains** to [Mandatory User Attributes](/auth-methods/sso/settings#requiring-an-sso-domain) in the project-level SSO settings. #### Application Access You can optionally configure [Federated Applications](/identity-federation/applications) that users of the tenant will automatically be assigned/have access to. ![tenant settings details](/assets/tenant-settings-details-application-access.webp) ### Session Management If you're using Descope just for SSO, and not relying on Descope session management, this section doesn't apply to you. You can override the project-level [session management policy](/sessions/validation) for a tenant. By default the tenant inherits the project policy. Select **Custom** under **Session Management** to set a tenant-specific policy. Tenant admins can configure the same policy directly from the [Tenant Profile Widget](/widgets/admins#tenant-profile-widget), without needing Console access. If a user exists in multiple tenants, the stricter elements of each tenant's session management policy will be used for the issued tokens. ![tenant settings session management](/assets/tenant-settings-session-management.webp) #### Refresh Token Timeout This value sets the validity period for the Descope refresh token, after which the user will need to re-authenticate. #### Session Token Timeout This value sets the validity period for the Descope session/access token. The value needs to be _at least_ 3 minutes, and cannot be longer than the previously configured [refresh token timeout](#refresh-token-timeout). #### Session Inactivity You can enable session inactivity detection by checking the box for `Enable session inactivity detection`. Once enabled, you can configure the `Inactivity timeout` window with your desired configuration. If a session token is not refreshed within this amount of time, Descope will consider that session "idle", and invalidate it automatically. ### SSO Setup Suite Configuration You can choose for the tenant to inherit the project level SSO Setup Suite Configuration, or to configure the SSO Setup Suite for the individual tenant directly. For a full list of configurable SSO Setup Suite settings, refer to the [project-level settings doc](/auth-methods/sso/settings#sso-setup-suite-settings). #### SSO Configuration Link In this section, you can generate and revoke a link for the tenant's [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). You can also send the link, directly to a Tenant Admin from the Descope Console, by entering their email address in the `Recipient Email` field. ![tenant settings SSO configuration](/assets/tenant-level-s4-config.webp) #### Custom Settings If you want to customize the SSO Setup Suite experience for a specific tenant, you can select `Custom` under the **SSO Setup Suite Configurations** section. Within this section, you can configure the following settings: | Setting | Details | | ------- | --------| | SSO Setup Suite Features | You can choose which specific features you want to enabled for the SSO Setup Suite. The controllable options are: SSO Configuration, SAML, OIDC, SCIM Configuration, Role Mapping, FGA Mapping, SSO Domains, JIT. Removing SSO Configuration leaves a [SCIM-only suite](/auth-methods/sso/settings#scim-only-suite); SSO Configuration and SCIM Configuration cannot both be removed | | Styling | You can select a different [style](/auth-methods/sso/settings#styling) for the SSO Setup Suite. | ![tenant settings custom settings](/assets/tenant-settings-custom-settings-sso-setup-suite.webp) # Token Claims (/management/token) Manage token claims in Descope using JWT Templates and flow actions to shape JWT payloads for your applications and integrations. # Token Claims Management Token management in Descope includes controlling the claim payloads in JWTs that are issued to users and machines. In practice, this usually means configuring custom claims beyond standard claims like `sub`, `iss`, and `exp` so downstream services can make authorization and personalization decisions without extra backend calls. For step-by-step setup details, see the [Custom Claims flow action](/flows/actions/custom-claims) and [JWT Templates](/management/token/jwt-templates) docs. ## Where Claims Appear in Tokens When configured, custom claims can appear in every JWT Descope issues for the user or access key, including: - **Session tokens** — short-lived JWTs sent to your application on every request. These are either issued for a user or an M2M client (with an [access key](/management/m2m-access-keys), [federated app](/getting-started/oidc-endpoints), or [inbound app](/identity-federation/inbound-apps)). - **Refresh tokens** — used to issue new session tokens. Custom claims travel with them, so they count against cookie size limits. - **ID tokens** — issued when Descope is acting as an [OIDC provider](/getting-started/oidc-endpoints) with [Federated Applications](/identity-federation/applications), [Inbound Apps](/identity-federation/inbound-apps), or [Agentic Clients](/agentic-identity-hub/core-components/clients). ## Standard Token Claims Descope JWTs combine **standard JWT claims**, **Descope-specific claims**, and **custom claims** you configure. Which claims appear depends on the token type (user or M2M), your [JWT Template](/management/token/jwt-templates) authorization format, and whether the user or access key has tenants, roles, or impersonation active. Claims fall into three categories: | Category | Meaning | | --- | --- | | **Always present** | Included on every token of that type. Your application can rely on these being there. | | **Context-dependent** | Added when a specific condition applies — for example, the user belongs to tenants, is being impersonated, or the client SDK submitted claims. | | **Configuration-dependent** | Added only when you set them via a [JWT Template](/management/token/jwt-templates), [Custom Claims flow action](/flows/actions/custom-claims), or OIDC scope. | For claim categories and a full reference table, see [Standard Token Claims](#standard-token-claims) below. ### User Session and Refresh Tokens Session and refresh tokens share the same claim payload. Custom claims you configure are typically stored in the refresh token and copied into each new session token upon each refresh. | Claim | Category | Description | | --- | --- | --- | | `sub` | Always present | Subject — the Descope User ID. | | `iss` | Always present | Issuer — your Descope Project ID, or a full OIDC issuer URL when using an OIDC-compliant [JWT Template](/management/token/jwt-templates). | | `iat` | Always present | Issued-at time (UNIX epoch seconds). | | `exp` | Always present | Session token expiration time. See [Claims returned by the Client SDK](#claims-returned-by-the-client-sdk). | | `drn` | Always present | Descope Resource Name — where the token is stored. Typically `DS` for Descope-managed sessions. | | `amr` | Always present | Authentication Methods Reference — array of methods used to authenticate. See [`amr` values](#amr-authentication-methods-reference) below. | | `rexp` | Context-dependent | Refresh token expiration. Present when a refresh token lifetime applies. See [Claims returned by the Client SDK](#claims-returned-by-the-client-sdk). | | `dct` | Context-dependent | Descope Current Tenant — the user's active tenant ID. Auto-set when the user belongs to one tenant; set after [tenant selection](/flows/screens/inputs/tenantselect-component) in a flow. | | `tenants` | Context-dependent | Map of tenant IDs to nested `roles` and `permissions`. Present when the user has tenant associations and your JWT template uses the default authorization format. Omitted with **Current Tenant, No Tenant Reference** or **No Descope Claims** — see [Authorization Claims Configuration](/management/token/jwt-templates#authorization-claims-configuration). | | `roles` | Context-dependent | Project-level roles, or tenant-scoped roles at the JWT root when using **Current Tenant, No Tenant Reference**. | | `permissions` | Context-dependent | Project-level permissions, or tenant-scoped permissions at the JWT root when using **Current Tenant, No Tenant Reference**. | | `act` | Context-dependent | Actor — present during [user impersonation](/user-impersonation). `act.sub` holds the impersonator's User ID; `sub` is the impersonated user. | | `nsec` | Context-dependent | Non-secure claims added by the [client SDK or API](/getting-started). **Do not trust** these on your backend — see [Secure vs non-secured custom claims](/security-best-practices/custom-claims#secure-vs-non-secured-custom-claims). | | `aud` | Configuration-dependent | Audience — intended recipients of the token. Set via JWT Template or Custom Claims action. | | `azp` | Configuration-dependent | Authorized party — added when Descope acts as an [OIDC provider](/getting-started/oidc-endpoints). See [Additional standard claims](/security-best-practices/custom-claims#additional-standard-claims). | | `dci` | Configuration-dependent | Descope Consent ID - an internal ID for the consent granted by the user. Only applicable for [Inbound Apps](/identity-federation/inbound-apps) and [Agentic Clients](/agentic-identity-hub/core-components/clients). | | `jti` | Configuration-dependent | JWT ID — a unique identifier for the token, useful for tracking individual tokens and preventing replay attacks. Added when **Add JTI Claim** is enabled on the [JWT Template](/management/token/jwt-templates#jwt-id-jti-claim). | | Custom claims | Configuration-dependent | Any application-specific keys you define via JWT Template or Custom Claims flow action. Server-set custom claims appear at the JWT root; client-set claims appear under `nsec`. | In the `tenants` claim, parent and [sub-tenant](/management/tenant-management/sub-tenants#jwt-structure) IDs sit at the same level. The claim does not encode hierarchy. #### `amr` (Authentication Methods Reference) The `amr` claim is an array of strings indicating which authentication methods were used for the session: | Value | Meaning | | --- | --- | | `oauth` | OAuth social login | | `email` | OTP, Magic Link, or Enchanted Link via email | | `sms` | OTP or Magic Link via SMS | | `whatsapp` | WhatsApp nOTP | | `webauthn` | Passkeys | | `totp` | Authenticator app (TOTP) | | `fed` | SSO / federated login (default for SSO) | | `pwd` | Password | | `mfa` | Two or more distinct authentication methods | You can [override `amr`](/security-best-practices/custom-claims#overriding-the-amr-claim) when your SSO provider includes specific authentication methods in its OIDC token. #### Claims Returned by the Client SDK Your frontend can read a **subset** of the claims above without decoding the session token itself. The Descope Client SDKs expose them as a plain `claims` object: | SDK | How to read `claims` | | --- | --- | | React, Next.js | `const { claims } = useSession()` — see [Auth Helpers](/client-sdk/auth-helpers#manage-session) | | Vue | `const { claims } = useSession()` (a ref — read `claims.value`) | | Angular | `DescopeAuthService.session$` → `session.claims` | | Web JS, HTML | `sdk.onClaimsChange((newClaims, oldClaims) => { ... })` | | Any SDK holding a raw auth or refresh response | the `claims` field on the returned JWT response object | For the minimum SDK version that returns this object, see [Auth Helpers](/client-sdk/auth-helpers#manage-session). A `claims` object for a user with two custom claims (`organization` and `role`) looks like this: ```json { "amr": ["pwd"], "dct": "T2U7vUH1NPy4JzWHruoOVIGyzYlu", "exp": 1753401600, "rexp": "2025-08-25T09:20:00Z", "organization": "acme", "role": "USER" } ``` For code that reads these values and warns a user before their session ends, see [Reading the expiration time](/sessions/management/web#reading-the-expiration-time). ### Access Key JWTs When an [M2M Access Key](/management/m2m-access-keys) is exchanged for a JWT, or a client secret is used with `client_credentials` flow with a [Federated Application](/getting-started/oidc-endpoints) or [Inbound App](/identity-federation/inbound-apps), the token follows the same general structure as a user session token, with a few differences: | Claim | Category | Description | | --- | --- | --- | | `sub` | Always present | Subject — the Access Key ID (not a user ID). | | `iss` | Always present | Issuer — your Descope Project ID. | | `iat` | Always present | Issued-at time (UNIX epoch seconds). | | `exp` | Always present | Token expiration time (UNIX epoch seconds). | | `drn` | Always present | Descope Resource Name — typically `DS`. | | `tenants` | Context-dependent | Tenants and authorization configured on the access key. Same nesting rules as user tokens. | | `roles` / `permissions` | Context-dependent | Authorization on the access key, at the root or nested under `tenants` depending on your [Access Key JWT Template](/management/token/jwt-templates#access-key-jwt-templates). | | `nsec` | Context-dependent | Claims submitted by the M2M client during [key exchange](/management/m2m-access-keys#custom-claims). Untrusted — use a JWT Template instead for server-verified claims. | | `jti` | Configuration-dependent | JWT ID — a unique identifier for the token. To include this in your JWTs, you must enable the **Add JTI Claim** option under [JWT Templates](/management/token/jwt-templates#jti-claim). | | Custom claims | Configuration-dependent | Claims from an Access Key JWT Template are inserted at the JWT root (not under `nsec`). | User session claims like `amr`, `dct`, and `act` do not apply to access key JWTs. ### OIDC ID tokens When Descope acts as an [OIDC provider](/getting-started/oidc-endpoints), ID tokens follow the OIDC specification. Standard claims are always present; profile, authorization, and custom claims require the corresponding scope. | Claim | Category | Description | | --- | --- | --- | | `iss` | Always present | Issuer — `https://api.descope.com/{projectId}`. | | `sub` | Always present | Subject — the Descope User ID. | | `aud` | Always present | Audience — the OIDC application Client ID. | | `iat` | Always present | Issued-at time. | | `exp` | Always present | Expiration time. | | `name`, `email`, `email_verified`, `picture`, `phone_number`, `phone_number_verified`, `given_name`, `family_name` | Configuration-dependent | Standard OIDC profile claims — included when the client requests the matching scope (`profile`, `email`, `phone`). See [OIDC Applications](/identity-federation/applications/oidc-apps#scopes). | | `tenants` | Configuration-dependent | User's tenants, roles, and permissions — included when the client requests the `descope.claims` scope. | | Custom claims | Configuration-dependent | Application-specific claims — included when the client requests the `descope.custom_claims` scope and claims are configured via JWT Template or Custom Claims action. | | `azp` | Configuration-dependent | Authorized party — the Client ID that requested the token. | | `jti` | Configuration-dependent | JWT ID — a unique identifier for the token. Added when **Add JTI Claim** is enabled on the [JWT Template](/management/token/jwt-templates#jwt-id-jti-claim). | ## Two Ways to Configure Claims You can manage JWT Templates as code with our infrastructure as code (IaC) providers, such as [Terraform](/management/token/jwt-templates#managing-jwt-templates-via-terraform). There are two places you can configure claims in Descope: | Method | Scope | When to use | | --- | --- | --- | | [JWT Template](/management/token/jwt-templates) (preferred) | Project-wide, code-manageable | Claims that should appear on every JWT issued by a project, application, or [Inbound App](/identity-federation/inbound-apps). Best for consistent token claims. | | [Custom Claims flow action](/flows/actions/custom-claims) | Per-flow | Claims that depend on flow context — conditional logic, [connector responses](/connectors/connectors-in-flows), step-up authentication, or values collected from the user during the flow. | It's worth noting that you can use both together. If a claim is set in both places, the `Custom Claims` flow action wins and overrides the JWT Template value for that flow. ## Supported Claim Value Types Custom claim values can be: - **Strings, booleans, or numbers** — simple, flat values. - **Dynamic values** — pulled from Descope user attributes, tenant attributes, or other available context. These automatically refresh when the underlying value changes (see [Dynamic Claim Updates](#dynamic-claim-updates) below). - **JSON objects** — for grouping related fields under a single claim key (see [Nested JSON Claims](#nested-json-claims) below). Both the JWT Template editor and the Custom Claims flow action support a **simple mode** for flat key/value entry and an **advanced mode** for defining the claim payload as a JSON object. ## Nested JSON Claims This is useful when downstream consumers expect related data grouped under a single key (for example, gateways like [Hasura](/management/token/jwt-templates) or external authorization systems that read structured claim objects). You can use a JSON object as the value of a custom claim. ![Example of a custom claims action with nested JSON](/assets/custom-claims-nested-json.webp) In advanced mode with [JWT Templates](/management/token/jwt-templates), you can set the claim key to a JSON object instead of a flat value: ```json { "my_json_custom_claim": { "hello": "I am a custom claim", "that_has": "Nested Json" } } ``` The object is embedded directly into the issued JWT under the specified key: ```json { "amr": ["oauth"], "drn": "DS", "exp": 1776282239, "iat": 1776281639, "iss": "P32jk5Nq29jcuXmAz56iGq5uRb7m", "my_json_custom_claim": { "hello": "I am a custom claim", "that_has": "Nested Json" }, "sub": "U3B57mYzHbJY8HFvjHvhRkTPq5xr" } ``` You can nest objects multiple levels deep, mix object and scalar values within the same template, and combine static fields with dynamic user-attribute references inside the same nested structure. ## Dynamic Claim Updates Claim values backed by a Descope user or tenant attribute update automatically whenever the underlying attribute changes and the user's session is refreshed. You don't need to re-issue tokens manually. This is useful for: - Real-time access control — role or permission changes take effect on the next refresh. - Reflecting up-to-date profile data (display name, locale, preferences) without an extra API call. - Multi-tenant context — `tenant_id`, `tenant.name`, or tenant-specific attributes that shift when a user switches tenants. Dynamic values can be used inside both flat and [nested](#nested-json-claims) claim definitions. ## Claim Limits Each key can have a maximum of **60 characters**, each claim value can have a maximum of **500 characters**, and each JWT can have a maximum of **100 keys**. These limits apply across both the JWT Template and the Custom Claims flow action. Because Descope stores custom claims in the refresh token (typically held in a cookie), large or numerous claims also contribute to your overall cookie size — see [JWT Claims security best practices](/security-best-practices/custom-claims#cookie-size) for guidance. ## Security Considerations Custom claims are read by every service that consumes the JWT. A few things to keep in mind: - **Don't store sensitive data:** JWTs are Base64-encoded, not encrypted. Treat anything in a claim as visible to anyone who holds the token. - **Trust only server-set claims:** Claims added by a [client SDK](/getting-started) are placed under an `nsec` claim and should not be trusted by your backend. Claims set via JWT Templates or the Custom Claims flow action (server-side) are trusted. - **Override standard claims carefully:** You can override claims like `aud` or [`amr`](/security-best-practices/custom-claims#overriding-the-amr-claim), but doing so has implications for token validation and audit trails. For full guidance, see [JWT Claims security best practices](/security-best-practices/custom-claims). # JWT Templates (/management/token/jwt-templates) Learn how to utilize user and access key JWT Templates within your Descope project. # JWT Templates ## Overview JSON Web Tokens (JWTs) are used for authentication and integration with various tools. Different tools have unique JWT formats, and Descope provides **JWT Templates** to help you customize your JWTs to match these formats seamlessly. ### Managing JWTs in Descope There are two primary ways to manage JWTs in Descope: 1. **JWT Templates (Preferred Method)** - JWT Templates allow for easy, code-manageable customization. - They are applied project-wide, making them the ideal solution for consistent JWT formatting. 2. **Custom Claims in Flows** - For flow-specific claims or claims related to scenarios like step-up authentication, use a **custom claims action** in a flow. - Learn more about custom claims actions [here](/flows/actions/custom-claims). This doc explains how to set up each of the different types of JWT templates (User and Access Keys) along with different features of JWT templates, and how you manage them via Terraform. ## User JWT Templates User JWT Templates define the structure of JWTs issued to authenticated users. Descope offers default templates for popular tools and allows custom configurations to streamline integration. ### Adding a User JWT Template 1. Navigate to **Project Settings > JWT Templates** in the [Descope Console](https://app.descope.com/settings/project/jwt). 2. Click **+ JWT Template** at the top right. 3. Select a **User JWT** template (filterable via the `User JWT` category). ![Add a User JWT template](/assets/user-jwt-template-add.webp) ### Configuring a User JWT Template Once added, customize the template to fit your integration needs. Below is an example of configurations available for the **Default OIDC compliant User JWT** template. ![Default User JWT Template](/assets/user-jwt-template-default.webp) #### General Settings - **Template Name** - Custom name for the template. - **Template Description** - Description for internal reference. #### Authorization Claims Configuration This section determines how authorization-related claims are structured: - **Default Descope JWT** - Project-level roles and permissions in the root, tenant-specific roles inside each tenant object. - **Current Tenant, No Tenant Reference** - Tenant-specific roles appear in the root, omitting the `tenants` claim. - **No Descope Claims** - Excludes Descope's default claims, only including custom claims. - **Set active tenant claim automatically** - When a user is associated with a single tenant, the tenant will be set as the user's active tenant, using the `dct` (Descope Current Tenant) claim in their JWT. This is required when using `tenant.name` or tenant attributes as custom claims. - **Exclude permissions claim** - When enabled, permissions will not be included in the JWT token. This is useful when a user has so many permissions that the JWT is too long to fit in cookies. #### Custom Claims Each key can have a maximum of **60 characters**, each claim value can have a maximum of **500 characters**, and each JWT can have a maximum of **100 keys**. - You can add **string, boolean, numerical, or dynamic values**. - Advanced mode allows defining claims using a **JSON object**. ![Custom Claims - Simple Mode](/assets/user-jwt-template-custom-claims-simple.webp) ![Custom Claims - Advanced Mode](/assets/user-jwt-template-custom-claims-advanced.webp) #### JWT ID (JTI) Claim The **Add JTI Claim** option adds a unique JWT ID (`jti` claim) to every token issued from this template, useful for tracking individual tokens and preventing replay attacks. ![Custom Claims - Add JTI Claim](/assets/jwt-template-jti-claim.webp) #### Setting the Project's User Token Format 1. Navigate to **Project Settings > Session Management**. 2. Select the preferred **User JWT Token Format**. 3. Click **Save**. ![Set User JWT Format](/assets/user-jwt-template-set-project-token-format.webp) If you need the public keys of your JWTs, you can find them at the URL underneath of **Security** in your [Project Settings](https://app.descope.com/settings/project). ![Public JWK URL](/assets/public-key-url-project-settings.webp) #### Testing the User JWT Template Once configured, you can see the new JWT via the flow runner, under your flow editor page in the Descope Console, or through [Descope Explorer](https://explorer.descope.com/). ## Access Key JWT Templates Access Key JWT Templates define the structure of JWTs issued when an **Access Key** is exchanged for a session. ### Adding an Access Key JWT Template 1. Navigate to **Project Settings > JWT Templates** in the [Descope Console](https://app.descope.com/settings/project/jwt). 2. Click **+ JWT Template** at the top right. 3. Select an **Access Key JWT** template (filterable via the `Access Keys JWT` category). ![Add Access Key JWT Template](/assets/access-key-jwt-template-add.webp) ### Configuring an Access Key JWT Template After adding a template, configure its settings. Below is an example of the **MongoDB Access Key JWT Template**. ![MongoDB Access Key JWT Template](/assets/access-key-jwt-template-mongo.webp) #### General Settings - **Template Name** - Custom name for the template. - **Template Description** - Description for internal reference. #### Authorization Claims Configuration Same options as **User JWT Templates**: - **Default Descope JWT** - **Current Tenant, No Tenant Reference** - **No Descope Claims** - **Set active tenant claim automatically** - **Exclude permissions claim** #### Custom Claims Each key can have a maximum of **60 characters**, each claim value can have a maximum of **500 characters**, and each JWT can have a maximum of **100 keys**. - You can add **string, boolean, numerical, or dynamic values**. - Advanced mode allows defining claims using a **JSON object**. Start typing `{{` in the editor to see the available dynamic values. ![Access Key JWT Template - Custom Claims Advanced Mode](/assets/access-key-jwt-template-custom-claims-advanced.webp) A `user.*` dynamic value, such as `{{user.userId}}`, only resolves when the access key being exchanged is bound to a user. Access keys created directly from the [Access Keys tab](https://app.descope.com/accessKeys) or the management API aren't bound to a user by default. See [Associating Access Key to Users](/management/m2m-access-keys#associating-access-key-to-users) for how to bind one. #### JTI Claim - **Add JTI Claim** - Adds a unique JWT ID (`jti` claim) to every token issued from this template, useful for tracking individual tokens and preventing replay attacks. #### Setting the Project's Access Key Token Format 1. Navigate to **Project Settings > Session Management**. 2. Select the preferred **Access Key JWT Token Format**. 3. Click **Save**. ![Set Access Key JWT Format](/assets/access-key-jwt-template-set-project-token-format.webp) If you need the public keys of your JWTs, you can find them at the URL underneath of **Security** in your [Project Settings](https://app.descope.com/settings/project). ![Public JWK URL](/assets/public-key-url-project-settings.webp) #### Testing the Access Key JWT Template You can test Access Key JWTs using any Descope SDK or [API](/api/access-keys/exchange-key), as well as [client credentials flow](/getting-started/oidc-endpoints#client-credentials-flow) to exchange an [Access Key](/management/m2m-access-keys) for a session token. --- Now that you've set up your JWT templates, you can ensure that your JWTs integrate smoothly with your application by customizing them to meet your specific requirements. ## Applying JWT Templates to Inbound Apps You can apply JWT templates to specific [Inbound Apps](/identity-federation/inbound-apps) by configuring the JWT template in the **Token Format** field of the Session Management section of your [Inbound App Settings](/identity-federation/inbound-apps/creating-inbound-apps#token-format). ![Apply JWT Template to Inbound App](/assets/apply-jwt-template-to-inbound-app.webp) ## Empty Claims Policy If custom claims you've set don't have a defined value to present, you can define how these **empty** claims will appear in your JWT: - **Default (Leave as Is)** - Claims with no values appear as empty strings (`""`). - **Null** - Claims with no values are explicitly set to `null`. - **Empty** - Claims with no values are omitted from the JWT. A common example of this is when you set a custom claim to the value of a tenant custom attribute, but there's no current tenant selected in the flow to take the custom attribute from. ## Dynamic Claims with JWT Templates Any custom claims added via a Custom Claim action in a flow, will override what the JWT template is configured to set. Claims in JWT templates can be dynamically updated based on real-time changes to user attributes or other relevant data. When a custom attribute is used as a claim in a JWT, its value will automatically update whenever the user's session is refreshed, provided the attribute has changed. This ensures that JWTs always reflect the most current state of user information without requiring manual intervention or additional API calls. This behavior is especially useful for enforcing real-time access control, adapting user permissions dynamically, and ensuring that dependent services always receive the latest claim data. ## Managing JWT Templates via Terraform You can configure JWT Templates using **Terraform**. Below is an example JWT template managed via our Terraform provider. Example Terraform Configuration: ```hcl resource "descope_jwt_template" "example" { name = "MyJWTTemplate" description = "Custom JWT for my app" empty_claim_policy = "nil", auth_schema = "default" enforce_issuer = true template = "{\"aud\":\"custom-aud\",\"custom_claim\":\"hellothere\"}" } ``` ### Available Attributes: - **name** (`string`, required) - JWT template name. - **description** (`string`) - Template description. - **auth_schema** (`string`, default: `"default"`) - Defines authorization claims format. - **enforce_issuer** (`bool`) - Enforce JWT issuer validation. - **template** (`string`, required) - JSON schema for the JWT template. For more details, visit our [Terraform documentation](/managing-environments/terraform). By following this guide, you can effectively configure JWT templates to meet your application's needs. # JWTs with SDKs (/management/token/sdks) Learn how to easily implement jwt management for your app with Descope using the Descope backend SDKs. # JWTs with SDKs You can use the Descope management SDK for JWT operations like adding custom claims, generating session tokens for users, et cetera. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ```sh title="Terminal" composer require descope/descope-php ``` ```sh title="Terminal" dotnet add package descope ``` ### Import and Initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope client you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key="xxxx") except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ```php require 'vendor/autoload.php'; use Descope\SDK\DescopeSDK; $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'] ]); ``` ```csharp // appsettings.json { "Descope": { "ProjectId": "your-project-id", "ManagementKey": "your-management-key" } } // Program.cs using Descope; using Microsoft.Extensions.Configuration; // ... In your setup code var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); var descopeProjectId = config["Descope:ProjectId"]; var descopeManagementKey = config["Descope:ManagementKey"]; var descopeConfig = new DescopeConfig(projectId: descopeProjectId); var descopeClient = new DescopeClient(descopeConfig) { ManagementKey = descopeManagementKey, }; ``` ### Update JWT With Custom Claims Learn more about configuring custom token claims in our [Token Claims Management docs](https://docs.descope.com/management/token). This operation updates a valid JWT with the custom claims you provide. You can optionally set how long the updated JWT remains valid using `refreshDuration` (in seconds). The new JWT will be returned. ```javascript // Args: // jwt (string): The JWT to update (required). // customClaims (object): Optional, custom claims to add to the JWT // refreshDuration (number): Optional, duration in seconds for which the new JWT will be valid const jwt = "original-jwt" const customClaims = { "custom-key1": "custom-value1", "custom-key2": "custom-value2", } const refreshDuration = 3600 const resp = await descopeClient.management.jwt.update(jwt, customClaims, refreshDuration) if (!resp.ok) { console.log("Failed to update JWT") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated JWT") console.log(resp.data.jwt) } ``` ```python # Args: # jwt (str): The JWT to update (required). # custom_claims (dict): Custom claims to add to the JWT # refresh_duration (int): Optional, duration in seconds for which the new JWT will be valid try: updated_jwt = descope_client.mgmt.jwt.update_jwt( jwt="original-jwt", custom_claims={ "custom-key1": "custom-value1", "custom-key2": "custom-value2", }, refresh_duration=3600, ) print("Successfully updated JWT.") print(updated_jwt) except AuthException as error: print("Unable to update JWT.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // jwt (string): The JWT to update (required). jwt := "original-jwt" // customClaims (map[string]interface{}): Optional, custom claims to add to the JWT customClaims := map[string]interface{}{ "custom-key1": "custom-value1", "custom-key2": "custom-value2", } // refreshDuration (int32): Optional, duration in seconds for which the new JWT will be valid refreshDuration := int32(3600) updatedJWT, err := descopeClient.Management.JWT().UpdateJWTWithCustomClaims(ctx, jwt, customClaims, refreshDuration) if err != nil { fmt.Println("Unable to update JWT.", err) } else { fmt.Println("Successfully updated JWT.") fmt.Println(updatedJWT) } ``` ```java // Args: // jwt (String): The JWT to update (required). // customClaims (Map): Custom claims to add to the JWT JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { String updatedJwt = jwts.updateJWTWithCustomClaims("original-jwt", new HashMap() {{ put("custom-key1", "custom-value1"); put("custom-key2", "custom-value2"); }}).getJwt(); System.out.println("Successfully updated JWT."); System.out.println(updatedJwt); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # jwt (String): The JWT to update (required). # custom_claims (Hash): Custom claims to add to the JWT updated_jwt = descope_client.update_jwt( jwt: 'original-jwt', custom_claims: { 'custom-key1': 'custom-value1', 'custom-key2': 'custom-value2', }, ) puts "Successfully updated JWT." puts updated_jwt ``` ```csharp // Args: // Jwt (string): The JWT to update (required). // CustomClaims (UpdateJWTRequest_customClaims): Custom claims to add to the JWT // RefreshDuration (int?): Optional, duration in seconds for which the new JWT will be valid var customClaims = new UpdateJWTRequest_customClaims(); customClaims.AdditionalData["custom-key1"] = "custom-value1"; customClaims.AdditionalData["custom-key2"] = "custom-value2"; var updateJwtRequest = new UpdateJWTRequest { Jwt = "original-jwt", CustomClaims = customClaims, RefreshDuration = 3600, }; var updateJwtResponse = await descopeClient.Mgmt.V1.Jwt.Update.PostAsync(updateJwtRequest); var updatedJwt = updateJwtResponse?.Jwt; Console.WriteLine("Successfully updated JWT."); Console.WriteLine(updatedJwt); ``` ### Generate JWT for Generic Auth Sign In This operation is only for Sign-In, and if the login ID doesn't already exist it will return an error and ask you to sign up the user first. This operation programmatically mints a Descope session (session and refresh JWTs) for an existing user (identified by `loginID`) independent of a specific auth method. The result is equivalent to that produced by a successful `SignIn` operation. Use this if you verify identity elsewhere and want Descope tokens for an existing user, if you need backend-initiated sessions, or to generate valid tokens for automated testing. This requires a management key and must only be called from trusted server-side code. You can also perform this operation through the [Generate JWT for Sign-In Management API](/api/management/generic-auth/generate-jwt-sign-in). ```javascript // Args: // loginId (string): The login ID of the existing user to sign in (required). // loginOptions (object): Optional, options to customize the generated session (custom claims, refreshDuration, etc.) const loginId = "user@example.com" const loginOptions = { customClaims: { "custom-key1": "custom-value1", }, refreshDuration: 3600, // Optional, duration in seconds for which the session will be valid } const resp = await descopeClient.management.jwt.signIn(loginId, loginOptions) if (!resp.ok) { console.log("Failed to generate JWT for sign in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully generated JWT for sign in") console.log(resp.data.sessionJwt) console.log(resp.data.refreshJwt) } ``` ```python from descope.management.common import MgmtLoginOptions # Args: # login_id (str): The login ID of the existing user to sign in (required). # login_options (MgmtLoginOptions): Optional, options to customize the generated session try: jwt_response = descope_client.mgmt.jwt.sign_in( login_id="user@example.com", login_options=MgmtLoginOptions( custom_claims={"custom-key1": "custom-value1"}, refresh_duration=3600, ), ) print("Successfully generated JWT for sign in.") print(jwt_response) except AuthException as error: print("Unable to generate JWT for sign in.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context. ctx := context.Background() // loginID (string): The login ID of the existing user to sign in (required). loginID := "user@example.com" // loginOptions (*descope.MgmLoginOptions): Optional, options to customize the generated session loginOptions := &descope.MgmLoginOptions{ CustomClaims: map[string]any{ "custom-key1": "custom-value1", }, RefreshDuration: int32(3600), } authInfo, err := descopeClient.Management.JWT().SignIn(ctx, loginID, loginOptions) if err != nil { fmt.Println("Unable to generate JWT for sign in.", err) } else { fmt.Println("Successfully generated JWT for sign in.") fmt.Println(authInfo.SessionToken.JWT) fmt.Println(authInfo.RefreshToken.JWT) } ``` ```java // Args: // loginId (String): The login ID of the existing user to sign in (required). // loginOptions (LoginOptions): Optional, options to customize the generated session JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { LoginOptions loginOptions = LoginOptions.builder() .customClaims(new HashMap() {{ put("custom-key1", "custom-value1"); }}) .build(); AuthenticationInfo authInfo = jwts.signIn("user@example.com", loginOptions); System.out.println("Successfully generated JWT for sign in."); System.out.println(authInfo.getToken().getJwt()); System.out.println(authInfo.getRefreshToken().getJwt()); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID of the existing user to sign in (required). // CustomClaims (GenerateJWTSignInRequest_customClaims): Optional, custom claims to add to the session JWT // RefreshDuration (int?): Optional, duration in seconds for which the session will be valid var customClaims = new GenerateJWTSignInRequest_customClaims(); customClaims.AdditionalData["custom-key1"] = "custom-value1"; var signInRequest = new GenerateJWTSignInRequest { LoginId = "user@example.com", CustomClaims = customClaims, RefreshDuration = 3600, }; var signInResponse = await descopeClient.Mgmt.V1.Auth.Signin.PostAsync(signInRequest); Console.WriteLine("Successfully generated JWT for sign in."); Console.WriteLine(signInResponse?.SessionJwt); Console.WriteLine(signInResponse?.RefreshJwt); ``` ### Generate JWT for Generic Auth Sign Up This operation is only for Sign-Up, and if the login ID already exists it will return an error and ask you to sign in the user. This operation programmatically mints a Descope session (session and refresh JWTs) for a new user independent of a specific auth method. The result is equivalent to that produced by a successful `SignUp` operation. This operation is especially useful for backend-driven user migrations, where you have already collected user details from another system and want to seamlessly onboard users into Descope. It allows you to validate the user’s session and issue a Descope token in a single step. This requires a management key and must only be called from trusted server-side code. You can also perform this operation through the [Generate JWT for Sign-Up Management API](/api/management/generic-auth/generate-jwt-sign-up). ```javascript // Args: // loginId (string): The login ID of the user to create (required). // user (object): Optional, user details such as email, phone, and name. // signUpOptions (object): Optional, options to customize the generated session (custom claims, refreshDuration, etc.) const loginId = "user@example.com" const user = { email: "user@example.com", phone: "+15551234567", name: "Jane Doe", } const signUpOptions = { customClaims: { "custom-key1": "custom-value1", }, refreshDuration: 3600, // Optional, duration in seconds for which the session will be valid } const resp = await descopeClient.management.jwt.signUp(loginId, user, signUpOptions) if (!resp.ok) { console.log("Failed to generate JWT for sign up") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully generated JWT for sign up") console.log(resp.data.sessionJwt) console.log(resp.data.refreshJwt) } ``` ```python from descope.management.common import MgmtUserRequest, MgmtSignUpOptions # Args: # login_id (str): The login ID of the user to create (required). # user (MgmtUserRequest): Optional, user details such as email, phone, and name. # signup_options (MgmtSignUpOptions): Optional, options to customize the generated session try: jwt_response = descope_client.mgmt.jwt.sign_up( login_id="user@example.com", user=MgmtUserRequest( name="Jane Doe", email="user@example.com", phone="+15551234567", ), signup_options=MgmtSignUpOptions( custom_claims={"custom-key1": "custom-value1"}, refresh_duration=3600, ), ) print("Successfully generated JWT for sign up.") print(jwt_response) except AuthException as error: print("Unable to generate JWT for sign up.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context. ctx := context.Background() // loginID (string): The login ID of the user to create (required). loginID := "user@example.com" // user (*descope.MgmtUserRequest): Optional, user details such as email, phone, and name. user := &descope.MgmtUserRequest{ User: descope.User{ Name: "Jane Doe", Email: "user@example.com", Phone: "+15551234567", }, } // signUpOptions (*descope.MgmSignUpOptions): Optional, options to customize the generated session signUpOptions := &descope.MgmSignUpOptions{ CustomClaims: map[string]any{ "custom-key1": "custom-value1", }, RefreshDuration: int32(3600), } authInfo, err := descopeClient.Management.JWT().SignUp(ctx, loginID, user, signUpOptions) if err != nil { fmt.Println("Unable to generate JWT for sign up.", err) } else { fmt.Println("Successfully generated JWT for sign up.") fmt.Println(authInfo.SessionToken.JWT) fmt.Println(authInfo.RefreshToken.JWT) } ``` ```java // Args: // loginId (String): The login ID of the user to create (required). // signUpUserDetails (MgmtSignUpUser): Optional, user details and session options. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { MgmtSignUpUser signUpUser = MgmtSignUpUser.builder() .user(User.builder() .name("Jane Doe") .email("user@example.com") .phone("+15551234567") .build()) .customClaims(new HashMap() {{ put("custom-key1", "custom-value1"); }}) .build(); AuthenticationInfo authInfo = jwts.signUp("user@example.com", signUpUser); System.out.println("Successfully generated JWT for sign up."); System.out.println(authInfo.getToken().getJwt()); System.out.println(authInfo.getRefreshToken().getJwt()); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID of the user to create (required). // User (SignUpUser): Optional, user details such as email, phone, and name. // CustomClaims (GenerateJWTSignUpRequest_customClaims): Optional, custom claims to add to the session JWT // RefreshDuration (int?): Optional, duration in seconds for which the session will be valid var customClaims = new GenerateJWTSignUpRequest_customClaims(); customClaims.AdditionalData["custom-key1"] = "custom-value1"; var signUpRequest = new GenerateJWTSignUpRequest { LoginId = "user@example.com", User = new SignUpUser { Email = "user@example.com", Phone = "+15551234567", Name = "Jane Doe", }, CustomClaims = customClaims, RefreshDuration = 3600, }; var signUpResponse = await descopeClient.Mgmt.V1.Auth.Signup.PostAsync(signUpRequest); Console.WriteLine("Successfully generated JWT for sign up."); Console.WriteLine(signUpResponse?.SessionJwt); Console.WriteLine(signUpResponse?.RefreshJwt); ``` ### Generate JWT for Generic Auth Sign Up or In This operation programmatically mints a Descope session (session and refresh JWTs) for a user identified by `loginID`, creating the user if they do not exist or signing in an existing user if they do, independent of a specific auth method. The result is equivalent to that produced by a successful `SignUpOrIn` operation. Use this when you want a single backend code path that works for both first-time and returning users. This requires a management key and must only be called from trusted server-side code. You can also perform this operation through the [Generate JWT for Sign-Up or Sign-In Management API](/api/management/generic-auth/generate-jwt-sign-up-or-in). ```javascript // Args: // loginId (string): The login ID of the user to sign up or in (required). // user (object): Optional, user details such as email, phone, and name (used only if the user is created). // signUpOptions (object): Optional, options to customize the generated session (custom claims, refreshDuration, etc.) const loginId = "user@example.com" const user = { email: "user@example.com", phone: "+15551234567", name: "Jane Doe", } const signUpOptions = { customClaims: { "custom-key1": "custom-value1", }, refreshDuration: 3600, // Optional, duration in seconds for which the session will be valid } const resp = await descopeClient.management.jwt.signUpOrIn(loginId, user, signUpOptions) if (!resp.ok) { console.log("Failed to generate JWT for sign up or in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully generated JWT for sign up or in") console.log(resp.data.sessionJwt) console.log(resp.data.refreshJwt) } ``` ```python from descope.management.common import MgmtUserRequest, MgmtSignUpOptions # Args: # login_id (str): The login ID of the user to sign up or in (required). # user (MgmtUserRequest): Optional, user details (used only if the user is created). # signup_options (MgmtSignUpOptions): Optional, options to customize the generated session try: jwt_response = descope_client.mgmt.jwt.sign_up_or_in( login_id="user@example.com", user=MgmtUserRequest( name="Jane Doe", email="user@example.com", phone="+15551234567", ), signup_options=MgmtSignUpOptions( custom_claims={"custom-key1": "custom-value1"}, refresh_duration=3600, ), ) print("Successfully generated JWT for sign up or in.") print(jwt_response) except AuthException as error: print("Unable to generate JWT for sign up or in.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context. ctx := context.Background() // loginID (string): The login ID of the user to sign up or in (required). loginID := "user@example.com" // user (*descope.MgmtUserRequest): Optional, user details (used only if the user is created). user := &descope.MgmtUserRequest{ User: descope.User{ Name: "Jane Doe", Email: "user@example.com", Phone: "+15551234567", }, } // signUpOptions (*descope.MgmSignUpOptions): Optional, options to customize the generated session signUpOptions := &descope.MgmSignUpOptions{ CustomClaims: map[string]any{ "custom-key1": "custom-value1", }, RefreshDuration: int32(3600), } authInfo, err := descopeClient.Management.JWT().SignUpOrIn(ctx, loginID, user, signUpOptions) if err != nil { fmt.Println("Unable to generate JWT for sign up or in.", err) } else { fmt.Println("Successfully generated JWT for sign up or in.") fmt.Println(authInfo.SessionToken.JWT) fmt.Println(authInfo.RefreshToken.JWT) } ``` ```java // Args: // loginId (String): The login ID of the user to sign up or in (required). // signUpUserDetails (MgmtSignUpUser): Optional, user details and session options. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { MgmtSignUpUser signUpUser = MgmtSignUpUser.builder() .user(User.builder() .name("Jane Doe") .email("user@example.com") .phone("+15551234567") .build()) .customClaims(new HashMap() {{ put("custom-key1", "custom-value1"); }}) .build(); AuthenticationInfo authInfo = jwts.signUpOrIn("user@example.com", signUpUser); System.out.println("Successfully generated JWT for sign up or in."); System.out.println(authInfo.getToken().getJwt()); System.out.println(authInfo.getRefreshToken().getJwt()); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID of the user to sign up or in (required). // User (SignUpUser): Optional, user details (used only if the user is created). // CustomClaims (GenerateJWTSignUpRequest_customClaims): Optional, custom claims to add to the session JWT // RefreshDuration (int?): Optional, duration in seconds for which the session will be valid var customClaims = new GenerateJWTSignUpRequest_customClaims(); customClaims.AdditionalData["custom-key1"] = "custom-value1"; var signUpOrInRequest = new GenerateJWTSignUpRequest { LoginId = "user@example.com", User = new SignUpUser { Email = "user@example.com", Phone = "+15551234567", Name = "Jane Doe", }, CustomClaims = customClaims, RefreshDuration = 3600, }; var signUpOrInResponse = await descopeClient.Mgmt.V1.Auth.SignupIn.PostAsync(signUpOrInRequest); Console.WriteLine("Successfully generated JWT for sign up or in."); Console.WriteLine(signUpOrInResponse?.SessionJwt); Console.WriteLine(signUpOrInResponse?.RefreshJwt); ``` ### Generate Client Assertion JWT for OAuth You can learn more about Private Key JWT authentication [here](https://docs.descope.com/auth-methods/oauth/providers/custom-providers#private-key-jwt). This operation mints a short-lived, signed client assertion JWT that your application can use to authenticate itself to an OAuth 2.0 authorization server (for example, in the `private_key_jwt` client authentication method or the client credentials flow). Instead of sending a static client secret, your backend presents this signed JWT as proof of the client's identity. This requires a management key and must only be called from trusted server-side code. ```javascript // Args: // issuer (string): The issuer of the JWT, typically the client ID (required). // subject (string): The subject of the JWT, typically the client ID (required). // audience (string[]): The intended audience, typically the authorization server token endpoint (required). // expiresIn (number): Number of seconds the token will be valid for (required). // flattenAudience (boolean): Optional, set the audience claim as a single string instead of an array (when only one value is provided). // algorithm (string): Optional, signing algorithm - one of 'RS256', 'RS384', 'ES384' (default is 'RS256'). const issuer = "https://example.com/issuer" const subject = "client-id-123" const audience = ["https://example.com/token"] const expiresIn = 300 const flattenAudience = false const algorithm = "RS256" const resp = await descopeClient.management.jwt.generateClientAssertionJwt( issuer, subject, audience, expiresIn, flattenAudience, algorithm, ) if (!resp.ok) { console.log("Failed to generate client assertion JWT") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully generated client assertion JWT") console.log(resp.data.jwt) } ``` ```java // Args: // issuer (String): The issuer of the JWT, typically the client ID (required). // subject (String): The subject of the JWT, typically the client ID (required). // audience (List): The intended audience, typically the authorization server (required). // expiresIn (Integer): Expiration time in seconds (required). // flattenAudience (Boolean): Optional, flatten the audience array to a single string. // algorithm (String): Optional, signing algorithm - one of RS256, RS384, or ES384. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { ClientAssertionResponse response = jwts.generateClientAssertionJwt( "client-id", "client-id", Arrays.asList("https://auth.example.com/token"), 3600, false, "RS256"); System.out.println("Successfully generated client assertion JWT."); System.out.println(response.getJwt()); } catch (DescopeException de) { // Handle the error } ``` # Anonymous Users (/management/user-management/anonymous-users) Learn how to configure Anonymous Users with Descope # Anonymous Users Anonymous users allow you to treat visitors as first-class identities before they provide a verified email, phone number, or username. You can reduce registration friction while still using Descope as your customer identity layer: issue session tokens, attach custom data, and later convert the same anonymous user to a standard user while retaining information you collected during the anonymous phase. ## How Anonymous Users Work Descope represents an anonymous user by issuing a **dedicated anonymous session JWT** (not a standard user record with login IDs). That token: - Is **signed by Descope** and behaves like other session tokens for your app (e.g. API authorization). - Has a **lifetime tied to the JWT** (and refresh behavior you configure)—when the session expires, that anonymous identity ends unless you refresh or convert the user. - Carries a **`danu` claim** (`true`) in the payload so your backend can tell this is an anonymous session. - Can include **custom claims** (for example user preferences, unverified email addresses, or app-specific flags) for use in your product. When you are ready, you can convert the anonymous user to a regular user, without losing any data you've already gathered on the user. See our blog on [Boosting conversions with anonymous users and guest checkout](https://www.descope.com/blog/post/descope-flows-anonymous-users). ## Creating Anonymous Users ### With Flows The **Create Anonymous user - Add Information To JWT** [flow template](https://app.descope.com/flows) provides a starting point. Upon completion, Descope issues an anonymous identity token. ![Create Anonymous User Flow](/assets/anon-users-create-flow.webp) The following example illustrates a typical JWT **payload** and **header** after the flow runs. The **`danu`** claim marks the session as anonymous; **`displayName`** (or any claims you configure) represents optional custom data for your application: ```js // Payload: { "danu": true, "displayName": "xxxxx", "drn": "DS", "exp": 1731843388, "iat": 1731842788, "iss": "xxxxxxxxx", "rexp": "2024-12-15T11:26:28Z", "sub": "xxxxxxxxx" } // Header: { "alg": "RS256", "kid": "xxxxxxxxxxxxxxxx", "typ": "JWT" } ``` ### With SDKs You can use the Descope Management SDK to **mint an anonymous session JWT** programmatically. A [management key](https://app.descope.com/settings/company/managementkeys) is required. For broader SDK setup, see [User management SDKs](/management/user-management/sdks). #### Create an Anonymous User This operation creates an anonymous user within the project with the details provided. You can also perform this operation through the [Anonymous User Management API](https://docs.descope.com/api/management/users/anonymous). ```javascript // Args: // customClaims (Record, optional): A dictionary of custom claims to include in the JWT. // These claims can be used to store additional user information. // selectedTenant (string, optional): The ID of the tenant to associate with the JWT. // This is useful for multi-tenant applications. // refreshDuration (number, optional): Duration in seconds for which the new JWT will be valid. const customClaims = { role: "guest", permissions: ["read"] }; const selectedTenant = "tenant_123"; const refreshDuration = 3600; const resp = await descopeClient.management.jwt.anonymous(customClaims, selectedTenant, refreshDuration); if (!resp.ok) { console.log("Failed to generate JWT for anonymous user."); console.log("Status Code: " + resp.code); console.log("Error Code: " + resp.error.errorCode); console.log("Error Description: " + resp.error.errorDescription); console.log("Error Message: " + resp.error.errorMessage); } else { console.log("Successfully generated JWT for anonymous user."); console.log(resp.data); } ``` ```python # Args: # custom_claims (dict, optional): A dictionary of custom claims to include in the JWT. # These claims can be used to store additional user information. # tenant_id (str, optional): The ID of the tenant to associate with the JWT. # This is useful for multi-tenant applications. # refresh_duration (int, optional): Duration in seconds for which the new JWT will be valid. try: jwt_response = descope_client.mgmt.jwt.anonymous( custom_claims={ "role": "guest", "permissions": ["read"] }, tenant_id="tenant_123", refresh_duration=3600, ) print("Successfully generated JWT for anonymous user") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print("Failed to generate JWT") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx (context.Context): Application context for the transmission of context capabilities like // cancellation signals during the function call. If no context is available, // use context.Background() as an alternative. // customClaims (map[string]any, optional): A map of custom claims to include in the JWT. These claims // can be used to store additional user information. // selectedTenant (string, optional): The ID of the tenant to associate with the JWT. This is useful // for multi-tenant applications. // refreshDuration (int32, optional): Duration in seconds for which the new JWT will be valid. ctx := context.Background() customClaims := map[string]any{ "role": "guest", "permissions": []string{"read"}, } selectedTenant := "tenant_123" refreshDuration := int32(3600) res, err := descopeClient.Management.JWT().Anonymous(ctx, customClaims, selectedTenant, refreshDuration) if err != nil { fmt.Println("Failed to generate JWT for anonymous user:", err) } else { fmt.Println("Successfully generated JWT for anonymous user:") fmt.Printf("Session Token: %s\n", res.SessionToken) fmt.Printf("Refresh Token: %s\n", res.RefreshToken) } ``` ```java // Args: // customClaims (Map): Optional, custom claims to include in the JWT. // selectedTenant (String): Optional, the tenant ID to associate with the JWT. // refreshDuration (int): Optional, duration in seconds for which the new JWT will be valid. JwtService jwts = descopeClient.getManagementServices().getJwtService(); try { AnonymousUserRequest request = AnonymousUserRequest.builder() .customClaims(new HashMap() {{ put("role", "guest"); }}) .selectedTenant("tenant_123") .refreshDuration(3600) .build(); AuthenticationInfo authInfo = jwts.anonymous(request); System.out.println("Successfully generated JWT for anonymous user."); System.out.println(authInfo.getToken().getJwt()); System.out.println(authInfo.getRefreshToken().getJwt()); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // CustomClaims (AnonymousUserRequest_customClaims): Optional, custom claims to include in the JWT. // SelectedTenant (string): Optional, the tenant ID to associate with the JWT. // RefreshDuration (int?): Optional, duration in seconds for which the new JWT will be valid. var customClaims = new AnonymousUserRequest_customClaims(); customClaims.AdditionalData["role"] = "guest"; var anonymousRequest = new AnonymousUserRequest { CustomClaims = customClaims, SelectedTenant = "tenant_123", RefreshDuration = 3600, }; var anonymousResponse = await descopeClient.Mgmt.V1.Auth.Anonymous.PostAsync(anonymousRequest); Console.WriteLine("Successfully generated JWT for anonymous user."); Console.WriteLine(anonymousResponse?.SessionJwt); Console.WriteLine(anonymousResponse?.RefreshJwt); ``` ## Converting Anonymous Users to Regular Users ### With Flows To move from an anonymous session to a regular user, use a flow such as [Anonymous User Conversion](https://app.descope.com/flows?template=sign-up-anonymous-user). This flow authenticates the user and links a verified login ID while preserving context from the anonymous phase. ![Update anonymous users magic link flow](/assets/anon-users-convert-flow.webp) - The template demonstrates one authentication pattern; you can adapt the same approach to other factors your product supports. - **Verify ownership** of the login ID (for example via magic link or OTP) before completing conversion. Doing so prevents users from attaching an email or phone number that already belongs to another account. ### With SDKs You may not have access to the refresh token of the anonymous user from your backend, depending on your [token management settings](/security-best-practices/refresh-token-storage). If this is the case, you will need to use Flows instead, to convert the anonymous user. You can convert an anonymous user in your backend by calling any **update-user** auth method while sending the anonymous user's **refresh JWT**. Descope detects that there is no real user row for that subject and that the JWT is anonymous (`danu: true`), then creates a regular user that **reuses the anonymous user's ID**. Anything you keyed off that ID stays attached. After you verify the OTP (or magic link / phone update), you receive a real session. On a successful conversion, the auth response includes `firstSeen: true`. #### Convert with OTP Update Email Start conversion by updating the anonymous user's email via OTP, then verify the code to complete conversion and issue a regular session. You must verify ownership of the login ID (for example via an OTP or magic link) before completing conversion. This is to prevent users from attaching an email or phone number, to the newly created user, that already belongs to another account. ```javascript // Args: // loginId (string): The new email — becomes the login ID for the converted user. // email (string): The email address to verify and attach. // refreshToken (string): The anonymous user's refresh JWT. // updateOptions (object): Use addToLoginIDs and onMergeUseExisting when attaching the new login ID. // code (string): The OTP code the user received by email. const loginId = "user@example.com"; const email = "user@example.com"; const refreshToken = "xxxxx"; // anonymous refresh JWT const updateOptions = { addToLoginIDs: true, onMergeUseExisting: true, }; const updateResp = await descopeClient.otp.update.email(loginId, email, refreshToken, updateOptions); if (!updateResp.ok) { console.log("Failed to start anonymous user conversion."); console.log("Status Code: " + updateResp.code); console.log("Error Code: " + updateResp.error.errorCode); console.log("Error Description: " + updateResp.error.errorDescription); console.log("Error Message: " + updateResp.error.errorMessage); } else { console.log("Successfully started OTP email update."); console.log(updateResp.data); } // After the user enters the OTP code: const code = "xxxxxx"; const verifyResp = await descopeClient.otp.verify.email(loginId, code); if (!verifyResp.ok) { console.log("Failed to verify OTP code."); console.log("Status Code: " + verifyResp.code); console.log("Error Code: " + verifyResp.error.errorCode); console.log("Error Description: " + verifyResp.error.errorDescription); console.log("Error Message: " + verifyResp.error.errorMessage); } else { console.log("Successfully converted anonymous user."); console.log("First seen: " + verifyResp.data.firstSeen); console.log(verifyResp.data); } ``` ```python # Args: # login_id (str): The new email — becomes the login ID for the converted user. # email (str): The email address to verify and attach. # refresh_token (str): The anonymous user's refresh JWT. # add_to_login_ids (bool): Append the email as a login ID. # on_merge_use_existing (bool): On login ID conflict, keep the existing user. # code (str): The OTP code the user received by email. login_id = "user@example.com" try: masked = descope_client.otp.update_user_email( login_id=login_id, email="user@example.com", refresh_token="xxxxx", # anonymous refresh JWT add_to_login_ids=True, on_merge_use_existing=True, ) print("Successfully started OTP email update") print(masked) except AuthException as error: print("Failed to start anonymous user conversion") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) # After the user enters the OTP code: try: jwt_response = descope_client.otp.verify_code( method=DeliveryMethod.EMAIL, login_id=login_id, code="xxxxxx", ) print("Successfully converted anonymous user") print("First seen: " + str(jwt_response.get("firstSeen"))) print(json.dumps(jwt_response, indent=4)) except AuthException as error: print("Failed to verify OTP code") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx (context.Context): Application context. Use context.Background() if none is available. // loginID (string): The new email — becomes the login ID for the converted user. // email (string): The email address to verify and attach. // updateOptions (*descope.UpdateOptions): Use AddToLoginIDs and OnMergeUseExisting when attaching the new login ID. // request (*http.Request): Must carry the anonymous refresh JWT (for example, the DSR cookie). // code (string): The OTP code the user received by email. // w (http.ResponseWriter): Optional; may be used to set session cookies on the response. ctx := context.Background() loginID := "user@example.com" email := "user@example.com" refreshJWT := "xxxxx" // anonymous refresh JWT updateOptions := &descope.UpdateOptions{ AddToLoginIDs: true, OnMergeUseExisting: true, } // r and w come from your HTTP handler. Attach the anonymous refresh JWT (DSR cookie). r.AddCookie(&http.Cookie{Name: descope.RefreshCookieName, Value: refreshJWT}) masked, err := descopeClient.Auth.OTP().UpdateUserEmail(ctx, loginID, email, updateOptions, r) if err != nil { fmt.Println("Failed to start anonymous user conversion:", err) } else { fmt.Println("Successfully started OTP email update:", masked) } // After the user enters the OTP code: code := "xxxxxx" authInfo, err := descopeClient.Auth.OTP().VerifyCode(ctx, descope.MethodEmail, loginID, code, w) if err != nil { fmt.Println("Failed to verify OTP code:", err) } else { fmt.Println("Successfully converted anonymous user") fmt.Printf("First seen: %v\n", authInfo.FirstSeen) } ``` ```java // Args: // loginId (String): The new email — becomes the login ID for the converted user. // email (String): The email address to verify and attach. // refreshToken (String): The anonymous user's refresh JWT. // updateOptions (UpdateOptions): Use addToLoginIds and onMergeUseExisting when attaching the new login ID. // code (String): The OTP code the user received by email. String loginId = "user@example.com"; String email = "user@example.com"; String refreshToken = "xxxxx"; // anonymous refresh JWT UpdateOptions updateOptions = UpdateOptions.builder() .addToLoginIds(true) .onMergeUseExisting(true) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String masked = otps.updateUserEmail(loginId, email, refreshToken, updateOptions); System.out.println("Successfully started OTP email update."); System.out.println(masked); // After the user enters the OTP code: String code = "xxxxxx"; AuthenticationInfo authInfo = otps.verifyCode(DeliveryMethod.EMAIL, loginId, code); System.out.println("Successfully converted anonymous user."); System.out.println("First seen: " + authInfo.getFirstSeen()); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The new email — becomes the login ID for the converted user. // Email (string): The email address to verify and attach. // refreshJwt (string): The anonymous user's refresh JWT (required by PostWithJwtAsync). // AddToLoginIDs / OnMergeUseExisting: Attach the email as a login ID and keep the existing user on merge. // Code (string): The OTP code the user received by email. var loginId = "user@example.com"; var email = "user@example.com"; var refreshJwt = "xxxxx"; // anonymous refresh JWT try { var updateResponse = await descopeClient.Auth.V1.Otp.Update.Email.PostWithJwtAsync( new UpdateUserEmailOTPRequest { LoginId = loginId, Email = email, AddToLoginIDs = true, OnMergeUseExisting = true, }, refreshJwt); Console.WriteLine("Successfully started OTP email update."); Console.WriteLine(updateResponse?.MaskedEmail); // After the user enters the OTP code: var code = "xxxxxx"; var authResponse = await descopeClient.Auth.V1.Otp.Verify.Email.PostAsync( new OTPVerifyCodeRequest { LoginId = loginId, Code = code, }); Console.WriteLine("Successfully converted anonymous user."); Console.WriteLine("First seen: " + authResponse?.FirstSeen); } catch (DescopeException ex) { // Handle the error } ``` # User Management (/management/user-management) Learn how to easily implement user management and authorization for your app with Descope. # User Management If you want to view the SDK documentation for User Management, click [here](/management/user-management/sdks). ## User Identity For each user created in your project, Descope assigns a unique identifier (`userId`) to the user. You cannot set or change the `userId` within the Descope UI, your application, or any API call. Descope also stores an array of login IDs, which are used as the login identifier parameter for all user authentication and update actions. Login IDs can be an email, phone, or username, and must be unique across all users in the project. The user object also includes additional attributes (which do not have to be unique across users) such as email, phone, name, and [custom attributes](/management/user-management#custom-user-attributes). Users can have defined access to [federated apps](/identity-federation/applications) and [tenants](/management/tenant-management), as well as [roles](/authorization/role-based-access-control) on the project and/or tenant level. ### Using a User ID in Place of a Login ID Most user management operations accept a User ID anywhere they accept a Login ID. Descope detects which kind of identifier you passed and resolves the user. See the [SDK reference](/management/user-management/sdks) for the exact parameters each function accepts. If you're using phone numbers as a login ID, make sure that the phone numbers are [formatted properly](/flows/screens/inputs/phone-numbers) before adding them, if using the Management SDK or User Management APIs. ## User Table The [Users page](https://app.descope.com/users) in the Descope Console provides a comprehensive table view of all users in your project. This interface serves as the central hub for managing and viewing user information. ### Managing Users from the Table From the user table, you can perform various actions: - **View user details** - Click on a user to view their complete profile and information - **Invite users** - Invite users individually or in batches - **Edit users** - Update user information, passwords, roles, and tenant associations - **Filter and search** - Filter and search for users based on attributes. For example: - **Status** - Active, disabled, or invited - **Roles** - Project-level or tenant-level roles - **Authentication method** - Whether users have a specific method enrolled, such as a password or passkey - **Custom attributes** - Any custom attribute values defined for your project - **Text search** - Name, email, phone, or login ID - **Modify user status** - Activate, disable, force logout, and delete users - **Bulk actions** - Perform actions on multiple users at once #### Filtering Examples You can filter the user table using various criteria, such as custom attributes, verified phone/email, status, roles, and more. Multiple filters can be combined to narrow down results. In the [Descope Console](https://app.descope.com/users), open the filter controls on the Users page and select the desired filters. ![user filtering example](/assets/user-filtering-example.webp) ### Recovery Email and Phone Columns The user table also includes two optional columns, **Recovery Email** and **Recovery Phone**, hidden by default like other optional columns. Each column shows the value plus a verified/unverified indicator, and you can sort and filter both the same way as the Email and Phone columns. You can set, update, or clear a user's recovery email or phone from the user edit dialog. Setting or changing a value marks it verified. ## Custom User Attributes Descope allows you to create custom attributes that can store further details about your users. You can create custom attributes within the user's page under the [custom attributes tab](https://app.descope.com/users/attributes). Custom attributes can be of the following types: - **Text** - Store text-based information - **Numeric** - Store numeric values - **Boolean** - Store true/false values - **Single select** - Choose one option from a list - **Multi select** - Choose multiple options from a list (utilized as an array) - **Date** - Store date values - **Month-Day** - Store the values in the MM-DD format Custom attributes can be used to store any data you want for the user. For example, this data could be a user's date of birth, location, etc. You can later utilize these attributes within [custom claims](/security-best-practices/custom-claims) or load them for a user and display them within your application. ## Inviting Users Learn more about creating and customizing user invitations to your application in our [Inviting Users doc](/management/user-management/invite-users). ## User Merging Descope supports the merging of user accounts. Merging accounts will be based on trusted email addresses. Within the [Authentication Methods](https://app.descope.com/settings/authentication) page of the Descope UI, Descopers can configure the Social Auth (OAuth) logins to merge with existing users. Within each of the Social Auth (OAuth) methods, Descopers can configure the merging of the users by enabling the toggle for `Merge user accounts based on returned email address from provider`. If the email address returned from the Social Auth (OAuth) provider matches an existing user, the Descope service will merge the accounts based on the user's email address. ## Associating Multiple Login IDs for a User Within your application, you may have users signing up or signing in with an email authentication method but also utilizing other methods, such as SMS or Social Login. Descope allows these different auth methods to be nested into the same user by allowing multiple login IDs to be associated with the user. Storing multiple login IDs enables the user to log in with: - Email auth method - SMS auth method - Social Login All of the login IDs will be associated with the same user. The user can then log into your application using their email address or phone number as the login ID. ### Using API or SDK When utilizing the API or SDK, if you want to enable this feature, you will use the options `AddToLoginIDs` and `OnMergeUseExisting`: - **AddToLoginIDs**: Setting this to `true` will enable the additional login IDs to be associated with the user - **OnMergeUseExisting**: - When set to `false`, it will merge the users based on the new user's details - When set to `true`, it will merge the users based on the existing user's details You can also associate multiple login IDs by utilizing the create, batch create, invite, or update functions below, passing the additional login IDs parameter. If you are utilizing flows, see our [doc on linking user identities in flows](/flows/actions/multiple-login-id) which covers implementation details. ## User Lifecycle Users within Descope can be in one of four states: - **Active** - Users can log in and interact with your application based on their assigned roles - **Disabled** - Users cannot log into your application, but remain in the Descope project and may be reactivated - **Invited** - Users have been invited but have not yet signed in; once logged in, they become active - **Invite Expired** - User invitations that were not accepted within the configured expiration period. These users can be re-invited to reset the invitation ### Force Logout A Descoper can force a logout on a user, useful in situations like suspicious activity where you would want to force reauthentication. Force logout will log the user out from all devices and sessions. This can be done directly via the [Users page](https://app.descope.com/users) of the Descope console, or using our [Management SDK function](/management/user-management/sdks#logout-all-user-sessions). # Inviting Users (/management/user-management/invite-users) This guide will cover customizing user invite templates and the various options you have when inviting users within Descope. # Inviting Users This guide covers how to implement user invitations within Descope. Descope provides several mechanisms to invite users with customization options and templates for invitations. ## User Invitation Settings Configure user invitations in the [Project Settings](https://app.descope.com/settings/project) page under the **Sign Ups and User Invitations** section. Refer to the [Project Settings doc](/management/project-settings#sign-ups-and-user-invitations) for more information. ## Invitation Expiration When enabled, user invitations will automatically expire if not accepted within a configured time period. This helps maintain security by ensuring invitations are not left open indefinitely and provides better control over your user onboarding process. ### How It Works 1. **Enable Expiration**: In [Project Settings](https://app.descope.com/settings/project), enable the **Invite Expiration** option under **Sign Ups and User Invitations**. 2. **Set Expiration Period**: Configure the expiration period in hours, days, or weeks. The default is typically 1 week. 3. **Invitation Status Changes**: When an invitation expires: - The user's status automatically changes from **Invited** to **Invite Expired** - The user cannot log in with the expired invitation - Any Magic Link tokens included in the invitation are also invalidated 4. **Re-inviting Expired Users**: Users with expired invitations can be re-invited, which: - Resets their status back to **Invited** - Restarts the expiration timer - Sends a new invitation email/SMS - Invalidates any previous Magic Link tokens If your invitation includes a Magic Link token (configured in Project Settings), the Magic Link expiration should always be equal to or shorter than the invitation expiration period. ### User Lifecycle with Expiration The invitation expiration feature introduces an additional user state. Users can transition between states as follows: - **Invited** → **Active**: When the user accepts the invitation and signs in before expiration - **Invited** → **Invite Expired**: When the configured expiration period passes without the user signing in - **Invite Expired** → **Invited**: When an administrator re-invites the user - **Active**: Once a user becomes active, any related invitations and Magic Links are automatically expired For more information on user states, see the [User Lifecycle documentation](/management/user-management#user-lifecycle). ## Invitation Templates When using a custom messaging connector instead of the Descope system connector, you can [create custom templates](/management/messaging-templates#user-invitation-templates) for user invitations and reference [tenant name and other dynamic variables](/management/messaging-templates#available-dynamic-variables). ## Invitation Methods ### Descope Console On the [User Management page](https://app.descope.com/users) of the Descope Console, you can invite users individually or in bulk. You can define the following in the user invite: - **Login IDs**: Enter one or more login IDs (emails or phone numbers) for invited users - **User Attributes**: Add details for the user(s). Can include name, email, phone, and any [custom attributes](/management/user-management#custom-user-attributes) - **Authorization**: Define user access to tenants, and assign [project and tenant-level roles](/authorization/role-based-access-control). Additionally, you can define access to [federated apps](/identity-federation/applications). - **User Invitations**: Determine whether or not to send an invitation to the user(s). This will use the messaging template configured in [Project Settings](/management/project-settings#user-invitations). If the selected template has [translations configured](/management/localization), you can also choose which locale to send the invitation in. When using bulk invite, the user information you input will be applied to all users with each login ID you set. If you need to provide different information for each user, you will have to add all users without any information and then modify each user in the user table afterwards. To create users with multiple login IDs, select `Add as Login Id` when adding an email or phone number under user attributes ![Invite Users from Console](/assets/invite-users-descope-console.webp) #### Re-inviting Users You can resend invitations to users in the following scenarios: - Users who have lost the original invitation email/SMS - Users whose invitations have expired (status: **Invite Expired**) - Users who need a fresh Magic Link token To re-invite a user: 1. Navigate to the [Users Page](https://app.descope.com/users) of the Descope Console 2. Click the three dots menu next to the user 3. Select the `Resend Invitation` option When you re-invite a user: - Their status is reset to **Invited** (if it was **Invite Expired**) - The invitation expiration timer restarts - A new invitation email/SMS is sent - Any previous Magic Link tokens are invalidated The `Resend Invitation` option is only available for users with **Invited** or **Invite Expired** status. Active users cannot be re-invited. ![Re-invite users](/assets/reinvite.webp) ### Descope Flows To learn how to invite users within a Flow, refer to our [User Invite Flow Action Doc](/flows/actions/user-invite) ### Management SDK You can invite users programmatically using the [Descope Management SDK](/management/user-management/sdks#invite-user). # Users with SDKs (/management/user-management/sdks) Learn how to easily implement user management and authorization for your app with Descope e using the Descope backend SDKs. # Users with SDKs You can use the Descope management SDK for common user management operations like create user, update user, delete user, etc. The management SDK requires a management key, which can be generated [here](https://app.descope.com/settings/company/managementkeys). ### Create User This operation creates a new user within the project with the details provided. `Create` will not send an invite. If you want to send an invite on creation, use [Invite User](/management/user-management/sdks#invite-user). ```javascript // Args: // loginId (str): user login_id. const loginId = "custom-login-id"; // displayName (str): Optional user display name. const displayName = "Joe Person"; // phone (str): Optional user phone number. const phone = "+15555555555"; // email (str): Optional user email address. const email = "email@company.com"; // userTenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. const userTenants = [{ tenantId: "TestTenant", roleNames: ["TestRole"] }]; // roles (List[str]): An optional list of the user's role names without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. const roles = ["Tenant Admin"]; // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = { attribute1: "Value 1", attribute2: "Value 2" }; // picture (str): Optional url for user picture const picture = "https://example.com/picture.jpg"; // verifiedEmail (bool): Set to true for the user to be able to login with the email address. const verifiedEmail = true; // or false // verifiedPhone (bool): Set to true for the user to be able to login with the phone number. const verifiedPhone = true; // or false // additionalLoginIds (optional List[str]): An optional list of additional login IDs to associate with the user const additionalLoginIds = ["MyUserName", "+12223334455"]; // templateId (str): Optional template Id for the invitation message const templateId = "my-template-id" // A user must have a login ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. const resp = await descopeClient.management.user.create( loginId, { email, phone, displayName, // picture, verifiedEmail, verifiedPhone, customAttributes, additionalLoginIds, // userTenants,// either userTenants or roles roles, templateId, } ); if (!resp.ok) { console.log("Failed to create user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): user login_id. # email (str): Optional user email address. # phone (str): Optional user phone number. # display_name (str): Optional user display name. # role_names (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. # user_tenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. user_tenants=[AssociatedTenant("TestTenant")] # picture (str): Optional url for user picture # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app # verified_email (bool): Set to true for the user to be able to login with the email address. # verifiedPhone (bool): Set to true for the user to be able to login with the phone number. # additional_login_ids (optional List[str]): An optional list of additional login IDs to associate with the user # A user must have a login ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. try: resp = descope_client.mgmt.user.create( login_id="email@company.com", email="email@company.com", display_name="Joe Person", phone="+15555555555", # You can update user_tenants or role_names, not both in the same action # user_tenants=user_tenants, role_names=["TestRole"], picture="xxxx", custom_attributes={"attribute1": "Value 1", "attribute2": "Value 2"}, verified_email=True, verified_phone=True, additional_login_ids=["MyUserName", "+12223334455"] ) print ("Successfully created user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to create user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): user loginID. loginID := "xxxx" // userRequest (UserRequest): A completed descope structure with applicable details for the user userReq := &descope.UserRequest{} userReq.Email = "email@company.com" userReq.Name = "Joe Person" userReq.Roles = nil // or []string{"TestRole1","TestRole2"} however, if roles is given, Tenants should then be nil userReq.Tenants = []*descope.AssociatedTenant{{TenantID: "TestTenant"}} userReq.CustomAttributes = map[string]any{"attribute1": "Value 1", "attribute2": "Value 2"} userReq.Picture = "xxxx" VerifiedEmail := true // or false userReq.VerifiedEmail = &VerifiedEmail VerifiedPhone := true // or false userReq.VerifiedPhone = &VerifiedPhone userReq.AdditionalLoginIds = ["MyUserName", "+12223334455"] // A user must have a login ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. _, err := descopeClient.Management.User().Create(ctx, loginID, userReq) if (err != nil){ fmt.Println("Unable to create user: ", err) } else { fmt.Println("User Successfully created.") } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. UserService us = descopeClient.getManagementServices().getUserService(); try { us.create("email@company.com", UserRequest.builder() .email("email@company.com") .verifiedEmail(true) .phone("+15555555555") .verifiedPhone(false) .displayName("Joe Person") .givenName("Joe") .middleName("A") .familyName("Person") .picture("https://example.com/picture.jpg") .roleNames(Arrays.asList("role-name1")) .userTenants(Arrays.asList( AssociatedTenant.builder() .tenantId("tenant-ID1") .roleNames(Arrays.asList("role-name1")) .build(), AssociatedTenant.builder() .tenantId("tenant-ID2") .build())) .customAttributes(Map.of("attribute1", "Value 1")) .additionalIdentifiers(Arrays.asList("MyUserName", "+12223334455")) .ssoAppIds(Arrays.asList("app-id-1")) .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): user login_id. # email (str): Optional user email address. # phone (str): Optional user phone number. # name (str): Optional user display name. # given_name / middle_name / family_name (str): Optional name components. # role_names (Array): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the user_tenants roles, which take precedence over them. # user_tenants (Array): An optional list of the user's tenants, and optionally their roles per tenant. # picture (str): Optional url for user picture. # custom_attributes (Hash): Optional, set the different custom attributes values of the keys that were previously configured in the Descope console app. # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # additional_identifiers (Array): An optional list of additional login IDs to associate with the user. # template_id (str): Optional template Id for the invitation message. # A user must have a login ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. begin resp = descope_client.create_user( login_id: 'desmond@descope.com', email: 'desmond@descope.com', name: 'Desmond Copeland', user_tenants: descope_client.associated_tenants_to_hash_array( [{ tenant_id: 'my-tenant-id', role_names: ['role-name1'] }] ), ) puts 'Successfully created user.' puts resp rescue Descope::AuthException => e puts "Unable to create user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->create( 'testuser1', // loginId 'newemail@example.com', // email '+1234567890', // phone 'Updated User', // displayName 'Updated', // givenName 'Middle', // middleName 'User', // familyName null, // picture null, // customAttributes true, // verifiedEmail true, // verifiedPhone null, // inviteUrl ['altUser1'], // additionalLoginIds ['app123'], // ssoAppIds null, // password ['admin', 'editor'], // roleNames [['tenantId' => 'tenant1']] // userTenants ); print_r($response); ``` ```csharp // Args: // loginID (string): Unique login ID for the new user (required). var loginID = "user-login-id"; // createRequest (CreateUserRequest): User details for creation. var createRequest = new CreateUserRequest { Identifier = loginID, Email = "email@company.com", Name = "new-user", UserTenants = new List { new AssociatedTenant { TenantId = "Tenant-ID1", RoleNames = new List { "role-name1" } } }, SsoAppIds = new List { "appId1", "appId2" }, // Invite (bool?): If true, an invitation email/SMS is sent. Invite = false, // Test (bool?): Flag to mark the user as a test account. Test = false }; try { var userRes = await descopeClient.Mgmt.V1.User.Create.PostAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Batch Create Users This operation creates multiple users within the project in a single request. Batch creation comes in two behaviors, which differ only in whether the new users are notified: - **Create** provisions the users directly, without sending anything. Created users are active immediately. Use this for migrations and for programmatic provisioning where you do not want your users to be contacted. - **Invite** provisions the users and sends each one an invitation via email or SMS. Invited users have a status of `invited` until they sign in for the first time. A batch request may partially succeed. Always inspect the `failedUsers` field in the response alongside `createdUsers`. A cleartext `password` on any user in the batch caps the whole request at 100 users. Batches that use only `hashedPassword`, or carry no password, have no such cap. ```javascript // Args: // users (array of Descope Users) // loginIdOrUserId (str): user login ID or user ID. When a user ID is provided with inviteBatch, the user must already exist — no new user is created, and the invite is resent. // email (str): Optional user email address. // phone (str): Optional user phone number. // displayName (str): Optional user display name. // roles (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. // userTenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app // picture (str): Optional url for user picture // verifiedEmail (bool): Set to true for the user to be able to login with the email address. // verifiedPhone (bool): Set to true for the user to be able to login with the phone number. // test (bool): Set to true if creating a test user, otherwise false // additionalLoginIds (optional List[str]): An optional list of additional login IDs to associate with the user // status (optional UserStatus): An optional status for the user. Can be one of "enabled", "disabled", "invited", or "expired". If not provided, defaults to "enabled". // createdTime (optional number): Unix timestamp in seconds to set as the user's creation date, for preserving signup dates when migrating users. Must be non-negative and not in the future. // password (optional str): Set a cleartext password for the new user. Batches containing a cleartext password are capped at 100 users. // hashedPassword (optional object): Import an existing password hash instead of a cleartext password. const users = [ { loginIdOrUserId: 'email2@company.com', email: 'email2@company.com', phone: '+15555555555', displayName: 'Joe Person', userTenants: [{ tenantId: 'TestTenant', roleNames: ['TestRole'] }], customAttributes: {"attribute1": "Value 1", "attribute2": "Value 2"}, picture: "https://xxxx.co/img", verifiedEmail: true, verifiedPhone: true, test: false, additionalLoginIds: ["MyUserName", "+12223334455"], status: "enabled", createdTime: 1609459200 }, { loginIdOrUserId: 'email@company.com', email: 'email@company.com', phone: '+15556667777', displayName: 'Desmond Copeland', userTenants: [{ tenantId: 'TestTenant', roleNames: ['TestRole'] }], customAttributes: {"attribute1": "Value 1", "attribute2": "Value 2"}, picture: "https://xxxx.co/img", verifiedEmail: true, verifiedPhone: true, test: false, additionalLoginIds: ["MyUserName", "+12223334455"], status: "invited" }, { // Optionally set a cleartext password for the new user (subject to the 100-user batch cap noted above). loginId: 'email3@company.com', email: 'email3@company.com', password: 'cleartext-password' }, { // Or import an existing password hash instead of a cleartext password. loginId: 'email4@company.com', email: 'email4@company.com', hashedPassword: { bcrypt: { hash: "$2a$..." } } } ] // --------------------------------------------------------------------------- // Option 1: create the users WITHOUT sending an invitation. // The users are provisioned directly and no email or SMS is sent to them. // --------------------------------------------------------------------------- const createResp = await descopeClient.management.user.createBatch(users); if (!createResp.ok) { console.log("Failed to batch create users.") console.log("Status Code: " + createResp.code) console.log("Error Code: " + createResp.error.errorCode) console.log("Error Description: " + createResp.error.errorDescription) console.log("Error Message: " + createResp.error.errorMessage) } else { // A batch can partially succeed, so always inspect failedUsers as well. console.log("Successfully created users: ", createResp.data.createdUsers) console.log("Users that could not be created: ", createResp.data.failedUsers) } // --------------------------------------------------------------------------- // Option 2: create the users AND send each one an invitation. // For inviteBatch, loginIdOrUserId may be a user ID to re-invite an existing user. // --------------------------------------------------------------------------- // inviteUrl // URL to include in user invitation for the user to sign in with const inviteUrl = "https://company.com/sign-in" // sendMail (bool): true or false for sending invite via email const sendMail = true // sendSMS (bool): true or false for sending invite via SMS const sendSMS = false // templateOptions (dict): Optional dynamic data injected into the template (keys must be lowercase) const templateOptions = { k1: 'v1', k2: 'v2' } // templateId (str): Optional template Id for the invitation message const templateId = "my-template-id" // locale (str): Optional locale applied to every invitation in the batch. Only takes effect when the selected template has translations configured; otherwise Descope uses the template's source language. const locale = "es" const inviteResp = await descopeClient.management.user.inviteBatch( users, inviteUrl, sendMail, sendSMS, templateOptions, templateId, locale ); if (!inviteResp.ok) { console.log("Failed to batch invite users.") console.log("Status Code: " + inviteResp.code) console.log("Error Code: " + inviteResp.error.errorCode) console.log("Error Description: " + inviteResp.error.errorDescription) console.log("Error Message: " + inviteResp.error.errorMessage) } else { // A batch can partially succeed, so always inspect failedUsers as well. console.log("Successfully invited users: ", inviteResp.data.createdUsers) console.log("Users that could not be invited: ", inviteResp.data.failedUsers) } ``` ```python # Args: # users (array of Descope Users) # login_id (str): user login_id. # email (str): Optional user email address. # phone (str): Optional user phone number. # display_name (str): Optional user display name. # roles (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. # user_tenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app # picture (str): Optional url for user picture # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # test (bool): Set to true if creating a test user, otherwise false # additional_login_ids (optional List[str]): An optional list of additional login IDs to associate with the user # status (optional str): Status for the user. Can be one of "enabled", "disabled", "invited", or "expired". users = [ { login_id: 'email2@company.com', email: 'email2@company.com', phone: '+15555555555', display_name: 'Joe Person', user_tenants: [{ tenantId: 'TestTenant', roleNames: ['TestRole'] }], custom_attributes: {"attribute1": "Value 1", "attribute2": "Value 2"}, picture: "https:#xxxx.co/img", verified_email: true, verified_phone: true, test: false, additional_login_ids: ["MyUserName", "+12223334455"], status: "enabled" }, { login_id: 'email@company.com', email: 'email@company.com', phone: '+15556667777', display_name: 'Desmond Copeland', user_tenants: [{ tenantId: 'TestTenant', roleNames: ['TestRole'] }], custom_attributes: {"attribute1": "Value 1", "attribute2": "Value 2"}, picture: "https:#xxxx.co/img", verified_email: true, verified_phone: true, test: false, additional_login_ids: ["MyUserName", "+12223334455"], status: "invited" } ] # invite_url # URL to include in user invitation for the user to sign in with # send_mail (bool): true or false for sending invite via email # send_sms (bool): true or false for sending invite via SMS # locale (str): Optional locale applied to every invitation in the batch. Only takes effect when the selected template has translations configured; otherwise Descope uses the template's source language. try: resp = descope_client.mgmt.user.invite_batch( users=users, invite_url="https:#company.com/sign-in", send_mail=True, send_sms=False, locale="es" ) print ("Successfully batch invited users.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to batch invite users.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // batchUsers ([]*descope.BatchUser): The users to create. LoginID is required on each user. // For InviteBatch, LoginID may be a user ID to re-invite an existing user. verifiedEmail := true verifiedPhone := false u1 := &descope.BatchUser{} u1.LoginID = "email2@company.com" u1.Email = "email2@company.com" u1.Name = "Joe Person" u1.Tenants = []*descope.AssociatedTenant{{TenantID: "TestTenant", Roles: []string{"TestRole"}}} u1.CustomAttributes = map[string]any{"attribute1": "Value 1", "attribute2": "Value 2"} u1.Picture = "https://xxxx.co/img" u1.VerifiedEmail = &verifiedEmail u1.VerifiedPhone = &verifiedPhone u1.AdditionalLoginIDs = []string{"MyUserName1", "+12223334445"} u1.Status = descope.UserStatusEnabled // or UserStatusDisabled, UserStatusInvited, UserStatusExpired // Optionally set a cleartext password for the new user. Batches containing a cleartext password are capped at 100 users. u1.Password = &descope.BatchUserPassword{Cleartext: "cleartext-password"} u2 := &descope.BatchUser{} u2.LoginID = "email@company.com" u2.Email = "email@company.com" u2.Name = "Desmond Copeland" u2.Tenants = []*descope.AssociatedTenant{{TenantID: "TestTenant", Roles: []string{"TestRole"}}} u2.CustomAttributes = map[string]any{"attribute1": "Value 1", "attribute2": "Value 2"} u2.VerifiedEmail = &verifiedEmail u2.VerifiedPhone = &verifiedPhone u2.AdditionalLoginIDs = []string{"MyUserName2", "+12223334455"} u2.Status = descope.UserStatusInvited // Optionally import an existing password hash instead of a cleartext password. u2.Password = &descope.BatchUserPassword{ Hashed: &descope.BatchUserPasswordHashed{ Bcrypt: &descope.BatchUserPasswordBcrypt{Hash: "$2a$..."}, }, } // To import a user with a pre-hashed SHA password, set Password.Hashed.Sha instead of a cleartext password: u3 := &descope.BatchUser{} u3.LoginID = "email3@company.com" u3.Email = "email3@company.com" u3.Password = &descope.BatchUserPassword{ Hashed: &descope.BatchUserPasswordHashed{ Sha: &descope.BatchUserPasswordSha{ Hash: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", Type: "sha256", // sha1, sha256, or sha512 Salt: "5d41402abc4b2a76b9719d911017c592", }, }, } batchUsers := []*descope.BatchUser{u1, u2, u3} // --------------------------------------------------------------------------- // Option 1: create the users WITHOUT sending an invitation. // --------------------------------------------------------------------------- res, err := descopeClient.Management.User().CreateBatch(ctx, batchUsers) if err != nil { fmt.Println("Unable to batch create users: ", err) } else { // A batch can partially succeed, so always inspect FailedUsers as well. fmt.Println("Created users: ", res.CreatedUsers) fmt.Println("Users that could not be created: ", res.FailedUsers) } // --------------------------------------------------------------------------- // Option 2: create the users AND send each one an invitation. // For InviteBatch, LoginID may be a user ID to re-invite an existing user. // --------------------------------------------------------------------------- // options (*descope.InviteOptions): Invite options including InviteURL, SendMail and/or SendSMS // Locale (str): Optional locale applied to every invitation in the batch. Only takes effect when the selected template has translations configured; otherwise Descope uses the template's source language. sendMail := true sendSMS := false options := &descope.InviteOptions{ InviteURL: "https://company.com/signIn", SendMail: &sendMail, SendSMS: &sendSMS, Locale: "es", } res, err = descopeClient.Management.User().InviteBatch(ctx, batchUsers, options) if err != nil { fmt.Println("Unable to batch invite users: ", err) } else { fmt.Println("Invited users: ", res.CreatedUsers) fmt.Println("Users that could not be invited: ", res.FailedUsers) } ``` ```java // Args: // users (List): Each entry represents a user to batch create/invite. // loginId (String): user login_id. // email (String): Optional user email address. // phone (String): Optional user phone number. // displayName (String): Optional user display name. // roleNames (List): An optional list of the user's roles without tenant association. These roles are mutually exclusive with userTenants roles, which take precedence over them. // userTenants (List): An optional list of the user's tenants, and optionally, their roles per tenant. // customAttributes (Map): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app // picture (String): Optional url for user picture // verifiedEmail (Boolean): Set to true for the user to be able to login with the email address. // verifiedPhone (Boolean): Set to true for the user to be able to login with the phone number. // additionalIdentifiers (List): An optional list of additional login IDs to associate with the user // A user must have a loginId, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. // Note: users created through these functions are always non-test users, and // BatchUserRequest does not expose a status field. UserService us = descopeClient.getManagementServices().getUserService(); List batchUsers = Arrays.asList( BatchUserRequest.builder() .loginId("email2@company.com") .email("email2@company.com") .verifiedEmail(true) .phone("+15555555555") .verifiedPhone(true) .displayName("Joe Person") .roleNames(Arrays.asList("role-name1")) .userTenants(Arrays.asList( AssociatedTenant.builder() .tenantId("tenant-ID1") .roleNames(Arrays.asList("role-name1")) .build())) .customAttributes(Map.of("attribute1", "Value 1")) .picture("https://example.com/picture.jpg") .additionalIdentifiers(Arrays.asList("MyUserName", "+12223334455")) .ssoAppIds(Arrays.asList("app-id-1")) // Optionally set a cleartext password for the new user. .password("cleartext-password") .build(), BatchUserRequest.builder() .loginId("email@company.com") .email("email@company.com") .verifiedEmail(true) .displayName("Desmond Copeland") // Or import a prehashed password from another service instead. .hashedPassword(BatchUserPasswordHashed.builder() .bcrypt(BatchUserPasswordBcrypt.builder().hash("$2a$...").build()) .build()) .build(), // To import a user with a pre-hashed SHA password, set hashedPassword.sha instead of password. BatchUserRequest.builder() .loginId("email3@company.com") .email("email3@company.com") .hashedPassword(BatchUserPasswordHashed.builder() .sha(BatchUserPasswordSha.builder() .hash("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8") .type("sha256") // sha1, sha256, or sha512 .salt("5d41402abc4b2a76b9719d911017c592") .build()) .build()) .build()); // --------------------------------------------------------------------------- // Option 1: create the users WITHOUT sending an invitation. // --------------------------------------------------------------------------- try { UsersBatchResponse res = us.createBatch(batchUsers); // A batch can partially succeed, so always inspect getFailedUsers() as well. res.getCreatedUsers().forEach(user -> System.out.println(user.getUserId())); res.getFailedUsers().forEach(failed -> System.out.println(failed.getFailure())); } catch (DescopeException de) { // Handle the error } // --------------------------------------------------------------------------- // Option 2: create the users AND send each one an invitation. // InviteOptions.sendEmail is sent as sendMail on the API request. // --------------------------------------------------------------------------- try { UsersBatchResponse res = us.inviteBatch(batchUsers, InviteOptions.builder() .inviteUrl("https://company.com/sign-in") .sendEmail(true) .sendSMS(false) .templateOptions(Map.of("k1", "v1")) .build()); res.getCreatedUsers().forEach(user -> System.out.println(user.getUserId())); res.getFailedUsers().forEach(failed -> System.out.println(failed.getFailure())); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # users (Array): Each user hash accepts the same fields as create_user. A user must have a login_id, other fields are optional: # login_id (str): user login_id. # email (str): Optional user email address. # phone (str): Optional user phone number. # name (str): Optional user display name. # role_names (Array): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the user_tenants roles, which take precedence over them. # user_tenants (Array): An optional list of the user's tenants, and optionally their roles per tenant. # custom_attributes (Hash): Optional, set the different custom attributes values of the keys that were previously configured in the Descope console app. # picture (str): Optional url for user picture. # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # additional_identifiers (Array): An optional list of additional login IDs to associate with the user. # password (str): Optional cleartext password for the new user. Batches containing a cleartext password are capped at 100 users. # hashed_password (Hash): Optional, import an existing password hash instead of a cleartext password. users = [ { login_id: 'desmond@descope.com', email: 'desmond@descope.com', name: 'Desmond Copeland', user_tenants: descope_client.associated_tenants_to_hash_array( [{ tenant_id: 'my-tenant-id', role_names: ['role-name1'] }] ), }, { login_id: 'joe@descope.com', email: 'joe@descope.com', name: 'Joe Person', user_tenants: descope_client.associated_tenants_to_hash_array( [{ tenant_id: 'my-tenant-id', role_names: ['role-name2'] }] ), }, { # Optionally set a cleartext password for the new user (subject to the 100-user batch cap noted above). login_id: 'jane@descope.com', email: 'jane@descope.com', password: 'cleartext-password', }, ] begin resp = descope_client.create_batch_users(users) puts 'Successfully batch created users.' puts resp rescue Descope::AuthException => e puts "Unable to batch create users. Error: #{e.message}" end ``` ```php use Descope\SDK\Management\UserObj; use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\Management\Password\UserPasswordSha; // Args (UserObj): Each UserObj represents a user to batch create/invite. A user must have a loginId, other fields are optional. // status (string|null): Optional status for the user. Can be one of "enabled", "disabled", or "invited". $users = [ new UserObj( 'batchuser1', // loginId 'batchuser1@example.com', // email '+14155551111', // phone 'Batch User One', // displayName 'Batch', // givenName null, // middleName 'One', // familyName ['admin'], // roleNames [['tenantId' => 'tenant1']], // userTenants (can be an empty array if no tenant) null, // picture null, // customAttributes true, // verifiedEmail true, // verifiedPhone null, // additionalLoginIds null, // ssoAppIds null, // password 'enabled' // status (optional: "enabled", "disabled", "invited") ), new UserObj( 'batchuser2', // loginId 'batchuser2@example.com', // email '+14155552222', // phone 'Batch User Two', // displayName 'Batch', // givenName null, // middleName 'Two', // familyName ['viewer'], // roleNames [['tenantId' => 'tenant2']], // userTenants (can be an empty array if no tenant) null, // picture null, // customAttributes true, // verifiedEmail null, // verifiedPhone null, // additionalLoginIds null, // ssoAppIds null, // password 'invited' // status (optional: "enabled", "disabled", "invited") ), ]; // Optionally set a cleartext password for the new user, by passing a UserPassword wrapping a cleartext string. // Batches containing a cleartext password are capped at 100 users. $cleartextUser = new UserObj( 'batchuser3', // loginId 'batchuser3@example.com', // email null, null, null, null, null, // phone, displayName, givenName, middleName, familyName null, null, null, null, // roleNames, userTenants, picture, customAttributes true, null, null, null, // verifiedEmail, verifiedPhone, additionalLoginIds, ssoAppIds new UserPassword('cleartext-password'), 'enabled' // status ); $users[] = $cleartextUser; // To import a user with a pre-hashed SHA password, pass a UserPassword wrapping a UserPasswordSha: $hashedUser = new UserObj( 'batchuser4', // loginId 'batchuser4@example.com', // email null, null, null, null, null, // phone, displayName, givenName, middleName, familyName null, null, null, null, // roleNames, userTenants, picture, customAttributes true, null, null, null, // verifiedEmail, verifiedPhone, additionalLoginIds, ssoAppIds new UserPassword(null, new UserPasswordSha( '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8', // hash 'sha256' // type )), 'enabled' // status ); $users[] = $hashedUser; // Batch create users WITHOUT sending an invite. // Use this to provision users directly in an "enabled" (active) state. $response = $descopeSDK->management->user->createBatch($users); print_r($response); // Batch create users AND send them an invite. // inviteUrl (string|null): URL to include in the invitation for the user to sign in with. $inviteUrl = 'https://myapp.com/invite'; // sendMail (bool|null): true to send the invite via email. $sendMail = true; // sendSms (bool|null): true to send the invite via SMS. $sendSms = false; $response = $descopeSDK->management->user->inviteBatch($users, $inviteUrl, $sendMail, $sendSms); print_r($response); ``` ```csharp // Args: // batchUsers (List): Users to create (can include cleartext or hashed passwords). // CreatedTime (int?): Unix timestamp in seconds to set as the user's creation date, for preserving signup dates when migrating users. Must be non-negative and not in the future. var batchUsers = new List { new CreateUsers { LoginId = "email1@company.com", Email = "email1@company.com", Name = "user-one", UserTenants = new List { new AssociatedTenant { TenantId = "Tenant-123", RoleNames = new List { "Member" } } }, Status = EnumValues.UserStatus.Invited, CreatedTime = 1609459200 }, new CreateUsers { LoginId = "email2@company.com", Email = "email2@company.com", Name = "user-two", Status = EnumValues.UserStatus.Enabled } }; // Optionally set a cleartext password for the new user (batches containing a cleartext password are capped at 100 users). batchUsers.Add(new CreateUsers { LoginId = "email3@company.com", Email = "email3@company.com", Password = "cleartext-password" }); // To import a user with a pre-hashed SHA password, set HashedPassword.Sha instead of a cleartext password: batchUsers.Add(new CreateUsers { LoginId = "email4@company.com", Email = "email4@company.com", HashedPassword = new PasswordImport { Sha = new PasswordImportSha { Hash = "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8", Type = "sha256" } } }); // Build the batch request. Invite-related settings live directly on the request // Invite (bool?) : if true, send invitations via email/SMS // InviteUrl (string?) : optional custom invite URL // SendMail (bool?) : send invite over email // SendSMS (bool?) : send invite over SMS // TemplateId / TemplateOptions : optional message template settings var batchRequest = new CreateUsersRequest { Users = batchUsers, Invite = true, // InviteUrl = "https://your-app.com/invite", // SendMail = true, // SendSMS = false, }; try { var batchRes = await descopeClient.Mgmt.V1.User.Create.Batch.PostAsync(batchRequest); // batchRes.CreatedUsers -> List // batchRes.FailedUsers -> List (batch can partially fail) foreach (var user in batchRes!.CreatedUsers!) { Console.WriteLine(user.LoginIds?.FirstOrDefault()); } } catch (DescopeException ex) { // Handle the error } ``` ### Invite User This operation creates a new user (when you pass a login ID) and sends them an invitation. In the Node.js and Go SDKs, you can also re-invite an existing user by passing their user ID instead. For the .NET SDK, user invitation is handled in the same function call as user creation. To invite users, set `Invite = true` on the `CreateUserRequest` or `CreateUsersRequest`. In the Node.js and Go SDKs, the identifier also accepts a User ID. A login ID creates the user if they do not exist. A user ID requires an existing user — no new user is created, and the invite is resent. When inviting users from the SDK, the default connector and template configured within [Project Settings](https://app.descope.com/settings/project) will be used, unless a different template Id is specified. Currently, when using the Java SDK, only the default connector and template can be used. ```javascript // loginIdOrUserId (str): user login ID or user ID. const loginIdOrUserId = "custom-login-id"; // displayName (str): Optional user display name. const displayName = "Joe Person"; // phone (str): Optional user phone number. const phone = "+15555555555"; // email (str): Optional user email address. const email = "email@company.com"; // userTenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. const userTenants = [{ tenantId: "TestTenant", roleNames: ["TestRole"] }]; // roles (List[str]): An optional list of the user's role names without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. const roles = ["Tenant Admin"]; // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = { attribute1: "Value 1", attribute2: "Value 2" }; // picture (str): Optional url for user picture const picture = "https://example.com/picture.jpg"; // verifiedEmail (bool): Set to true for the user to be able to login with the email address. const verifiedEmail = true; // or false // verifiedPhone (bool): Set to true for the user to be able to login with the phone number. const verifiedPhone = true; // or false // additionalLoginIds (optional List[str]): An optional list of additional login IDs to associate with the user const additionalLoginIds = ["MyUserName", "+12223334455"]; // inviteUrl (str): URL to include in user invitation for the user to sign in with const inviteUrl = "https://company.com/sign-in" // sendMail (bool): true or false for sending invite via email const sendMail = true // sendSMS (bool): true or false for sending invite via SMS const sendSMS = false // templateId (str): Optional template Id for the invitation message const templateId = "my-template-id" // locale (str): Optional locale for the invitation message. Only takes effect when the selected template has translations configured; otherwise Descope uses the template's source language. const locale = "es" // A user must have a login ID or user ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. const resp = await descopeClient.management.user.invite( loginIdOrUserId, { email, phone, displayName, // picture, verifiedEmail, verifiedPhone, customAttributes, additionalLoginIds, // userTenants,// either userTenants or roles roles, inviteUrl, sendSMS, sendMail, templateId, locale, } ); if (!resp.ok) { console.log("Failed to invite user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully invited user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): user login_id. # email (str): Optional user email address. # phone (str): Optional user phone number. # display_name (str): Optional user display name. # role_names (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. # user_tenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. user_tenants=[AssociatedTenant("TestTenant")] # picture (str): Optional url for user picture # custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app # verified_email (bool): Set to true for the user to be able to login with the email address. # verifiedPhone (bool): Set to true for the user to be able to login with the phone number. # invite_url // URL to include in user invitation for the user to sign in with # send_mail (bool): true or false for sending invite via email # send_sms (bool): true or false for sending invite via SMS # additional_login_ids (List[str]): An optional list of additional login IDs to associate with the user # template_id (str): optional template Id for the invitation message # locale (str): Optional locale for the invitation message. Only takes effect when the selected template has translations configured; otherwise Descope uses the template's source language. # A user must have a login ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. try: resp = descope_client.mgmt.user.invite( login_id="email@company.com", email="email@company.com", display_name="Joe Person", phone="+15555555555", # You can update user_tenants or role_names, not both in the same action # user_tenants=user_tenants, role_names=["TestRole"], picture="xxxx", custom_attributes={"attribute1": "Value 1", "attribute2": "Value 2"}, verified_email=True, verified_phone=True, invite_url="https://company.com/sign-in", send_mail = True, send_sms = False, additional_login_ids = ["MyUserName", "+12223334455"], template_id = "my-template-id", locale = "es" ) print ("Successfully invited user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to invite user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): user loginID or userID. loginIDOrUserID := "xxxx" // userRequest (UserRequest): A completed descope structure with applicable details for the user userReq := &descope.UserRequest{} userReq.Email = "email@company.com" userReq.Name = "Joe Person" userReq.Roles = nil // or []string{"TestRole1","TestRole2"} however, if roles is given, Tenants should then be nil userReq.Tenants = []*descope.AssociatedTenant{{TenantID: "TestTenant"}} userReq.CustomAttributes = map[string]any{"attribute1": "Value 1", "attribute2": "Value 2"} userReq.TemplateOptions = map[string]any{"option1": "Value 1", "option2": "Value 2"} userReq.Picture = "xxxx" VerifiedEmail := true // or false userReq.VerifiedEmail = &VerifiedEmail VerifiedPhone := true // or false userReq.VerifiedPhone = &VerifiedPhone userReq.AdditionalLoginIds = ["MyUserName", "+12223334455"] userReq.templateID = "my-template-id" // optional template Id for the invitation message // invite options (&descope.InviteOptions{}): Details of the invite configuration including inviteURL inviteOptions := &descope.InviteOptions{InviteURL: "https://company.com/signIn", sendSMS: false, sendEmail: true, Locale: "es"} // A user must have a login ID or user ID, other fields are optional. Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. res, err := descopeClient.Management.User().Invite(ctx, loginIDOrUserID, userReq, inviteOptions) if (err != nil){ fmt.Println("Unable to invite user: ", err) } else { fmt.Println("User Successfully invited: ", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. // You can configure the invite URL in the Descope console prior to using this function, or pass inviteUrl in InviteOptions. UserService us = descopeClient.getManagementServices().getUserService(); try { us.invite("email@company.com", UserRequest.builder() .email("email@company.com") .verifiedEmail(true) .phone("+15555555555") .verifiedPhone(false) .displayName("Joe Person") .givenName("Joe") .middleName("A") .familyName("Person") .picture("https://example.com/picture.jpg") .roleNames(Arrays.asList("role-name1")) .userTenants(Arrays.asList( AssociatedTenant.builder() .tenantId("tenant-ID1") .roleNames(Arrays.asList("role-name1")) .build(), AssociatedTenant.builder() .tenantId("tenant-ID2") .build())) .customAttributes(Map.of("key1", "value1")) .additionalIdentifiers(Arrays.asList("MyUserName", "+12223334455")) .ssoAppIds(Arrays.asList("app-id-1")) .build(), InviteOptions.builder() .inviteUrl("https://my-app.com/invite") .sendEmail(true) .sendSMS(false) .templateId("my-template-id") .templateOptions(Map.of("option1", "value1")) .build() ); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): user login_id. # email (str): Optional user email address. Required (or login_id must be an email) since the invite is sent by email. # phone (str): Optional user phone number. # name (str): Optional user display name. # role_names (Array): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the user_tenants roles, which take precedence over them. # user_tenants (Array): An optional list of the user's tenants, and optionally their roles per tenant. # picture (str): Optional url for user picture. # custom_attributes (Hash): Optional, set the different custom attributes values of the keys that were previously configured in the Descope console app. # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # additional_identifiers (Array): An optional list of additional login IDs to associate with the user. # template_id (str): Optional template Id for the invitation message. # Make sure to configure the invite URL in the Descope console prior to using this function. # associated_tenants is an array of hashes, where each hash represents a tenant and the roles for that tenant. associated_tenants = [{ tenant_id: 'tenant_id1', role_names: %w[role_name1 role_name2] }] begin resp = descope_client.invite_user( login_id: 'desmond@descope.com', email: 'desmond@descope.com', name: 'Desmond Copeland', user_tenants: descope_client.associated_tenants_to_hash_array(associated_tenants), ) puts 'Successfully invited user.' puts resp rescue Descope::AuthException => e puts "Unable to invite user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->invite( 'newuser1', // loginId 'invite@example.com', // email '+1234567890', // phone 'New User', // displayName 'John', // givenName 'Middle', // middleName 'Doe', // familyName 'https://example.com/profile.jpg', // picture ['department' => 'Engineering'], // customAttributes true, // verifiedEmail true, // verifiedPhone 'https://myapp.com/invite', // inviteUrl true, // sendMail true // sendSms ); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to invite. var loginID = "user-login-id"; // createRequest (CreateUserRequest): User details; set Invite = true to send an invitation. var customAttributes = new CreateUserRequest_customAttributes(); customAttributes.AdditionalData = new Dictionary { { "attribute1", "Value 1" }, { "attribute2", "Value 2" } }; var createRequest = new CreateUserRequest { Identifier = loginID, Email = "email@company.com", Name = "Joe Person", Phone = "+15555555555", UserTenants = new List { new AssociatedTenant { TenantId = "TestTenant", RoleNames = new List { "Tenant Admin" } } }, CustomAttributes = customAttributes, Invite = true, InviteUrl = "https://company.com/sign-in", SendMail = true, SendSMS = false, TemplateId = "my-template-id" }; try { var userRes = await descopeClient.Mgmt.V1.User.Create.PostAsync(createRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update User This operation updates an existing user with the details provided. It is important to note that all parameters are used as overrides to the existing user; empty fields will override populated fields. If you wish to only update a subset of fields, reference the [Patch User](#patch-user) operation instead. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // loginIdOrUserId (str): user login ID or user ID. const loginIdOrUserId = "custom-login-id"; // displayName (str): Optional user display name. const displayName = "Joe Person"; // phone (str): Optional user phone number. const phone = "+15555555555"; // email (str): Optional user email address. const email = "email@company.com"; // userTenants (List[UserTenants]): An optional list of the user's tenants, and optionally, their roles per tenant. These roles are mutually exclusive with the general `role_names`, and take precedence over them. const userTenants = [{ tenantId: "TestTenant", roleNames: ["TestRole"] }]; // roles (List[str]): An optional list of the user's role names without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over them. const roles = ["Tenant Admin"]; // customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app const customAttributes = { attribute1: "Value 1", attribute2: "Value 2" }; // picture (str): Optional url for user picture const picture = "https://example.com/picture.jpg"; // verifiedEmail (bool): Set to true for the user to be able to login with the email address. const verifiedEmail = true; // or false // verifiedPhone (bool): Set to true for the user to be able to login with the phone number. const verifiedPhone = true; // or false // additionalLoginIds (optional List[str]): An optional list of additional login IDs to associate with the user const additionalLoginIds = ["MyUpdatedUserName", "+9999998765"]; const resp = await descopeClient.management.user.update( loginIdOrUserId, { email, phone, displayName, // picture, verifiedEmail, verifiedPhone, customAttributes, additionalLoginIds, // userTenants,// either userTenants or roles roles, } ); if (!resp.ok) { console.log("Failed to update user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user to update. # email (str): Optional user email address. # phone (str): Optional user phone number. # display_name (str): Optional user display name. user = {"login_id": "email@company.com", "display_name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # role_names (List[str]): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the `user_tenant` roles, which take precedence over the general roles. # custom_attributes: Optional map of custom attribute keys to values. Use None/null for a key to clear that attribute. # custom_attributes = {"mycustomattribute": None} # clears that attribute # picture (str): Optional url to user avatar. Leave empty to remove. # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # additional_login_ids (optional List[str]): An optional list of additional login IDs to associate with the user try: resp = descope_client.mgmt.user.update( login_id=user["login_id"], email="updateEmail@email.com", display_name=user["display_name"], phone="+12222222222", # You can update user_tenants or role_names, not both in the same action role_names=["TestyTester"], # user_tenants=[AssociatedTenant("testWithID")], picture="https://example.com/picture.png", custom_attributes={"mycustomattribute": "Test"}, verified_email=True, verified_phone=True, additional_login_ids=["MyUserName", "+12223334455"] ) print ("Successfully updated user. New user info:") # Display Updates try: resp = descope_client.mgmt.user.load(login_id=user["login_id"]) print ("Successfully loaded user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load user after update.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) except AuthException as error: print ("Unable to update user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): user loginID or userID. loginIDOrUserID := "xxxx" // userRequest (UserRequest): A completed descope structure with applicable details for the user userReq := &descope.UserRequest{} userReq.Email = "email@company.com" userReq.Name = "Joe Person" userReq.Phone = "+12223334455" userReq.Roles = nil // or []string{"TestRole1","TestRole2"} however, if roles is given, Tenants should then be nil userReq.Tenants = []*descope.AssociatedTenant{{TenantID: "TestTenant"}} userReq.CustomAttributes = map[string]any{"mycustomattribute": "Test"} userReq.Picture = "https://example.com/picture.png" VerifiedEmail := true // or false userReq.VerifiedEmail = &VerifiedEmail VerifiedPhone := true // or false userReq.VerifiedPhone = &VerifiedPhone userReq.AdditionalLoginIds = ["MyUserName", "+12223334455"] res, err := descopeClient.Management.User().Update(ctx, loginIDOrUserID, userReq) if (err != nil){ fmt.Println("Unable to update user: ", err) } else { fmt.Println("User Successfully updated", res) } ``` ```java // A user must have a loginID or userID, other fields are optional. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .userTenants(Arrays.asList( .userTenants(Arrays.asList( AssociatedTenant.builder() .tenantId("tenant-ID1") .roleNames(Arrays.asList("role-name1")) .build(), AssociatedTenant.builder() .tenantId("tenant-ID2") .build())) .additionalIdentifiers(Arrays.asList("MyUserName", "+12223334455")) .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # email (str): Optional user email address. # phone (str): Optional user phone number. # name (str): Optional user display name. # given_name / middle_name / family_name (str): Optional name components. # role_names (Array): An optional list of the user's roles without tenant association. These roles are mutually exclusive with the user_tenants roles, which take precedence over them. # user_tenants (Array): An optional list of the user's tenants, and optionally their roles per tenant. # picture (str): Optional url for user picture. # custom_attributes (Hash): Optional, set the different custom attributes values of the keys that were previously configured in the Descope console app. # verified_email (bool): Set to true for the user to be able to login with the email address. # verified_phone (bool): Set to true for the user to be able to login with the phone number. # additional_identifiers (Array): An optional list of additional login IDs to associate with the user. associated_tenants = [{ tenant_id: 'tenant_id1', role_names: %w[role_name1 role_name2] }] # Update will override all fields as is. Use carefully. begin resp = descope_client.update_user( login_id: 'desmond@descope.com', email: 'desmond@descope.com', phone: '+15555555555', name: 'Desmond Copeland', given_name: 'Desmond', family_name: 'Copeland', picture: 'https://example.com/picture.jpg', custom_attributes: { 'attribute1' => 'Value 1', 'attribute2' => 'Value 2' }, verified_email: true, verified_phone: true, additional_identifiers: ['MyUserName', '+12223334455'], # You can update user_tenants or role_names, not both in the same action # role_names: %w[role-name1], user_tenants: descope_client.associated_tenants_to_hash_array(associated_tenants) ) puts 'Successfully updated user.' puts resp rescue Descope::AuthException => e puts "Unable to update user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->update( 'testuser1', // loginId 'updatedemail@example.com', // email '+1234567890', // phone 'Updated User', // displayName 'Updated', // givenName 'Middle', // middleName 'User', // familyName 'https://example.com/newpic.jpg', // picture ['department' => 'HR'], // customAttributes true, // verifiedEmail true, // verifiedPhone ['altUser1'], // additionalLoginIds [''], // ssoAppIds ); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // updateRequest (UpdateUserRequest): A full/partial user object whose non‑null fields will override existing values. Use carefully. var customAttributes = new UpdateUserRequest_customAttributes(); customAttributes.AdditionalData = new Dictionary { { "attribute1", "New Value" } }; var updateRequest = new UpdateUserRequest { Identifier = loginID, Email = "email@company.com", Name = "updated-name", Phone = "+15555555555", CustomAttributes = customAttributes, RoleNames = new List { "Role1" }, UserTenants = new List { new AssociatedTenant { TenantId = "Tenant-123", RoleNames = new List { "Role2" } } } }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Patch User This operation updates only the fields provided for an existing user, leaving all other fields unchanged. Unlike [Update User](/management/user-management/sdks#update-user), omitted fields will not override existing values. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // loginIdOrUserId (str): user login ID or user ID. const loginIdOrUserId = "desmond@descope.com"; // options (PatchUserOptions): Only the fields you provide will be updated. const options = { displayName: "Desmond Copeland Jr.", // email: "new@company.com", // phone: "+15555555555", // verifiedEmail: true, // verifiedPhone: true, // customAttributes: { attribute1: "Value 1" }, // picture: "https://example.com/picture.jpg", // roles: ["Tenant Admin"], // userTenants: [{ tenantId: "TestTenant", roleNames: ["TestRole"] }], }; const resp = await descopeClient.management.user.patch(loginIdOrUserId, options); if (!resp.ok) { console.log("Failed to patch user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully patched user.") console.log(resp.data) } ``` ```python import json # Args: # login_id (str): The login ID or User ID of the user to patch. # All other fields are optional — only provided fields will be updated. try: resp = descope_client.mgmt.user.patch( login_id="desmond@descope.com", display_name="Desmond Copeland Jr.", # email="new@company.com", # phone="+15555555555", # verified_email=True, # verified_phone=True, # custom_attributes={"attribute1": "Value 1"}, # picture="https://example.com/picture.jpg", # role_names=["TestRole"], # user_tenants=[AssociatedTenant("TestTenant")], ) print("Successfully patched user.") print(json.dumps(resp, indent=2)) except AuthException as error: print("Unable to patch user.") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): user loginID or userID. loginIDOrUserID := "desmond@descope.com" // userRequest (UserRequest): Only the fields you set will be updated; unset fields are left unchanged. userReq := &descope.UserRequest{} userReq.Name = "Desmond Copeland Jr." // userReq.Email = "new@company.com" // userReq.Phone = "+15555555555" // userReq.CustomAttributes = map[string]any{"attribute1": "Value 1"} res, err := descopeClient.Management.User().Patch(ctx, loginIDOrUserID, userReq) if err != nil { fmt.Println("Unable to patch user: ", err) } else { fmt.Println("User successfully patched", res) } ``` ```java // Only the fields provided in PatchUserRequest will be updated; all other fields remain unchanged. UserService us = descopeClient.getManagementServices().getUserService(); try { us.patch("desmond@descope.com", PatchUserRequest.builder() .name("Desmond Copeland Jr.") // .email("new@company.com") // .phone("+15555555555") .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID or User ID of the user to patch. # All other fields are optional — only provided fields will be updated. begin resp = descope_client.patch_user( login_id: 'desmond@descope.com', display_name: 'Desmond Copeland Jr.', # email: 'new@company.com', # phone: '+15555555555', # given_name: 'Desmond', # family_name: 'Copeland Jr.', # picture: 'https://example.com/picture.jpg', # custom_attributes: { 'attribute1' => 'Value 1' }, # verified_email: true, # verified_phone: true, # user_tenants: descope_client.associated_tenants_to_hash_array(associated_tenants), ) puts 'Successfully patched user.' puts resp rescue Descope::AuthException => e puts "Unable to patch user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->patch( 'testuser1', // loginId (login ID or user ID) 'patched@example.com', // email null, // phone 'Patched Name', // displayName null, // givenName null, // middleName null, // familyName ['admin'], // roleNames null, // userTenants ['department' => 'HR'], // customAttributes null, // picture null, // verifiedEmail null, // verifiedPhone null // ssoAppIds ); print_r($response); ``` ```csharp // Args: // Identifier (string): The login ID or user ID of the user to patch (required). // All other fields are optional — only provided fields will be updated. var request = new PatchUserRequest { Identifier = "desmond@descope.com", Name = "Desmond Copeland Jr.", // Email = "new@company.com", // Phone = "+15555555555", // GivenName = "Desmond", // FamilyName = "Copeland Jr.", }; try { var userRes = await descopeClient.Mgmt.V1.Mgmt.User.Patch.PatchAsync(request); } catch (DescopeException ex) { // Handle the error } ``` ### Load Existing User Details This operation loads the details of an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. Suppose you frequently load a user for a specific user detail, such as their email address or a particular custom attribute. In that case, you can save execution time and additional API/SDK calls to load the user by adding the items to the custom claim. For details on adding items to the custom claims, see [this documentation](/flows/actions/custom-claims). ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to be loaded. const loginIdOrUserId = "xxxx" let resp = await descopeClient.management.user.load(loginIdOrUserId) if (!resp.ok) { console.log("Failed to load user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded user.") console.log(resp.data) } ``` The older `loadByUserId(userId)` method is still supported for backward compatibility. ```python # Args: # login_id (str): The login_id of the user to be loaded. try: resp = descope_client.mgmt.user.load(login_id="xxxx") print ("Successfully loaded user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) # If needed, users can be loaded using the user_id as well. The response is the same as above. try: user_resp = descope_client.mgmt.user.load_by_user_id(user_id="xxxx") print ("Successfully loaded user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to be loaded. loginIDOrUserID := "xxxx" res, err := descopeClient.Management.User().Load(ctx, loginIDOrUserID) if (err != nil){ fmt.Println("Unable to load user: ", err) } else { fmt.Println("User Successfully loaded: ", res) } ``` The older `LoadByUserID(ctx, userID)` method is still supported for backward compatibility. ```java // A user must have a loginID, other fields are optional. UserService us = descopeClient.getManagementServices().getUserService(); // If needed, users can be loaded using their ID as well try { us.loadByUserId(""); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login_id of the user to be loaded. # Load specific user begin user_resp = descope_client.load_user('desmond@descope.com') user = user_resp['user'] puts 'Successfully loaded user.' puts user rescue Descope::AuthException => e puts "Unable to load user. Error: #{e.message}" end # Args: # user_id (str): The user ID of the user to be loaded. # If needed, users can be loaded using the user ID as well. The response is the same as above. begin user_resp = descope_client.load_by_user_id('') user = user_resp['user'] puts 'Successfully loaded user.' puts user rescue Descope::AuthException => e puts "Unable to load user. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to load. var loginID = "user-login-id"; try { var userRes = await descopeClient.Mgmt.V1.User.GetWithIdentifierAsync(loginID); } catch (DescopeException ex) { // Handle the error } ``` Each entry in the `userTenants` field of the response includes the user's tenant-level roles and permissions, allowing you to retrieve a user's authorization information without making additional role lookup calls. ### Get User's Login History Retrieve users' authentication history, by the given user's ids. You get one entry per login, with these fields: | Field | Type | Description | |-------|------|-------------| | `userId` | string | The ID of the user who logged in. | | `loginTime` | int | When the login happened, as a Unix timestamp in seconds. | | `city` | string | The city the login came from. | | `country` | string | The country the login came from. | | `ip` | string | The IP address the login came from. | | `selectedTenant` | string | The ID of the tenant the user signed in to. | `selectedTenant` holds the tenant that became the user's active tenant for that login, matching the [`dct` claim](/management/token#user-session-and-refresh-tokens) in the session JWT. Descope sets it when a tenant arrives in the flow's start options, when the user picks one through the [Tenant Select component](/flows/screens/inputs/tenantselect-component), or when the user belongs to a single tenant and your JWT template has [Set active tenant claim automatically](/management/token/jwt-templates#authorization-claims-configuration) turned on. Every other login returns an empty string, including logins recorded before the field existed. The `selectedTenant` field is available through the Descope API and the Python and Go SDKs. ```javascript // Args: // userIds (list[str]): user IDs to load history for. const userIds = ["xxxx", "yyyy"] const resp = await descopeClient.management.user.history(userIds); if (!resp.ok) { console.log("Failed to load users history.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded users history.") console.log(resp.data) } ``` ```python # Args: # user_ids (list[str]): user IDs to load history for. try: resp = descope_client.mgmt.user.history(user_ids=["xxxx", "yyyy"]) print ("Successfully loaded users history.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Failed to load users history.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userIds (list[str]): user IDs to load history for. userIds := ["xxxx", "yyyy"] res, err := descopeClient.Management.User().History(ctx, userIds) if (err != nil){ fmt.Println("Failed to load users history. ", err) } else { fmt.Println("Successfully loaded users history. ") for _, u := range res { fmt.Println(u) } } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); try { us.history(["xxxx", "yyyy"]); } catch (DescopeException de) { // Handle the error } ``` ### Load Existing User's Provider Token This operation loads the user's access token generated by the OAuth/OIDC provider, using a valid management key. When querying for OAuth providers, this only applies when utilizing your own account with the provider and have selected `Manage tokens from provider` selected under the [social auth methods](https://app.descope.com/settings/authentication/social). ```javascript // Args: // loginId (str): The login_id of the user to be loaded. const loginId = "xxxx" // provider (str): The provider name (google, facebook, etc') const provider = "google" const resp = await descopeClient.management.user.getProviderToken(loginId, provider) if (!resp.ok) { console.log(resp) console.log("Unable to load user's provider token.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded user's provider token.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user to be loaded. # provider (str): The provider name (google, facebook, etc') try: resp = descope_client.mgmt.user.get_provider_token(login_id="xxxx", provider="google") print ("Successfully loaded user's provider token.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to load user's provider token.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The loginID of the user to be loaded. loginID := "xxxx" // provider (str): The provider name (google, facebook, etc') provider := "google" res, err := descopeClient.Management.User().GetProviderToken(ctx, loginID, provider) if (err != nil){ fmt.Println("Unable to load user's provider token: ", err) } else { fmt.Println("Successfully loaded user's provider token.", res) } ``` ```ruby # Args: # login_id (str): The login_id of the user to be loaded. # provider (str): The provider name (google, facebook, etc'). begin resp = descope_client.get_provider_token(login_id: 'desmond@descope.com', provider: 'google') puts "Successfully loaded user's provider token." puts resp rescue Descope::AuthException => e puts "Unable to load user's provider token. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user whose provider token you want. var loginID = "user-login-id"; // provider (string): The social provider (e.g., "google", "facebook"). var provider = "google"; try { var tokenRes = await descopeClient.Mgmt.V1.User.Provider.Token.GetAsync(config => { config.QueryParameters.LoginId = loginID; config.QueryParameters.Provider = provider; }); } catch (DescopeException ex) { // Handle the error } ``` ### Search Users This operation returns user details based on the applicable search. Pass `null` for a key in `customAttributes` to match users where that attribute is unset — the same way `null` clears an attribute on **[Update a User's Custom Attributes](#update-a-users-custom-attributes)**. ```javascript // Args: // tenantIds (List[str]): Optional list of tenant IDs to filter by const tenantIds = ["Test1", "Test2", "Test3"] // roleNames (List[str]): Optional list of role names to filter by const roleNames = ["TestRole1", "TestRole2", "TestRole3"] // limit (number): Optional limit of the number of users returned. Leave empty for default. const limit = 1 // page (number): Optional pagination control. Pages start at 0 and must be non-negative. const page = 0 // testUsersOnly: boolean: Given true, it will only return test users. const testUsersOnly = false // withTestUser: boolean: Given true, it will also return test users. False will omit test users. const withTestUser = true // customAttributes: Record: Searches users with certain custom attributes. // Pass null for a key to match users where that attribute is unset. const customAttributes = {"mycustomattribute": "Test", "unsetattribute": null} // statuses (List[str]): a list of statuses to search users for, the options are: "invited", "enabled", "disabled" const statuses = ["invited", "enabled", "disabled"] // emails (List[str]): Optional list of emails to search for const emails = ["email@company.com"] // phones (List[str]): Optional list of phones to search for const phones = ["+12223334444"] // userIds (List[str]): Optional list of user IDs to search for const userIds = ["user-id-1"] // sort (List[str]): Optional list of fields to sort by. const sort = [{ field: "displayName", desc: true }] // text (str): Optional full text search across relevant columns. const text = "" // fromCreatedTime (number): Optional search parameter returning users who were created on or after this time (in Unix epoch milliseconds). const fromCreatedTime = 1735689600000 // toCreatedTime (number): Optional search parameter returning users who were created on or before this time (in Unix epoch milliseconds). const toCreatedTime = 1738368000000 // fromModifiedTime (number): Optional search parameter returning users whose last modification/update occurred on or after this time (in Unix epoch milliseconds). const fromModifiedTime = 1735689600000 // toModifiedTime (number): Optional search parameter returning users whose last modification/update occurred on or before this time (in Unix epoch milliseconds). const toModifiedTime = 1738368000000 // tenantRoleIds (Record): Optional map of tenant IDs to role IDs to filter by. Set and: true to require all listed roles instead of any. const tenantRoleIds = { Test1: { values: ['role-id-1', 'role-id-2'], and: true } } // tenantRoleNames (Record): Optional map of tenant IDs to role names to filter by. Set and: true to require all listed roles instead of any. const tenantRoleNames = { Test2: { values: ['TestRole1', 'TestRole2'], and: false } } // Search all users with no filter: let resp = await descopeClient.management.user.search({}) // Search users with limit filter: let resp = await descopeClient.management.user.search({ limit: 10 }) // Search users with tenant filter: let resp = await descopeClient.management.user.search({ tenantIds: ['Test1', 'Test2'] }) // Search users with role filter: let resp = await descopeClient.management.user.search({ roleNames: ['TestRole1', 'TestRole2'] }) // Search users with a combination of filters: let resp = await descopeClient.management.user.search({ tenantIds: ['Test1', 'Test2'], roleNames: ['TestRole1', 'TestRole2'], fromCreatedTime, toCreatedTime, fromModifiedTime, toModifiedTime, tenantRoleIds, tenantRoleNames }); if (!resp.ok) { console.log("Failed to search users.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully searched users.") console.log(resp.data) } ``` ```python # Args: # tenant_ids (List[str]): Optional list of tenant IDs to filter by # role_names (List[str]): Optional list of role names to filter by # limit (int): Optional limit of the number of users returned. Leave empty for default. # page (int): Optional pagination control. Pages start at 0 and must be non-negative. # test_users_only: boolean: Given true, it will only return test users. # with_test_user: boolean: Given true, it will also return test users. False will omit test users. # custom_attributes: dict: Searches users with certain custom attributes # statuses (List[str]): a list of statuses to search users for, the options are: "invited", "enabled", "disabled" # emails (List[str]): Optional list of emails to search for # phones (List[str]): Optional list of phones to search for # user_ids (List[str]): Optional list of user IDs to search for user_ids = ["user-id-1"] # sort (List[str]): Optional list of fields to sort by. # text (str): Optional full text search across relevant columns. # from_created_time (int): Optional search parameter returning users who were created on or after this time (in Unix epoch milliseconds). # to_created_time (int): Optional search parameter returning users who were created on or before this time (in Unix epoch milliseconds). # from_modified_time (int): Optional search parameter returning users whose last modification/update occurred on or after this time (in Unix epoch milliseconds). # to_modified_time (int): Optional search parameter returning users whose last modification/update occurred on or before this time (in Unix epoch milliseconds). # tenant_role_ids (dict): Optional mapping of tenant ID to role IDs. Dict value is in the form of {"tenant_id": {"values":["role_id1", "role_id2"], "and": True}} # tenant_role_names (dict): Optional mapping of tenant ID to role names. Dict value is in the form of {"tenant_id": {"values":["role_name1", "role_name2"], "and": True}} try: # Search all users with no filter: resp = descope_client.mgmt.user.search_all() # Search all users with limit filter: resp = descope_client.mgmt.user.search_all(limit=limit) # Search all users with tenant filter: resp = descope_client.mgmt.user.search_all(tenant_ids=tenant_ids) # Search all users with role filter: resp = descope_client.mgmt.user.search_all(role_names=role_names) # Search all users with a combination of filters: resp = descope_client.mgmt.user.search_all(tenant_ids=["Test1", "Test2", "Test3"], role_names=["TestRole1", "TestRole2", "TestRole3"], limit=1, page=0, test_users_only=False, with_test_user=True, custom_attributes={"mycustomattribute": "Test"}, statuses=["invited", "enabled", "disabled"], emails=["email@company.com"], phones=["+12223334444"], sort=[{"field": "displayName", "desc": True}], text="", from_created_time=1735689600000, to_created_time=1735689600000, from_modified_time=1735689600000, to_modified_time=1735689600000, tenant_role_ids={"Test1": {"values": ["role-id-1", "role-id-2"], "and": True}}, tenant_role_names={"Test2": {"values": ["TestRole1", "TestRole2"], "and": False}}) print ("Successfully searched users.") users = resp["users"] for user in users: print(print(json.dumps(user, indent=2))) except AuthException as error: print ("Unable to search users.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // searchOptions: Options for searching and filtering users // TenantIds(List[str]): Optional list of tenant IDs to filter by // Roles (List[str]): Optional list of role names to filter by // Limit (int32): Optional limit of the number of users returned. Leave empty for default. // Page (int): The page parameter allow to paginate over the results. Pages start at 0 and must non-negative. // WithTestUsers: boolean: Given true, it will also return test users. False will omit test users. // TestUsersOnly: boolean: Given true, it will only return test users. // CustomAttributes: map[string]any: Searches users with certain custom attributes. Set a key to nil to match users where that attribute is unset. // Emails (List[str]): Optional list of emails to search for // Phones (List[str]): Optional list of phones to search for // UserIds (List[str]): Optional list of user IDs to search for // Sort (List[str]): Optional list of fields to sort by. // Text (str): Optional full text search across relevant columns. // FromCreatedTime (int64): Optional search parameter returning users who were created on or after this time (in Unix epoch milliseconds). // ToCreatedTime (int64): Optional search parameter returning users who were created on or before this time (in Unix epoch milliseconds). // FromModifiedTime (int64): Optional search parameter returning users whose last modification/update occurred on or after this time (in Unix epoch milliseconds). // ToModifiedTime (int64): Optional search parameter returning users whose last modification/update occurred on or before this time (in Unix epoch milliseconds). // TenantRoleIDs (map[string]*RoleList): Optional map of tenant IDs to role IDs to filter by. RoleList.And requires all listed roles instead of any. // TenantRoleNames (map[string]*RoleList): Optional map of tenant IDs to role names to filter by. RoleList.And requires all listed roles instead of any. // Note - you can leave any of these items as nil as to not filter on them as well. searchOptions := &descope.UserSearchOptions{ TenantIDs: []string{"Test1", "Test2", "Test3"}, Roles: []string{"TestRole1", "TestRole2", "TestRole3"}, Limit: 5, Statuses: []descope.UserStatus{"invited", "enabled", "disabled"}, Page: 0, WithTestUsers: true, TestUsersOnly: false, CustomAttributes: map[string]any{"mycustomattribute": "Test", "unsetattribute": nil}, Emails: []string{"email@company.com"}, Phones: []string{"+12223334444"}, Sort: []descope.UserSearchSort{ {Field: "displayName", Desc: true}, }, Text: "", FromCreatedTime: 1735689600000, ToCreatedTime: 1738368000000, FromModifiedTime: 1735689600000, ToModifiedTime: 1738368000000, TenantRoleIDs: map[string]*descope.RoleList{"Test1": {Values: []string{"role-id-1", "role-id-2"}, And: true}}, TenantRoleNames: map[string]*descope.RoleList{"Test2": {Values: []string{"TestRole1", "TestRole2"}, And: false}}, } // Pagination: SearchAll returns one page at a time. `res` is only the current page, while `total` is // how many users match your filters across all pages. res, total, err := descopeClient.Management.User().SearchAll(ctx, searchOptions) if err != nil { fmt.Println("Unable to search users:", err) } else { fmt.Printf("Successfully searched users: got %d users (total matching: %d)\n", len(res), total) for _, u := range res { fmt.Println(u) } } ``` ```java // Args: // UserSearchRequest: the search filters. All fields are optional. // tenantIds (List): Optional list of tenant IDs to filter by // roleNames (List): Optional list of role names to filter by // tenantRoleIds (Map): Optional map of tenant IDs to role IDs to filter by. Set and(true) to require all listed roles instead of any. // tenantRoleNames (Map): Optional map of tenant IDs to role names to filter by. Set and(true) to require all listed roles instead of any. // limit (Integer): Optional limit of the number of users returned. Leave empty for default. // page (Integer): Optional pagination control. Pages start at 0 and must be non-negative. // withTestUser (Boolean): Given true, it will also return test users. False will omit test users. // testUsersOnly (Boolean): Given true, it will only return test users. // customAttributes (Map): Searches users with certain custom attributes. Set a key to null to match users where that attribute is unset (use a mutable map, e.g. new HashMap<>(), since Map.of() rejects null values). // statuses (List): a list of statuses to search users for, the options are: UserStatus.INVITED, UserStatus.ENABLED, UserStatus.DISABLED // emails (List): Optional list of emails to search for // phones (List): Optional list of phones to search for // loginIds (List): Optional list of login IDs to search for // userIds (List): Optional list of user IDs to search for // ssoAppIds (List): Optional list of Federated Application IDs to filter by // text (String): Optional full text search across relevant columns. // fromCreatedTime (Instant): Optional search parameter returning users who were created on or after this time. // toCreatedTime (Instant): Optional search parameter returning users who were created on or before this time. // fromModifiedTime (Instant): Optional search parameter returning users whose last modification/update occurred on or after this time. // toModifiedTime (Instant): Optional search parameter returning users whose last modification/update occurred on or before this time. // Note - you can leave out any of these fields so as not to filter on them. UserService us = descopeClient.getManagementServices().getUserService(); // Search all users, optionally according to tenant, role, free text and/or time filters. // Results can be paginated using the limit and page parameters. try { AllUsersResponseDetails response = us.searchAll(UserSearchRequest.builder() .tenantIds(Arrays.asList("Test1", "Test2")) .roleNames(Arrays.asList("TestRole1", "TestRole2")) .tenantRoleIds(Collections.singletonMap("Test1", RolesList.builder().values(Arrays.asList("role-id-1", "role-id-2")).and(true).build())) .tenantRoleNames(Collections.singletonMap("Test2", RolesList.builder().values(Arrays.asList("TestRole1", "TestRole2")).and(true).build())) .text("desmond") .limit(10) .page(0) .build()); // Pagination: getUsers() returns the current page only, while getTotal() is how many // users match your filters across all pages. for (UserResponse user : response.getUsers()) { // Do something } System.out.println("Total matching users: " + response.getTotal()); } catch (DescopeException e) { System.out.println("Unable to search users: " + e.getMessage()); } ``` ```ruby # Args: # login_id (str): Optional login ID to filter by. # tenant_ids (Array): Optional list of tenant IDs to filter by. # role_names (Array): Optional list of role names to filter by. # text (str): Optional full text search across relevant columns. # limit (int): Optional limit of the number of users returned. Leave empty for default. # page (int): Optional pagination control. Pages start at 0 and must be non-negative. # sso_only (bool): Given true, it will only return SSO users. # test_users_only (bool): Given true, it will only return test users. # with_test_user (bool): Given true, it will also return test users. False will omit test users. # custom_attributes (Hash): Searches users with certain custom attributes. Set a key to nil to match users where that attribute is unset. # statuses (Array): A list of statuses to search users for, the options are: "invited", "enabled", "disabled". # emails (Array): Optional list of emails to search for. # phones (Array): Optional list of phones to search for. # sso_app_ids (Array): Optional list of Federated Application IDs to filter by. # tenant_role_ids (Hash): Optional map of tenant IDs to lists of role IDs to filter by. # tenant_role_names (Hash): Optional map of tenant IDs to lists of role names to filter by. # Search all users, optionally according to specified filter(s). # Results can be paginated using the limit and page parameters. begin users_resp = descope_client.search_all_users( login_id: 'desmond@descope.com', tenant_role_ids: { 'Test1' => ['role-id-1', 'role-id-2'] }, tenant_role_names: { 'Test2' => ['TestRole1', 'TestRole2'] } ) users = users_resp['users'] users.each do |user| # Do something end rescue Descope::AuthException => e puts "Unable to search users. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->searchAll( "", // loginId [], // tenantIds ['admin', 'viewer'], // roleNames 50, // limit "", // text 1, // page false, // ssoOnly false, // testUsersOnly false, // withTestUser ['someattribute' => null], // customAttributes: matches users where someattribute is unset ['enabled'], // statuses ['user@example.com'], // emails ['+1234567890'], // phones ['ssoApp123'], // ssoAppIds [ // sort ['field' => 'displayName', 'desc' => true] ], [ // tenantRoleIds "Test1" => ["role-id-1", "role-id-2"] ], [ // tenantRoleNames "Test2" => ["TestRole1", "TestRole2"] ] ); print_r($response); ``` ```csharp // Args: // searchRequest (SearchUsersRequest): Fine‑tune filters for tenants and/or roles. Paginate by limit and page. var searchRequest = new SearchUsersRequest { TenantIds = new List { "Tenant-123", "Tenant-456" }, RoleNames = new List { "Role1", "Role2" }, Limit = 50, Page = 0, Sort = new List { new SortField { Field = "displayName", Desc = true } }, Text = "", // TenantRoleIds/TenantRoleNames are plain dictionaries with no typed "and" property. // Add an "and" key (true/false) to require all listed roles instead of any. TenantRoleIds = new SearchUsersRequest_tenantRoleIds { AdditionalData = new Dictionary { ["Test1"] = new Dictionary { ["values"] = new List { "role-id-1", "role-id-2" }, ["and"] = true } } }, TenantRoleNames = new SearchUsersRequest_tenantRoleNames { AdditionalData = new Dictionary { ["Test2"] = new Dictionary { ["values"] = new List { "TestRole1", "TestRole2" }, ["and"] = false } } } }; try { var users = await descopeClient.Mgmt.V2.User.Search.PostAsync(searchRequest); } catch (DescopeException ex) { // Handle the error } ``` Each entry in the `userTenants` field of the response includes the user's tenant-level roles and permissions, allowing you to retrieve a user's authorization information without making additional role lookup calls. ### Update a User's Email Address This operation allows administrators to update a user's email address. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update the email for. const loginIdOrUserId = "xxxx" // email (str): The new email address for the user. Leave empty to remove. const email = "xxxx@xxxxxx.xxx" // verified (bool): Set to true for the user to be able to login with the email address. const verified = true // or false // failOnConflict (bool, optional): false (default) merges with a conflicting user; // true fails the request before any merge happens. const failOnConflict = false // or true let resp = await descopeClient.management.user.updateEmail(loginIdOrUserId, email, verified, failOnConflict) if (!resp.ok) { console.log(resp) console.log("Failed to update user's email address.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's email address.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update the email for. # email (str): The new email address for the user. Leave empty to remove. # verified (bool): Set to true for the user to be able to login with the email address. # fail_on_conflict (bool, optional): False (default) merges with a conflicting user; True fails the request before any merge happens. try: resp = descope_client.mgmt.user.update_email(login_id="xxxx", email="xxxx@xxxxxx.xxx", verified=True, fail_on_conflict=False) print("Successfully updated user's email address.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's email address.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The login ID or userID of the user to update the email for. loginIDOrUserID := "xxxx" // email (str): The new email address for the user. Leave empty to remove. email := "xxxx@xxxxxx.xxx" // verified (bool): Set to true for the user to be able to login with the email address. verified := true // or false // failOnConflict (bool): REQUIRED — this is a breaking addition, existing calls must // be updated to pass a value. false merges with a conflicting user; true fails the // request before any merge happens. failOnConflict := false // or true res, err := descopeClient.Management.User().UpdateEmail(ctx, loginIDOrUserID, email, verified, failOnConflict) if (err != nil){ fmt.Println("Unable to update user's email address: ", err) } else { fmt.Println("Successfully updated user's email address: ", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. // failOnConflict is not currently available in a published Java SDK release. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .loginId("") .phone("980-012-1234") .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update the email for. # email (str): The new email address for the user. Leave empty to remove. # verified (bool): Set to true for the user to be able to login with the email address. # failOnConflict is not currently available in the Ruby SDK. begin resp = descope_client.update_email(login_id: 'desmond@descope.com', email: 'desmond@descope.com', verified: true) puts "Successfully updated user's email address." puts resp rescue Descope::AuthException => e puts "Unable to update user's email address. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // newEmail (string?): The new email address, or null to remove. var newEmail = "email@company.com"; // verified (bool): Must be true for the user to log in with this email. var verified = true; // failOnConflict is not currently available in the .NET SDK. var updateRequest = new UpdateUserEmailRequest { Identifier = loginID, Email = newEmail, Verified = verified }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Email.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` #### Handling login ID conflicts on update When updating a user's email or phone number to a value already assigned to a different user, the `failOnConflict` parameter controls whether the request fails or merges the two accounts. By default (`failOnConflict` omitted or `false`), the conflicting user is deleted and their data is folded into the user being updated; setting it to `true` fails the request instead, before any merge happens. Note that `failOnConflict: true` currently returns a `500`, while a default-path conflict during a concurrent update returns a retryable `409` (`E013017`) — the two failure modes aren't currently distinguishable by status code alone in the same way. See the [Update User Email](/api/management/users/update-user-email) and [Update User Phone](/api/management/users/update-user-phone) API references for the full request schema. **Known limitation:** when relying on the default merge behavior, the phone number can be dropped during the merge in some cases ([etc#4442](https://github.com/descope/etc/issues/4442), still open). Setting `failOnConflict: true` avoids this by preventing the merge entirely. **Note:** `failOnConflict` governs collisions with a different existing user's login ID. This is separate from `AddToLoginIDs`/`OnMergeUseExisting`, which intentionally links a second login ID to the same user. ### Update a User's Login ID This operation allows administrators to update a user's Login ID. If you'd like to remove a login ID, provide an empty string for the new login ID. ```javascript // Args: // login_id (str): The login ID of the user to update the Login ID for. const loginId = "xxxx" // new_login_id (str): New login ID to set for the user. const newLoginId = "xxxx" const resp = await descopeClient.management.user.updateLoginId(loginId, newLoginId) if (!resp.ok) { console.log(resp) console.log("Unable to update user's Login ID.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's Login ID.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update the Login ID for. # new_login_id (str): New login ID to set for the user. try: resp = descope_client.mgmt.user.update_login_id(login_id="xxxx", new_login_id="xxxx") print ("Successfully updated user's Login ID.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's Login ID.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // login_id (str): The login ID of the user to update the Login ID for. loginID := "xxxx" // new_login_id (str): New login ID to set for the user. newLoginID := "xxxx" res, err := descopeClient.Management.User().UpdateLoginID(ctx, loginID, newLoginID) if (err != nil){ fmt.Println("Unable to update user's Login ID: ", err) } else { fmt.Println("Successfully updated user's Login ID.", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); try { us.updateLoginId("email@company.com", "newEmail@company.com") } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update the Login ID for. # new_login_id (str): New login ID to set for the user. Provide an empty string to remove the login ID. begin resp = descope_client.update_login_id(login_id: 'desmond@descope.com', new_login_id: 'desmond.copeland@descope.com') puts "Successfully updated user's Login ID." puts resp rescue Descope::AuthException => e puts "Unable to update user's Login ID. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The user’s current login ID to be replaced. var loginID = "user-login-id"; // newLoginID (string?): The new login ID, or null to remove the current one var newLoginID = "new-login-id"; var updateRequest = new UpdateUserLoginIDRequest { LoginId = loginID, NewLoginId = newLoginID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Loginid.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update a User's Phone Number This operation allows administrators to update a user's phone number. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update the phone number for. const loginIdOrUserId = "xxxx" // phone (str): The new user phone number. Leave empty to remove. const phone = "+17777777777" // verified (bool): Set to true for the user to be able to login with the phone number. const verified = true // or false // failOnConflict (bool, optional): false (default) merges with a conflicting user; // true fails the request before any merge happens. const failOnConflict = false // or true let resp = await descopeClient.management.user.updatePhone(loginIdOrUserId, phone, verified, failOnConflict) if (!resp.ok) { console.log(resp) console.log("Failed to update user's phone number.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's phone number.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update the phone for. # phone (str): The new user phone number. Leave empty to remove. # verified (bool): Set to true for the user to be able to login with the phone number. # fail_on_conflict (bool, optional): False (default) merges with a conflicting user; True fails the request before any merge happens. try: resp = descope_client.mgmt.user.update_phone(login_id="xxxx", phone="+1777777777", verified=True, fail_on_conflict=False) print("Successfully updated user's phone number.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's phone number.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The login ID or userID of the user to update the phone for. loginIDOrUserID := "xxxx" // phone (str): The new user phone number. Leave empty to remove. phone := "+13333333333" // verified (bool): Set to true for the user to be able to login with the phone number. verified := true // or False // failOnConflict (bool): REQUIRED — this is a breaking addition, existing calls must // be updated to pass a value. false merges with a conflicting user; true fails the // request before any merge happens. failOnConflict := false // or true res, err := descopeClient.Management.User().UpdatePhone(ctx, loginIDOrUserID, phone, verified, failOnConflict) if (err != nil){ fmt.Println("Unable to update user's phone number: ", err) } else { fmt.Println("Successfully updated user's phone number: ", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. // failOnConflict is not currently available in a published Java SDK release. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .loginId("") .phone("980-012-1234") .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update the phone number for. # phone (str): The new user phone number. Leave empty to remove. # verified (bool): Set to true for the user to be able to login with the phone number. # failOnConflict is not currently available in the Ruby SDK. begin resp = descope_client.update_phone(login_id: 'desmond@descope.com', phone: '+15555555555', verified: true) puts "Successfully updated user's phone number." puts resp rescue Descope::AuthException => e puts "Unable to update user's phone number. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // newPhone (string?): The new phone number, or null to remove. var newPhone = "+15555555555"; // verified (bool): Must be true for the user to log in with this phone. var verified = true; // failOnConflict is not currently available in the .NET SDK. var updateRequest = new UpdateUserPhoneRequest { Identifier = loginID, Phone = newPhone, Verified = verified }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Phone.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` #### Handling login ID conflicts on phone update When updating a user's email or phone number to a value already assigned to a different user, the `failOnConflict` parameter controls whether the request fails or merges the two accounts. By default (`failOnConflict` omitted or `false`), the conflicting user is deleted and their data is folded into the user being updated; setting it to `true` fails the request instead, before any merge happens. Note that `failOnConflict: true` currently returns a `500`, while a default-path conflict during a concurrent update returns a retryable `409` (`E013017`) — the two failure modes aren't currently distinguishable by status code alone in the same way. See the [Update User Email](/api/management/users/update-user-email) and [Update User Phone](/api/management/users/update-user-phone) API references for the full request schema. **Known limitation:** when relying on the default merge behavior, the phone number can be dropped during the merge in some cases ([etc#4442](https://github.com/descope/etc/issues/4442), still open). Setting `failOnConflict: true` avoids this by preventing the merge entirely. **Note:** `failOnConflict` governs collisions with a different existing user's login ID. This is separate from `AddToLoginIDs`/`OnMergeUseExisting`, which intentionally links a second login ID to the same user. ### Update a User's Display Name This operation allows administrators to update a user's display name. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // displayName (str): Optional user display name. Leave empty to remove. const displayName = "Updated Display Name" let resp = await descopeClient.management.user.updateDisplayName(loginIdOrUserId, displayName) if (!resp.ok) { console.log(resp) console.log("Failed to update user's display name.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's display name.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # display_name (str): Optional user display name. Leave empty to remove. try: resp = descope_client.mgmt.user.update_display_name(login_id="xxxx", display_name="Updated Display Name") print ("Successfully updated user's display name.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's display name.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The login ID userID of the user to update. loginIDOrUserID := "xxxx" // displayName (str): Optional user display name. Leave empty to remove. displayName := "Updated Display Name" res, err := descopeClient.Management.User().UpdateDisplayName(ctx, loginIDOrUserID, displayName) if (err != nil){ fmt.Println("Unable to update user's display name: ", err) } else { fmt.Println("Successfully updated user's display name: ", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .loginId("") .phone("980-012-1234") .build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # name (str): Optional user display name. Leave empty to remove. # given_name (str): Optional user given (first) name. # middle_name (str): Optional user middle name. # family_name (str): Optional user family (last) name. begin resp = descope_client.update_display_name( login_id: 'desmond@descope.com', name: 'Desmond Copeland', given_name: 'Desmond', family_name: 'Copeland' ) puts "Successfully updated user's display name." puts resp rescue Descope::AuthException => e puts "Unable to update user's display name. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // newDisplayName (string?): The new display name, or null to remove. var newDisplayName = "updated-display-name"; var updateRequest = new UpdateUserDisplayNameRequest { Identifier = loginID, Name = newDisplayName }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Name.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update a User's Picture This operation allows administrators to update a user's profile picture granularly without updating all user details. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // picture (str): Optional url to user avatar. Leave empty to remove. const picture = "https://example.com/picture.png" const resp = await descopeClient.management.user.updatePicture(loginIdOrUserId, picture) if (!resp.ok) { console.log(resp) console.log("Unable to update user's picture.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's picture.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # picture (str): Optional url to user avatar. Leave empty to remove. try: resp = descope_client.mgmt.user.update_picture(login_id="xxxx", picture="https://example.com/picture.png") print ("Successfully updated user's picture.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's picture.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // picture (str): Optional url to user avatar. Leave empty to remove. picture := "https://example.com/picture.png" res, err := descopeClient.Management.User().UpdatePicture(ctx, loginIDOrUserID, picture) if (err != nil){ fmt.Println("Unable to update user's picture: ", err) } else { fmt.Println("Successfully updated user's picture.", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .phone("980-012-1234") .picture("").build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # picture (str): Optional url to user avatar. Leave empty to remove. begin resp = descope_client.update_picture(login_id: 'desmond@descope.com', picture: 'https://example.com/picture.png') puts "Successfully updated user's picture." puts resp rescue Descope::AuthException => e puts "Unable to update user's picture. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "custom-login-id"; // picture (string?): The new picture URL, or null to remove. var newPicture = "https://example.com/picture.png"; var updateRequest = new UpdateUserPictureRequest { LoginId = loginID, Picture = newPicture }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Picture.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Update a User's Custom Attributes This operation allows administrators to update a user's custom attributes granularly without updating all user details. Pass `null` as the attribute value to **clear** the attribute on the user. This works for all custom attribute types (string, number, boolean, and so on). You no longer need type-specific workarounds such as `""` or `0`. The same `null` behavior applies when you set a key to `null` inside the `customAttributes` map on **[Update User](#update-user)**. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // attributeKey: The custom attribute that needs to be updated, this attribute needs to exists in Descope console app const attributeKey = "mycustomattribute" // attributeValue: The value to set, or null to clear the attribute const attributeValue = "Test Value" // const attributeValue = null // clears the attribute const resp = await descopeClient.management.user.updateCustomAttribute(loginIdOrUserId, attributeKey, attributeValue) if (!resp.ok) { console.log(resp) console.log("Unable to update user's custom attribute.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated user's custom attribute.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # attribute_key: The custom attribute that needs to be updated, this attribute needs to exists in Descope console app # attribute_val: The value to set, or None to clear the attribute # attribute_val = None # clears the attribute try: resp = descope_client.mgmt.user.update_custom_attribute(login_id="xxxx", attribute_key="mycustomattribute", attribute_val="Test Value") print ("Successfully updated user's custom attribute.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's custom attribute.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // attributeKey (str): The custom attribute that needs to be updated, this attribute needs to exists in Descope console app attributeKey := "mycustomattribute" // attributeValue: The value to set, or nil to clear the attribute attributeValue := "Test Value" // var attributeValue any = nil // clears the attribute res, err := descopeClient.Management.User().UpdateCustomAttribute(ctx, loginIDOrUserID, attributeKey, attributeValue) if (err != nil){ fmt.Println("Unable to update user's custom attribute: ", err) } else { fmt.Println("Successfully updated user's custom attribute.", res) } ``` ```java // A user must have a loginID, other fields are optional. // Roles should be set directly if no tenants exist, otherwise set on a per-tenant basis. UserService us = descopeClient.getManagementServices().getUserService(); // Update will override all fields as is. Use carefully. try { us.update("email@company.com", UserRequest.builder() .email("email@company.com") .displayName("Joe Person") .phone("980-012-1234") .customAttributes(customAttributes) .picture("").build()); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # attribute_key (str): The custom attribute that needs to be updated, this attribute needs to exist in the Descope console app. # attribute_value: The value to be updated. begin resp = descope_client.update_custom_attribute( login_id: 'desmond@descope.com', attribute_key: 'mycustomattribute', attribute_value: 'Test Value' ) puts "Successfully updated user's custom attribute." puts resp rescue Descope::AuthException => e puts "Unable to update user's custom attribute. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // attrKey (string): An existing custom‑attribute key configured in the Descope console. var attrKey = "my-custom-attribute"; // attrVal (object): Value matching the type defined for the key, or null to clear the attribute. object attrVal = "attribute-value"; // object attrVal = null; // clears the attribute var updateRequest = new UpdateUserCustomAttributeRequest { LoginId = loginID, AttributeKey = attrKey, AttributeValue = new UntypedString((string)attrVal) }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.CustomAttribute.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Expire a User's Password This operation allows administrators to expire an existing user's password. Upon next login, the user will need to follow the reset password flow. ```javascript // Args: // loginId (str): The login ID of the user to expire password for. const loginId = "xxxx" const resp = await descopeClient.management.user.expirePassword(loginId) if (!resp.ok) { console.log(resp) console.log("Unable to expire user's password.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully expired user's password.") } ``` ```python # Args: # login_id (str): The login ID of the user expire password for. try: resp = descope_client.mgmt.user.expire_password(login_id="xxxx") print ("Successfully expired user's password.") except AuthException as error: print ("Unable to expire user's password.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user to expire password for. loginID := "xxxx" err := descopeClient.Management.User().ExpirePassword(ctx, loginID) if (err != nil){ fmt.Println("Unable to expire user's password: ", err) } else { fmt.Println("Successfully expired user's password.") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // Set a user's password try { us.expirePassword("my-custom-id"); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to expire the password for. begin resp = descope_client.expire_password('desmond@descope.com') puts "Successfully expired user's password." puts resp rescue Descope::AuthException => e puts "Unable to expire user's password. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user whose password should be expired. var loginID = "user-login-id"; var expireRequest = new ExpireUserPasswordRequest { Identifier = loginID }; try { await descopeClient.Mgmt.V1.User.Password.Expire.PostAsync(expireRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Set a Temporary User's Password This operation allows administrators to set a temporary password for an existing user. This will require the user to change their password on next authentication. ```javascript // Args: // loginId (str): The login ID of the user set password for. const loginId = "xxxx" // password (str): The password to be set for the user. const password = "xxxxx" const resp = await descopeClient.management.user.setTemporaryPassword(loginId, password) if (!resp.ok) { console.log(resp) console.log("Unable to set user's password.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully set user's password.") } ``` ```python # Args: # login_id (str): The login ID of the user set password for. # password (str): The password to be set for the user. try: resp = descope_client.mgmt.user.set_temporary_password(login_id="xxxx", password="xxxxx") print ("Successfully set user's password.") except AuthException as error: print ("Unable to set user's password.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user to set password for. loginID := "xxxx" // password (str): The password to be set for the user. password := "xxxxx" err := descopeClient.Management.User().SetTemporaryPassword(ctx, loginID, password) if (err != nil){ fmt.Println("Unable to set user's password: ", err) } else { fmt.Println("Successfully set user's password.") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); // Set a user's password try { us.setPassword("my-custom-id", "some-password"); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # login_id (str): The login ID of the user to set the password for. # password (str): The temporary password to be set for the user. begin resp = descope_client.set_temporary_password(login_id: 'desmond@descope.com', password: '') puts "Successfully set user's password." puts resp rescue Descope::AuthException => e puts "Unable to set user's password. Error: #{e.message}" end ``` ```php $descopeSDK->management->user->setTemporaryPassword("testuser1", new UserPassword(cleartext: "temporaryPassword123")); ``` ```csharp // Args: // loginID (string): The login ID of the user whose password is being set. var loginID = "user-login-id"; // tempPassword (string): The temporary password to assign (will be marked expired). var tempPassword = "TempP@ssw0rd!"; var setPasswordRequest = new SetUserPasswordRequest { Identifier = loginID, Password = tempPassword }; try { await descopeClient.Mgmt.V1.User.Password.Set.Temporary.PostAsync(setPasswordRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Set an Active Password for User This endpoint allows you to set an active password for an existing user. This will allow the user to authenticate with this password without changing it. ```javascript // Args: // loginId (str): The login ID of the user set password for. const loginId = "xxxx" // password (str): The password to be set for the user. const password = "xxxxx" const resp = await descopeClient.management.user.setActivePassword(loginId, password) if (!resp.ok) { console.log(resp) console.log("Unable to set user's password.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully set user's password.") } ``` ```python # Args: # login_id (str): The login ID of the user set password for. # password (str): The password to be set for the user. try: resp = descope_client.mgmt.user.set_active_password(login_id="xxxx", password="xxxxx") print ("Successfully set user's password.") except AuthException as error: print ("Unable to set user's password.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user to set password for. loginID := "xxxx" // password (str): The password to be set for the user. password := "xxxxx" err := descopeClient.Management.User().SetActivePassword(ctx, loginID, password) if (err != nil){ fmt.Println("Unable to set user's password: ", err) } else { fmt.Println("Successfully set user's password.") } ``` ```java // pending release ``` ```ruby # Args: # login_id (str): The login ID of the user to set the password for. # password (str): The active password to be set for the user. begin resp = descope_client.set_active_password(login_id: 'desmond@descope.com', password: '') puts "Successfully set user's password." puts resp rescue Descope::AuthException => e puts "Unable to set user's password. Error: #{e.message}" end ``` ```php $descopeSDK->management->user->setActivePassword("testuser1", new UserPassword(cleartext: "activePassword123")); ``` ```csharp // Args: // loginID (string): The login ID of the user whose password is being set. var loginID = "user-login-id"; // newPass (string): The active (non‑expired) password to assign. var newPassword = "Str0ngP@ssw0rd"; var setPasswordRequest = new SetUserPasswordRequest { Identifier = loginID, Password = newPassword }; try { await descopeClient.Mgmt.V1.User.Password.Set.Active.PostAsync(setPasswordRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Add a Role to a User This operation allows administrators to add roles to an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // roleNames (List[str]): A list of roles to add to a user without tenant association. const roleNames = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.addRoles(loginIdOrUserId, roleNames) if (!resp.ok) { console.log(resp) console.log("Failed to add roles to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added roles to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # role_names (List[str]): A list of roles to add to a user without tenant association. try: resp = descope_client.mgmt.user.add_roles(login_id="xxxx", role_names=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully updated user's roles.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's roles.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // roleNames (List[str]): A list of roles to add to a user without tenant association. roleNames := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().AddRoles(ctx, loginIDOrUserID, roleNames) if (err != nil){ fmt.Println("Unable to add roles to user: ", err) } else { fmt.Println("Successfully added roles to user: ", res) } ``` ```java ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("My Updated Permission"); /** * Add roles for a user without tenant association. Use AddTenantRoles for users that are part of * a multi-tenant project. * * @param loginId The loginID is required. * @param roles User Roles * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.addRoles("", roles); ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): Provide tenant_id when roles are tenant-scoped. If provided, the user must be a member of that tenant. # role_names (Array): A list of roles to add to the user. begin resp = descope_client.user_add_roles( login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id', role_names: %w[TestRole1 TestRole2] ) puts 'Successfully added roles to user.' puts resp rescue Descope::AuthException => e puts "Unable to add roles to user. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "custom-login-id"; // roles (List): Roles to add (project‑level unless tenantID provided). var roles = new List { "Role1", "Role2" }; // tenantID (string?): Optional tenant association. Provide tenantId when roles are tenant‑scoped. string? tenantID = null; var addRequest = new UpdateUserRolesRequest { Identifier = loginID, RoleNames = roles, TenantId = tenantID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Add.PostAsync(addRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Set Roles for a User This endpoint allows you to set a user's roles. This will override the current roles associated to the user and will set all passed roles. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // roles (List[str]): A list of roles to set for a user without tenant association. const roles = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.setRoles(loginIdOrUserId, roles) if (!resp.ok) { console.log(resp) console.log("Failed to set roles to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully set roles to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # roles (List[str]): A list of roles to set for a user without tenant association. try: resp = descope_client.mgmt.user.set_roles(login_id="xxxx", roles=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully set user's roles.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to set user's roles.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // roles (List[str]): A list of roles to set for a user without tenant association. roles := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().SetRoles(ctx, loginIDOrUserID, roles) if (err != nil){ fmt.Println("Unable to set roles to user: ", err) } else { fmt.Println("Successfully set roles to user: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("TestRole1","TestRole2"); /** * Add roles for a user without tenant association. Use AddTenantRoles for users that are part of * a multi-tenant project. * * @param loginId The loginID is required. * @param roles User Roles * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.setRoles("", roles); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "usr-login-id"; // roles (List): Roles to set (overrides existing). var roles = new List { "Role1", "Role2" }; // tenantID (string?): Optional tenant association. Provide tenantId when roles are tenant‑scoped. string? tenantID = null; var setRequest = new UpdateUserRolesRequest { Identifier = loginID, RoleNames = roles, TenantId = tenantID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Set.PostAsync(setRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Remove a Role from a User This operation allows administrators to remove roles from an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // roleNames (List[str]): A list of roles to remove from a user without tenant association. const roleNames = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.removeRoles(loginIdOrUserId, roleNames) if (!resp.ok) { console.log(resp) console.log("Failed to remove roles from user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully removed roles from user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # role_names (List[str]): A list of roles to remove from a user without tenant association. try: resp = descope_client.mgmt.user.remove_roles(login_id="xxxx", role_names=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully updated user's roles.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to update user's roles.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // roleNames (List[str]): A list of roles to remove from a user without tenant association. roleNames := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().RemoveRoles(ctx, loginIDOrUserID, roleNames) if (err != nil){ fmt.Println("Unable to remove roles from user: ", err) } else { fmt.Println("Successfully removed roles from user: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("My Updated Permission"); /** * Remove roles from a user without tenant association. * * @param loginId The loginID is required. * @param roles Use RemoveTenantRoles for users that are part of a multi-tenant project. * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.removeRoles("", roles); ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): Provide tenant_id when roles are tenant-scoped. If provided, the user must be a member of that tenant. # role_names (Array): A list of roles to remove from the user. begin resp = descope_client.user_remove_roles( login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id', role_names: %w[TestRole1 TestRole2] ) puts 'Successfully removed roles from user.' puts resp rescue Descope::AuthException => e puts "Unable to remove roles from user. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "custom-login-id"; // roles (List): Roles to remove. var roles = new List { "Role1", "Role2" }; // tenantID (string?): Optional tenant association. Provide tenantId when roles are tenant‑scoped. string? tenantID = null; var removeRequest = new UpdateUserRolesRequest { Identifier = loginID, RoleNames = roles, TenantId = tenantID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Remove.PostAsync(removeRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Add a Tenant to a User This operation allows administrators to add tenants to an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // tenantId (str): The ID of the tenant to add to the user. const tenantId = "TestTenant" let resp = await descopeClient.management.user.addTenant(loginIdOrUserId, tenantId) if (!resp.ok) { console.log(resp) console.log("Failed to add tenant to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added tenant to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the tenant to add to the user. try: resp = descope_client.mgmt.user.add_tenant(login_id="xxxx", tenant_id="TestTenant") print ("Successfully added tenant to user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to add tenant to user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // tenantID (str): The ID of the tenant to add to the user. tenantID := "TestTenant" res, err := descopeClient.Management.User().AddTenant(ctx, loginIDOrUserID, tenantID) if (err != nil){ fmt.Println("Unable to add tenant to user: ", err) } else { fmt.Println("Successfully added tenant to user: ", res) } ``` ```java ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("My Updated Permission"); /** * Add a tenant association for an existing user. * * @param loginId The loginID is required. * @param tenantId Tenant ID * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.addTenant("", ""); ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the tenant to add to the user. begin resp = descope_client.user_add_tenant(login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id') puts 'Successfully added tenant to user.' puts resp rescue Descope::AuthException => e puts "Unable to add tenant to user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->addTenant("testuser1", "tenantId1"); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // tenantID (string): The tenant ID to add to the user. var tenantID = "tenant-id"; var addRequest = new UpdateUserTenantRequest { Identifier = loginID, TenantId = tenantID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Tenant.Add.PostAsync(addRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Remove a Tenant from a User This operation allows administrators to remove tenants from an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // tenantId (str): The ID of the tenant to remove from the user. const tenantId = "TestTenant" let resp = await descopeClient.management.user.removeTenant(loginIdOrUserId, tenantId) if (!resp.ok) { console.log(resp) console.log("Failed to remove tenant from user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully removed tenant from user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the tenant to add to the user. try: resp = descope_client.mgmt.user.remove_tenant(login_id="xxxx", tenant_id="TestTenant") print ("Successfully removed tenant from user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to remove tenant from user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // tenantID (str): The ID of the tenant to add to the user. tenantID := "TestTenant" res, err := descopeClient.Management.User().RemoveTenant(ctx, loginIDOrUserID, tenantID) if (err != nil){ fmt.Println("Unable to remove tenant from user: ", err) } else { fmt.Println("Successfully removed tenant from user: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); /** * Remove a tenant association from an existing user. * * @param loginId The loginID is required. * @param tenantId Tenant ID * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.removeTenant("", ""); ``` ```ruby # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the tenant to remove from the user. begin resp = descope_client.user_remove_tenant(login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id') puts 'Successfully removed tenant from user.' puts resp rescue Descope::AuthException => e puts "Unable to remove tenant from user. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->removeTenant("testuser1", "tenantId1"); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // tenantID (string): The tenant ID to remove from the user. var tenantID = "tenant-id"; var removeRequest = new UpdateUserTenantRequest { Identifier = loginID, TenantId = tenantID }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Tenant.Remove.PostAsync(removeRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Add Roles to a User in a Specific Tenant This operation allows administrators to add roles to a user within a specific tenant. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // tenantId (str): The ID of the user's tenant. const tenantId = "TestTenant" // roleNames (List[str]): A list of roles to add to the user. const roleNames = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.addTenantRoles(loginIdOrUserId, tenantId, roleNames) if (!resp.ok) { console.log(resp) console.log("Unable to add roles to the user in the specified tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added roles to the user in the specified tenant.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the user's tenant. # role_names (List[str]): A list of roles to add to the user. try: resp = descope_client.mgmt.user.add_tenant_roles(login_id="xxxx", tenant_id="TestTenant", role_names=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully added roles to the user in the specified tenant.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to add roles to the user in the specified tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // tenantID (str): The ID of the user's tenant. tenantID := "TestTenant" // roleNames (List[str]): A list of roles to add to the user. roleNames := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().AddTenantRoles(ctx, loginIDOrUserID, tenantID, roleNames) if (err != nil){ fmt.Println("Unable to add roles to the user in the specified tenant: ", err) } else { fmt.Println("Successfully added roles to the user in the specified tenant: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("My Updated Permission"); /** * Add roles for a user in a specific tenant. * * @param loginId The loginID is required. * @param tenantId Tenant ID * @param roles Tenant Roles * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.addTenantRoles("", "", roles); ``` ```ruby # Note: Unlike user_add_roles (which only updates a user's roles, optionally # scoped to a tenant the user already belongs to), add_tenant_role posts to the # add-tenant endpoint. It ensures the user is associated with the tenant and # assigns the given roles in a single call. # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the user's tenant. # role_names (Array): A list of roles to add to the user. begin resp = descope_client.add_tenant_role( login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id', role_names: %w[TestRole1 TestRole2] ) puts 'Successfully added roles to the user in the specified tenant.' puts resp rescue Descope::AuthException => e puts "Unable to add roles to the user in the specified tenant. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->addTenantRoles("testuser1", "tenantId1", ["user"]); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // tenantID (string): The ID of the user's tenant. var tenantID = "TestTenant"; // roles (List): A list of roles to add to the user within the tenant. var roles = new List { "TestRole1", "TestRole2" }; var addRequest = new UpdateUserRolesRequest { Identifier = loginID, TenantId = tenantID, RoleNames = roles }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Add.PostAsync(addRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Set Roles for a User in a Specific Tenant This operation allows administrators to set roles to a user within a specific tenant. This will override the current roles associated to the user for the tenant and will set all passed roles. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // tenantId (str): The ID of the user's tenant. const tenantId = "TestTenant" // roles (List[str]): A list of roles to set for the user. const roles = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.setTenantRoles(loginIdOrUserId, tenantId, roles) if (!resp.ok) { console.log(resp) console.log("Unable to set roles to the user in the specified tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully set roles to the user in the specified tenant.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the user's tenant. # roles (List[str]): A list of roles to set for the user. try: resp = descope_client.mgmt.user.set_tenant_roles(login_id="xxxx", tenant_id="TestTenant", roles=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully set roles to the user in the specified tenant.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to set roles to the user in the specified tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // tenantID (str): The ID of the user's tenant. tenantID := "TestTenant" // roles (List[str]): A list of roles to set for the user. roles := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().SetTenantRoles(ctx, loginIDOrUserID, tenantID, roles) if (err != nil){ fmt.Println("Unable to set roles to the user in the specified tenant: ", err) } else { fmt.Println("Successfully set roles to the user in the specified tenant: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("TestRole1","TestRole2"); /** * Add roles for a user in a specific tenant. * * @param loginId The loginID is required. * @param tenantId Tenant ID * @param roles Tenant Roles * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.setTenantRoles("", "", roles); ``` ```php $response = $descopeSDK->management->user->setTenantRoles("testuser1", "tenantId1", ["admin"]); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // tenantID (string): The ID of the user's tenant. var tenantID = "TestTenant"; // roles (List): A list of roles to set for the user within the tenant (overrides existing). var roles = new List { "TestRole1", "TestRole2" }; var setRequest = new UpdateUserRolesRequest { Identifier = loginID, TenantId = tenantID, RoleNames = roles }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Set.PostAsync(setRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Remove Roles from a User in a Specific Tenant This operation allows administrators to remove roles from a user within a specific tenant. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx" // tenantId (str): The ID of the user's tenant. const tenantId = "TestTenant" // roleNames (List[str]): A list of roles to remove from the user. const roleNames = ["TestRole1","TestRole2"] let resp = await descopeClient.management.user.removeTenantRoles(loginIdOrUserId, tenantId, roleNames) if (!resp.ok) { console.log(resp) console.log("Unable to remove roles from the user in the specified tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully removed roles from the user in the specified tenant.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the user's tenant. # role_names (List[str]): A list of roles to remove from the user. try: resp = descope_client.mgmt.user.remove_tenant_roles(login_id="xxxx", tenant_id="TestTenant", role_names=["TestRole1", "TestRole2", "TestRole3"]) print ("Successfully removed roles from the user in the specified tenant.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to remove roles from the user in the specified tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // tenantID (str): The ID of the user's tenant. tenantID := "TestTenant" // roleNames (List[str]): A list of roles to remove from the user. roleNames := []string{"TestRole1","TestRole2"} res, err := descopeClient.Management.User().RemoveTenantRoles(ctx, loginIDOrUserID, tenantID, roleNames) if (err != nil){ fmt.Println("Unable to remove roles from the user in the specified tenant: ", err) } else { fmt.Println("Successfully removed roles from the user in the specified tenant: ", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List roles = Arrays.asList("My Updated Permission"); /** * Add roles for a user in a specific tenant. * * @param loginId The loginID is required. * @param tenantId Tenant ID * @param roles Tenant Roles * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.removeTenantRoles("", "", roles); ``` ```ruby # Note: user_remove_tenant_roles posts to the remove-tenant endpoint (the same # one used by user_remove_tenant), passing the role_names to remove. This # differs from user_remove_roles(tenant_id:), which posts to the dedicated # remove-role endpoint. # Args: # login_id (str): The login ID of the user to update. # tenant_id (str): The ID of the user's tenant. # role_names (Array): A list of roles to remove from the user. begin resp = descope_client.user_remove_tenant_roles( login_id: 'desmond@descope.com', tenant_id: 'my-tenant-id', role_names: %w[TestRole1 TestRole2] ) puts 'Successfully removed roles from the user in the specified tenant.' puts resp rescue Descope::AuthException => e puts "Unable to remove roles from the user in the specified tenant. Error: #{e.message}" end ``` ```php $response = $descopeSDK->management->user->removeTenantRoles("testuser1", "tenantId1", ["admin"]); print_r($response); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // tenantID (string): The ID of the user's tenant. var tenantID = "TestTenant"; // roles (List): A list of roles to remove from the user within the tenant. var roles = new List { "TestRole1", "TestRole2" }; var removeRequest = new UpdateUserRolesRequest { Identifier = loginID, TenantId = tenantID, RoleNames = roles }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Role.Remove.PostAsync(removeRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Associate an Application to a User This operation allows administrators to associate an Application with a user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // loginIdOrUserId (str): The login ID or user ID of the user to update. const loginIdOrUserId = "xxxx"; // ssoAppIds (array(str)): The IDs of the sso apps to add to the user. const ssoAppIds = ["app1", "app2"]; let resp = await descopeClient.management.user.addSSOapps(loginIdOrUserId, ssoAppIds) if (!resp.ok) { console.log(resp) console.log("Unable to add sso apps to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added sso apps to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # sso_ids (array(str)): The IDs of the sso apps to add to the user. try: user = descope_client.mgmt.user.set_sso_apps( login_id="xxxx", sso_app_ids=["appId1", "appId2"] ) print ("Successfully added sso apps to user.") except AuthException as error: print ("Unable to add sso apps to user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // ssoAppIds (array(str)): The IDs of the sso apps to add to the user. ssoAppIds := []string{"appId3"} user, err := descopeClient.Management.User().AddSSOApps(context.Background(), loginIDOrUserID, ssoAppIds) if (err != nil){ fmt.Println("Unable to add sso apps to user: ", err) } else { fmt.Println("Successfully added sso apps to user: ", user) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List ssoAppIds = Arrays.asList("appId1", "appId2"); us.addSsoApps("", ssoAppIds); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // ssoAppIDs (List): One or more SSO application IDs to associate. var ssoAppIDs = new List { "appId1", "appId2" }; var addRequest = new UpdateUserSSOAppsRequest { Identifier = loginID, SsoAppIds = ssoAppIDs }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Ssoapp.Add.PostAsync(addRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Set Applications for user This operation allows administrators to set Applications associated to a user. This will override the current Application associated to the user for the user and set all passed Applications. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // loginIdOrUserId (str): The login ID or user ID of the user to update. loginIdOrUserId = "xxxx" // ssoAppIds (array(str)): The IDs of the sso apps to add to the user. ssoAppIds = ["app1", "app2"] let resp = await descopeClient.management.user.setSSOapps(loginIdOrUserId, ssoAppIds) if (!resp.ok) { console.log(resp) console.log("Unable to set sso apps to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully set sso apps to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # sso_ids (array(str)): The IDs of the sso apps to add to the user. try: user = descope_client.mgmt.user.set_sso_apps( login_id="xxxx", sso_app_ids=["appId1", "appId2"] ) print ("Successfully set sso apps to user.") except AuthException as error: print ("Unable to set tenant to user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // ssoAppIds (array(str)): The IDs of the sso apps to add to the user. ssoAppIds := []string{"appId3"} user, err := descopeClient.Management.User().SetSSOApps(context.Background(), loginIDOrUserID, ssoAppIds) if (err != nil){ fmt.Println("Unable to set sso apps to user: ", err) } else { fmt.Println("Successfully set sso apps to user: ", user) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List ssoAppIds = Arrays.asList("appId1", "appId2"); us.setSsoApps("", ssoAppIds); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "custom-login-id"; // ssoAppIDs (List): The set of SSO application IDs to associate (overwrites existing list). var ssoAppIDs = new List { "appId1", "appId2" }; var setRequest = new UpdateUserSSOAppsRequest { Identifier = loginID, SsoAppIds = ssoAppIDs }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Ssoapp.Set.PostAsync(setRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Remove an Application from a User This operation allows administrators to remove an Application from being associated with a user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // loginIdOrUserId (str): The login ID or user ID of the user to update. loginIdOrUserId = "xxxx" // ssoAppIds (array(str)): The IDs of the sso apps to add to the user. ssoAppIds = ["app1", "app2"] let resp = await descopeClient.management.user.removeSSOapps(loginIdOrUserId, ssoAppIds) if (!resp.ok) { console.log(resp) console.log("Unable to remove sso apps to user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully removed sso apps to user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to update. # sso_ids (array(str)): The IDs of the sso apps to remove to the user. try: user = descope_client.mgmt.user.remove_sso_apps( login_id="xxxx", sso_app_ids=["appId1", "appId2"] ) print ("Successfully removed sso apps from user.") except AuthException as error: print ("Unable to remove sso apps from user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to update. loginIDOrUserID := "xxxx" // sso_ids (array(str)): The IDs of the sso apps to remove from the user. ssoAppIds := []string{"appId3"} user, err := descopeClient.Management.User().RemoveSSOApps(context.Background(), loginIDOrUserID, ssoAppIds) if (err != nil){ fmt.Println("Unable to add sso apps to user: ", err) } else { fmt.Println("Successfully removed sso app from user: ", user) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); List ssoAppIds = Arrays.asList("appId1", "appId2"); us.removeSsoApps("", ssoAppIds); ``` ```csharp // Args: // loginID (string): The login ID of the user to update. var loginID = "user-login-id"; // ssoAppIDs (List): One or more SSO application IDs to remove. var ssoAppIDs = new List { "appId2" }; var removeRequest = new UpdateUserSSOAppsRequest { Identifier = loginID, SsoAppIds = ssoAppIDs }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Ssoapp.Remove.PostAsync(removeRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Activate User This operation allows administrators to activate an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to be activated. const loginIdOrUserId = "xxxx" let resp = await descopeClient.management.user.activate(loginIdOrUserId) if (!resp.ok) { console.log(resp) console.log("Failed to activate user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully activated user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to be activated. try: resp = descope_client.mgmt.user.activate(login_id="xxxx") print ("Successfully activated user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to activate user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to be activated. loginIDOrUserID := "xxxx" res, err := descopeClient.Management.User().Activate(ctx, loginIDOrUserID) if (err != nil){ fmt.Println("Unable to activate user: ", err) } else { fmt.Println("User successfully activated", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); /** * Activate an existing user. * * @param loginId The loginID is required. * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.activate(""); ``` ```ruby # Args: # login_id (str): The login ID of the user to be activated. begin resp = descope_client.activate('desmond@descope.com') puts 'Successfully activated user.' puts resp rescue Descope::AuthException => e puts "Unable to activate user. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to activate. var loginID = "user-login-id"; var activateRequest = new UpdateUserStatusRequest { Identifier = loginID, Status = EnumValues.UserStatus.Enabled }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Status.PostAsync(activateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Deactivate User This operation allows administrators to deactivate an existing user. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to be deactivated. const loginIdOrUserId = "xxxx" let resp = await descopeClient.management.user.deactivate(loginIdOrUserId) if (!resp.ok) { console.log(resp) console.log("Failed to deactivate user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deactivated user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login ID of the user to be deactivated. try: resp = descope_client.mgmt.user.deactivate(login_id="xxxx") print ("Successfully deactivated user.") print(json.dumps(resp, indent=2)) except AuthException as error: print ("Unable to deactivate user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to be deactivated. loginIDOrUserID := "xxxx" res, err := descopeClient.Management.User().Deactivate(ctx, loginIDOrUserID) if (err != nil){ fmt.Println("Unable to deactivate user: ", err) } else { fmt.Println("User successfully deactivated", res) } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); /** * Deactivate an existing user. * * @param loginId The loginID is required. * @return {@link UserResponseDetails UserResponseDetails} * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.deactivate(""); ``` ```ruby # Args: # login_id (str): The login ID of the user to be deactivated. begin resp = descope_client.deactivate('desmond@descope.com') puts 'Successfully deactivated user.' puts resp rescue Descope::AuthException => e puts "Unable to deactivate user. Error: #{e.message}" end ``` ```csharp // Args: // loginID (string): The login ID of the user to deactivate. var loginID = "user-login-id"; var deactivateRequest = new UpdateUserStatusRequest { Identifier = loginID, Status = EnumValues.UserStatus.Disabled }; try { var userRes = await descopeClient.Mgmt.V1.User.Update.Status.PostAsync(deactivateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Logout All User Sessions This operation allows administrators to log an existing user out of all sessions. This operation can be done via loginId or userId. ```javascript // Args: // loginId (str): The loginId of the user to be logged out. const loginId = "email@company.com" const resp = await descopeClient.management.user.logoutUser(loginId); if (!resp.ok) { console.log("Failed to logout user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully logged user out.") } // Args: // userId (str): The userId of the user to be logged out. const userId = "email@company.com" const resp = await descopeClient.management.user.logoutUserByUserId(userId); if (!resp.ok) { console.log("Failed to logout user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully logged user out.") } ``` ```python # Args: # login_id (str): The login_id of the user that's to be logged out. try: resp = descope_client.mgmt.user.logout_user(login_id="xxxxx") print("Successfully logged user out.") except AuthException as error: print ("Failed to logout user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) # Args: # user_id (str): The user_id of the user that's to be logged out. try: resp = descope_client.mgmt.user.logout_user_by_user_id(user_id="xxxxx") print("Successfully logged user out.") except AuthException as error: print ("Failed to logout user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The loginID of the user to be logged out. loginID := "xxxx" err := descopeClient.Management.User().LogoutUser(ctx, loginID) if (err != nil){ fmt.Println("Failed to logout user.", err) } else { fmt.Println("Successfully logged user out.") } // Args: // userID (str): The userID of the user to be logged out. userID := "xxxx" err := descopeClient.Management.User().LogoutUserByUserID(ctx, userID) if (err != nil){ fmt.Println("Failed to logout user.", err) } else { fmt.Println("Successfully logged user out.") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); us.logoutUser(""); us.logoutUserByUserId(""); ``` ```ruby # Args: # login_id (str): The login ID of the user to be logged out. # Logout user from all devices by login ID begin resp = descope_client.logout_user('desmond@descope.com') puts 'Successfully logged user out.' puts resp rescue Descope::AuthException => e puts "Failed to logout user. Error: #{e.message}" end # Args: # user_id (str): The user ID of the user to be logged out. # Logout user from all devices by user ID begin resp = descope_client.logout_user_by_id('') puts 'Successfully logged user out.' puts resp rescue Descope::AuthException => e puts "Failed to logout user. Error: #{e.message}" end ``` ```php // Args: // loginId (string): The login ID of the user to log out. // sessionTypes (array): Optional session types to revoke. Empty array logs out all sessions. $descopeSDK->management->user->logoutUser('desmond@descope.com', []); // Args: // userId (string): The user ID of the user to log out. // sessionTypes (array): Optional session types to revoke. Empty array logs out all sessions. $descopeSDK->management->user->logoutUserByUserId('U2abc...', []); ``` ```csharp // Args: // loginID (string?): The login ID of the user to log out (optional if user ID is provided). string? loginID = "user-login-id"; // userID (string?): The user ID of the user to log out (optional if login ID is provided). string? userID = "user-uuid"; var logoutRequest = new UserLogoutRequest { Identifier = loginID, UserId = userID }; try { await descopeClient.Mgmt.V1.User.Logout.PostAsync(logoutRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Delete User's Passkeys This operation will delete all existing passkeys for a user. ```javascript // Args: // loginId (str): The loginId of the user to be remove passkeys for. const loginId = "email@company.com" const resp = await descopeClient.management.user.removeAllPasskeys(loginId); if (!resp.ok) { console.log("Failed to remove user's passkeys.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully removed user's passkeys.") } ``` ```python # Args: # login_id (str): The login_id of the user to be remove passkeys for. try: resp = descope_client.mgmt.user.remove_all_passkeys(login_id="xxxxx") print("Successfully removed user's passkeys.") except AuthException as error: print ("Failed to remove user's passkeys.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The loginID of the user to be remove passkeys for. loginID: = "xxxx" err := descopeClient.Management.User().RemoveAllPasskeys(ctx, userID) if (err != nil){ fmt.Println("Failed to remove user's passkeys.", err) } else { fmt.Println("Successfully removed user's passkeys.") } ``` ```java // Pending release ``` ```csharp // Args: // loginID (string): The login ID of the user whose passkeys will be removed. var loginID = "user-login-id"; var deletePasskeysRequest = new RemoveUserPasskeysRequest { LoginId = loginID }; try { await descopeClient.Mgmt.V1.User.Passkeys.DeletePath.PostAsync(deletePasskeysRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Delete User This operation allows administrators to delete an existing user. It is important to note that this operation is irreversible and the user will be removed and will not be able to be added back without recreation. The Login ID parameter also accepts a User ID. Descope detects which one you passed and resolves the user. ```javascript // Args: // loginIdOrUserId (str): The login ID or user ID of the user to be deleted. const loginIdOrUserId = "email@company.com" const resp = await descopeClient.management.user.delete(loginIdOrUserId); if (!resp.ok) { console.log("Failed to delete user.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted user.") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user that's to be deleted. try: resp = descope_client.mgmt.user.delete(login_id="xxxxx") print("Successfully deleted user.") except AuthException as error: print ("Failed to delete user.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginIDOrUserID (str): The loginID or userID of the user to be deleted. loginIDOrUserID := "xxxx" err := descopeClient.Management.User().Delete(ctx, loginIDOrUserID) if (err != nil){ fmt.Println("Unable to delete user: ", err) } else { fmt.Println("User Successfully deleted") } ``` ```java UserService us = descopeClient.getManagementServices().getUserService(); /** * Delete an existing user. * *

IMPORTANT: This action is irreversible. Use carefully. * * @param loginId The loginID is required. * @throws DescopeException If there occurs any exception, a subtype of this exception will be * thrown. */ us.delete(""); ``` ```ruby # Args: # login_id (str): The login ID of the user to be deleted. begin resp = descope_client.delete_user('desmond@descope.com') puts 'Successfully deleted user.' puts resp rescue Descope::AuthException => e puts "Failed to delete user. Error: #{e.message}" end ``` ```php $descopeSDK->management->user->delete("testuser1"); ``` ```csharp // Args: // loginID (string): The login ID of the user to delete (irreversible). var loginID = "user-login-id"; var deleteRequest = new DeleteUserRequest { Identifier = loginID }; try { await descopeClient.Mgmt.V1.User.DeletePath.PostAsync(deleteRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Batch Delete Users This operation deletes several existing users in a single request. As with [Delete User](#delete-user), this operation is irreversible: the deleted users are removed from the project and cannot be added back without recreation. ```javascript // Args: // userIds (string[]): The Descope user IDs of the users to be deleted. const userIds = ["", ""] const resp = await descopeClient.management.user.deleteBatch(userIds); if (!resp.ok) { console.log("Failed to batch delete users.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully batch deleted users.") } ``` ```python # Args: # user_ids (List[str]): The Descope user IDs of the users to be deleted. try: descope_client.mgmt.user.delete_batch(user_ids=["", ""]) print("Successfully batch deleted users.") except AuthException as error: print ("Failed to batch delete users.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userIDs ([]string): The Descope user IDs of the users to be deleted. userIDs := []string{"", ""} err := descopeClient.Management.User().DeleteBatch(ctx, userIDs) if (err != nil){ fmt.Println("Unable to batch delete users: ", err) } else { fmt.Println("Users Successfully deleted") } ``` ```java // Batch delete isn't available in the published Java SDK yet. Delete // users one at a time, or call the Batch Delete Users API directly: // https://docs.descope.com/api/management/users/batch-delete-users UserService us = descopeClient.getManagementServices().getUserService(); us.deleteByUserId(""); us.deleteByUserId(""); ``` ```ruby # Batch delete is not currently available in the Ruby SDK. # Delete users individually with delete_user, or call the Batch Delete Users API # directly: https://docs.descope.com/api/management/users/batch-delete-users ``` ```php // Args: // userIds (array): The Descope user IDs of the users to be deleted. $descopeSDK->management->user->deleteBatch(["", ""]); ``` ```csharp // Args: // UserIds (List): The Descope user IDs of the users to delete (irreversible). var deleteRequest = new DeleteUsersRequest { UserIds = new List { "", "" } }; try { await descopeClient.Mgmt.V1.User.DeletePath.Batch.PostAsync(deleteRequest); } catch (DescopeException ex) { // Handle the error } ``` # Exporting Users (/management/user-management/user-exporting) This guide will cover the fundamentals of user exporting with Descope. # Exporting Users This guide will cover how to export users from Descope. ## Overview There are two main methods for exporting users from Descope: 1. **API/SDK Methods**: For migrating users between projects or to external systems, use the [Search Users API](/api/management/users/search-users) or the SDK methods to export user data programmatically. This is the recommended approach for project migrations and external system integrations. 2. **CSV Export**: If you need to export users as a CSV file (useful when dealing with large batches of users), you can use the Descope Console's CSV export feature. ## Using the Search Users API or SDK For scenarios where you need to migrate user data between projects or to a different system, you can use the [Search Users API](/api/management/users/search-users) or the SDK methods to export user data programmatically. You can export user data securely by utilizing the [Search Users](/api/management/users/search-users) endpoint of the Descope Backend API. This endpoint allows for the programmatic extraction of user information. Alternatively, the [searchAll()](/management/user-management/sdks#search-users) function available in the Descope Backend SDKs can be employed to retrieve a comprehensive list of users. Submitting an empty request payload will return all users. Here's an example `curl` command to do so: ```bash curl -i -X POST \ __BaseURL__/v1/mgmt/user/search \ -H 'Authorization: Bearer __ProjectID__:' \ -H 'Content-Type: application/json' \ -d '{}' ``` ### Example Output The output of the above curl command will be a JSON object containing user details. Below is an example of what the response might look like: ```json { "users": [ { "loginIds": ["brian"], "userId": "U2iOCFIwsdgeGvusdfkKUb9HJKsdi", "name": "Brian", "email": "", "phone": "", "verifiedEmail": false, "verifiedPhone": false, "roleNames": [], "userTenants": [], "status": "enabled", "externalIds": ["brian"], "picture": "", "test": false, "customAttributes": {}, "createdTime": 1719352134, "TOTP": false, "SAML": false, "OAuth": {}, "webauthn": false, "password": false, "ssoAppIds": [], "givenName": "", "middleName": "", "familyName": "" }, ... ], "total": 2152023 } ``` ## Migrating Users to a New System While direct access to users' hashed passwords is not provided through our Backend APIs, you can contact our [Support team](/support) for assistance. We can generate a `.csv` file containing your users' data and facilitate a secure data transfer. ## Tracking Migration Progress If you're migrating users into Descope gradually rather than all at once (for example, via JIT provisioning from Okta CIS or another identity provider - see [Tracking migration progress](/migrate/okta-cis#tracking-migration-progress) in the Okta CIS guide), you can use the `password` filter on the [Search Users API](/api/management/users/search-users) to see how many users have completed migration versus how many still need to. - **`password: true`** - Returns users who already have a password set in Descope. - **`password: false`** - Returns users who do not yet have a password set in Descope. ```bash curl -i -X POST \ __BaseURL__/v2/mgmt/user/search \ -H 'Authorization: Bearer __ProjectID__:' \ -H 'Content-Type: application/json' \ -d '{"password": false}' ``` Compare the `total` field returned for a `password: true` search against a `password: false` search to see how many users remain. The same filtering works for `totp`, `webauthn`, and `scim` — for example, use `{"webauthn": false}` to find users who still need to enroll a passkey. ## Exporting Users as CSV This method of export is not meant to transfer users between different projects. If you need to export users as a CSV file (useful when dealing with large batches of users), you can use the Descope Console. Head over to the users page, select the required users for export, and the "Export CSV" button will appear: ![export users csv ui](/assets/export-users-ui-button.webp) By pressing the button, you should be prompted to download the file. # Tracking User Updates (/management/user-management/user-tracking) Learn how to track user updates using Descope's Search Users API and Audit Webhooks. # Tracking User Updates ## Overview Tracking user updates is essential for various use cases such as data analytics, user activity monitoring, and access control adjustments. Descope provides two primary methods to track user updates effectively: 1. **Search Users API** - Retrieve users based on their creation or modification timestamps. 2. **Audit Webhooks** - Stream real-time audit events for user actions such as creation, modification, and deletion. This guide will explore both approaches, their use cases, and how to implement them in your system. ## Method 1: Using the Search Users API The **Search Users API** allows you to filter users based on various attributes, including their creation or last modification timestamps. This is useful when you need to: - Track newly created users over a specific period. - Monitor modifications to existing users. - Maintain an up-to-date user cache for analytics or reporting. ### API Endpoint **Endpoint:** `POST /v2/mgmt/user/search`. The docs are [here](/api/management/users/search-users). ### Search Users API Request Body Below is an example of the request body: ```json { "loginId": "", "tenantIds": [], "roleNames": [], "limit": "", "text": "", "page": "", "ssoOnly": "", "withTestUser": "", "testUsersOnly": "", "customAttributes": {}, "statuses": [], "emails": [], "phones": [], "ssoAppIds": [], "sort": [], "loginIds": [], "fromCreatedTime": "", "toCreatedTime": "", "fromModifiedTime": "", "toModifiedTime": "" } ``` ### Search Users API Parameters The API provides the following parameters to filter users: | Parameter | Type | Description | |--------------------|------|-------------| | `fromCreatedTime` | int | Include users created on or after this time (Unix epoch milliseconds). | | `toCreatedTime` | int | Include users created on or before this time (Unix epoch milliseconds). | | `fromModifiedTime`| int | Include users modified on or after this time (Unix epoch milliseconds). | | `toModifiedTime` | int | Include users modified on or before this time (Unix epoch milliseconds). | ### Example Use Case: User Data Synchronization If you store user data in a database and need to periodically update it, you can use the `fromModifiedTime` parameter to fetch all users modified since the last synchronization timestamp. #### Example API Call ```json POST /v2/mgmt/user/search { "fromModifiedTime": 1700000000 } ``` This request will return all users who were updated on or after **Unix epoch time 1700000000**. For more details, visit the [Search Users API documentation](https://docs.descope.com/api/management/users/search-users). ## Method 2: Using Audit Webhooks for Event-Based Updates The **Audit Webhook** feature allows you to receive events when users are created, modified, or deleted. Note that these events are not delivered in real-time and include an internal throttling mechanism to manage system load. This is useful when: - You need to track user changes without polling the Search Users API. - Your system relies on event-driven architectures. - You want to log or trigger actions based on user modifications. ### Setting Up an Audit Webhook 1. Navigate to the [Connectors page](https://app.descope.com/connectors) **and Select the "Audit Webhook".** 2. Configure the following: - **Name**: The Audit Webhook instance name. - **Base URL**: Your API endpoint to receive audit events. - **Authentication Type**: (Optional) Based on your implementation, choose one from the following - None, Bearer Token, API Key, Basic or OAuth2.0. - **Event Type**: Select "Stream filtered audit events only", Select "Action" as the key, "Includes" as the operator, then add `User Created`, `User Modified`, and `User Deleted`. 3. Test and save the Audit Webhook instance. ### Example Audit Event Payload When a user is modified, an event like the following is sent to your audit webhook endpoint. Each event includes the originating client IP as a top-level `remoteAddress` field, so you do not need to read it out of `request_details.headers`: ```json { "remoteAddress": "203.0.113.42", "Change": { "added_multi_tenant_roles": [ "xx" ], "added_roles": [ "xx" ], "custom_attribute_emailConsent": true, "custom_attribute_myAttribute": true, "display_name": "Test Me", "family_name": "Test", "given_name": "Me", "middle_name": "Middle", "phone": "12223334455" }, "correlation_id": "xx", "request_details": { "contentLength": "956", "headers": { "descope": { "cf-bot-score": "99", "cf-connecting-ip": "xx", "cf-ja3-hash": "xx", "cf-ray": "xx-DFW", "cf-verified-bot": "false", "x-request-id": "xx" }, "http": { "origin": "https://app.descope.com", "referer": "https://app.descope.com/", "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" } }, "host": "console.descope.com", "method": "POST", "uri": "/console/v1/users/xx", "url": "/console/v1/users/xx" } } ``` For more information about available events, please review the [Audit Events article](/audit-trails-and-integrations/audit-events). ### Example Use Case: Keeping an External Database in Sync If your system needs to track user record modifications, an audit webhook can push changes to your API, which updates your database. Note that due to the throttling mechanism, there may be a delay between when a change occurs and when your system receives the event. ## Choosing the Right Approach | Use Case | Recommended Method | |----------|-------------------| | Periodic batch updates of users | **Search Users API** | | Event-based tracking of user changes | **Audit Webhook** | | Hybrid approach (initial sync + event-based updates) | **Both** | ### Hybrid Approach: Combining API & Webhooks For an optimal setup: 1. Use the **Search Users API** to initially populate your user database. 2. Set up an **Audit Webhook** to capture user changes and keep your data updated. 3. Periodically re-sync using the API as a fallback in case of webhook failures or to catch any missed events due to throttling. ## Conclusion Tracking user updates is crucial for maintaining accurate records and responding to changes efficiently. By leveraging Descope's **Search Users API** and **Audit Webhooks**, you can implement a reliable and scalable approach to user monitoring, whether for data analytics, security audits, or operational needs. # Federated Apps (/identity-federation/applications) Integrate and manage Federated Applications with Descope. Follow our guides to set up your applications' SSO securely and seamlessly. # Federated Apps Federated Applications in Descope enable you to establish secure Single Sign-On (SSO) connections between your applications and Descope's authentication system. This allows users to authenticate once with Descope and gain access to multiple connected applications without needing to log in separately to each one. Descope supports two main types of federated applications: 1. [**OIDC Applications**](/identity-federation/applications/oidc-apps) 2. [**SAML Applications**](/identity-federation/applications/saml-apps) When connecting to an application using either OIDC or SAML, Descope acts as the Federated Identity Provider (IdP), allowing you to unify your users' login experience across multiple applications. You can configure and manage these federated applications through the [Federated Apps](https://app.descope.com/applications) tab in the Descope Console, or through our [Management SDKs](/identity-federation/applications/sdks). Configuring federated applications is a Pro/Enterprise-tier feature. ## Associating Users with Applications Descope allows you to control which users can access specific federated applications either through the console (as pictured below), or with our [management SDKs](/management/user-management/sdks#associate-an-application-to-a-user). Additionally, federated applications can be automatically assigned to users [based on their tenant assignment](/management/tenant-management/tenant#application-access). ![OIDC federated authentication flow with Descope](/assets/user-application-assignment.webp) # SAML (/identity-federation/applications/saml-apps) Configure Descope as a SAML Identity Provider. Set up SP-initiated SSO, IdP-initiated SSO, and single logout for your federated applications. # SAML Applications Configure a Federated Application to use Descope as a SAML 2.0 Identity Provider. Your service provider redirects users to Descope for authentication, Descope runs your configured flow, and returns a signed SAML assertion. Standard SAML 2.0 protocol throughout — no custom auth logic required. Configuring additional federated applications (beyond the default) is a Pro+ feature. For a full reference of all SAML application settings, see the [Configuration Reference](#configuration-reference) below. ## Creating a SAML Application Navigate to [Federated Apps](https://app.descope.com/applications) and click `+ Application` in the top right. Choose a template from the Application Library or create a Generic SAML Application. Provide an **Application Name**, and optionally an **Application ID** and **Description**. ![Create a SAML Application within Descope](/assets/saml-create-application.webp) ## Configuring a SAML Application Once created, configure the application from its settings page. The two sides you need to configure are the **Identity Provider** (Descope's details, which you give to your SP) and the **Service Provider** (your SP's details, which you give to Descope). ### Identity Provider Configuration Configure your SP with Descope's details. You can use the metadata URL or enter fields manually. #### Option 1: Metadata URL (recommended) ``` __BaseURL__/v1/auth/saml/idp/metadata?projectId=xxxxx&ssoAppId=yyyy ``` If your SP does not support fetching metadata from a URL, download the XML file and upload it manually. #### Option 2: Manual Configuration Configure the SP manually using the following values from the Identity Provider section: - **SSO URL** - **Entity ID** - **Public Certificate** ![SSO URL, Entity ID](/assets/saml-sso-url-entity-id.webp) If your SP requires fingerprint hashes (SHA1 or SHA256) instead of a certificate file, Descope provides these for download as well. You may also upload a custom certificate to replace the default Descope certificate. ![Fingerprint hashes and public certificate](/assets/saml-fingerprint-hashes.webp) ### Service Provider Configuration Configure Descope with your SP's details. You can supply a metadata URL or enter fields manually. #### Option 1: Metadata URL If your SP provides a metadata URL, Descope extracts the ACS URLs, Entity ID, and certificates automatically. ![Download Metadata XML](/assets/saml-service-provider-configuration.webp) #### Option 2: Manual Configuration Provide the following values from your SP: - **ACS URL** - **Entity ID** (supports wildcards and regex) - **Public Certificate** ![ACS URL, Entity ID](/assets/saml-acs-configuration.webp) ![Mandatory SP configuration fields](/assets/saml-mandatory-sp-fields.webp) Regex expressions are supported in the **Entity ID** field to match dynamic or multi-subdomain configurations. ### Advanced Settings #### Allowed ACS Callback URLs Specify additional ACS URLs to support logins from multiple domains or environments. Supports exact URLs and regex patterns. ![Certificate and Allowed ACS URLs](/assets/wildcard-acs-url.webp) Use regex for broader ACS URL coverage across staging, QA, and production environments. #### SAML Subject and Name ID Many SPs require the user's email as the `NameID` subject. You can choose which user attribute to use: User ID, Email, Phone, or any custom attribute. When using a custom attribute, set the NameID format to `unspecified` for maximum compatibility. Supported NameID formats: ``` urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ``` ![Creating a custom attribute in Descope](/assets/saml-create-custom-attribute.webp) ![Setting custom attribute as NameID in SAML settings](/assets/saml-set-custom-attribute-nameid.webp) #### Default Relay State Determines where users are redirected after signing in via IdP-initiated SSO. Can be customized per application instance or user group. #### Error Redirect URL A hosted error page for end users, shown when a timeout or misconfiguration occurs during SAML login. #### Force Authentication When enabled, Descope runs the authentication flow even if the user is already signed in — equivalent to the SP sending `prompt=login`. ![Force authentication checkbox](/assets/saml-force-authentication.webp) When a user already has a valid session and Force Authentication is disabled, Descope redirects the user back to the SP without running the flow. In this case, [`lastAuth`](/flows/dynamic-keys#lastauth) context keys are not populated for that request, since the flow itself was skipped. ### SSO Mapping #### User Attribute Mapping Map Descope user attributes to the attribute names your SP expects in the SAML assertion. | Descope user attribute | User attribute name | | ---------------------- | ------------------- | | Descope user email | `email` | | Descope display name | `name` | | Descope phone number | `phone` | ![Configuring user attribute mapping for a SAML Application within Descope](/assets/saml-configure-user-attribute.webp) #### Group Mapping Map Descope roles to the group attribute your SP expects. | Descope Roles | Group attribute name | | ------------- | -------------------- | | `Manager` | `Manager` | ![Configuring group mapping for a SAML Application within Descope](/assets/saml-configure-group-mapping.webp) ## Login Hints Descope supports passing login hints via query parameters when initiating SAML authentication. Useful when user identity is already known before the flow begins. Supported parameters (all behave identically): - `username` - `login_hint` - `loginHint` - `LoginHint` (used by Okta CIS) Append to any SP-initiated or IdP-initiated URL: ``` __BaseURL__/v1/auth/saml/idp/initiate?app=P2NvMv9S3-SA2hNtkVFXq3X3X&login_hint=user@example.com ``` This populates `form.externalId` inside the Descope flow, where it can be used in any action or condition. ## Single Logout (SLO) Descope supports a lightweight SLO that terminates the Descope session and redirects users to a configured logout URL. It does not propagate logout to other SPs connected to the same project. To configure: 1. Set a **Logout URL** in your SAML application — this is where users land after logout. 2. Copy the **IdP Logout URL** from the console and configure it as the logout endpoint in your SP. ``` __BaseURL__/v1/auth/idp/sso/logout?app=- ``` ![SAML Single Logout Configuration](/assets/saml-slo-config.webp) ## IdP-Initiated SSO Every SAML application has an IdP-initiated URL under the Identity Provider settings: ``` __BaseURL__/v1/auth/saml/idp/initiate?app=P2NvMv9S3-SA2hNtkVFXq3X3X ``` Users navigate to this URL, authenticate with Descope, and are posted directly to the SP's ACS URL — no SP redirect required. ### Query Parameters | Parameter | Description | | --------- | ----------- | | `tenant` | Directs users to authenticate with a specific tenant. Accepts tenant name, tenant ID, or email domain. | | `login_hint` / `loginHint` / `username` | Pre-fills user identity in the flow. | | `RelayState` | Determines where users are redirected after login at the SP. | | `flow_token` | A single-use token from an [Embedded Link](/auth-methods/embedded-link) (generated via the Management SDK) or a test-user [Magic Link](/auth-methods/magic-link) (generated via the Management SDK's test-user endpoint). Validate it with the [Verify Token](/flows/actions/verify-token) action. Any custom claims on the token land on the flow's `customClaims` context value as a JSON string. Parse the claims (for example with a [Scriptlet](/flows/actions/scriptlets)) into an object and store it under its own context key before pointing [Add SAML Attributes](/flows/actions/add-saml-attributes)'s "Context Key for SAML Attribute Map" at it; `Add SAML Attributes` can't read `customClaims` directly. | The IdP-initiated URL recognizes only the parameters listed above. Descope doesn't forward other query parameters into the flow. Example with tenant and login hint: ``` __BaseURL__/v1/auth/saml/idp/initiate?app=P2NvMv9S3-SA2hNtkVFXq3X3X&tenant=corp&login_hint=user@example.com ``` ## Configuration Reference | Setting | Section | Description | | ------- | ------- | ----------- | | Application Name | Application Details | Display name of the application. Can be updated. | | Application ID | Application Details | Unique identifier set at creation. Cannot be changed. Available in flows to render application-specific logic. | | Description | Application Details | Optional context for the application. | | Application Icon | Application Details | Custom icon for the application. | | Flow Hosting URL | SSO Configuration | URL of the Descope flow users are redirected to when signing in. | | Descope Metadata (XML) | SSO Configuration — Identity Provider | Metadata URL for your Descope SAML application. | | Download Metadata (XML) | SSO Configuration — Identity Provider | Downloads the metadata XML for manual SP configuration. | | Descope Entity ID | SSO Configuration — Identity Provider | Unique identifier for the Descope SAML application. | | SSO URL | SSO Configuration — Identity Provider | The SSO URL to configure in your SP. | | IdP-Initiated URL | SSO Configuration — Identity Provider | Entry point for IdP-initiated SSO. | | Descope Certificate | SSO Configuration — Identity Provider | Downloads the public certificate for assertion validation. | | Metadata URL (SP) | SSO Configuration — Service Provider | SP metadata URL for dynamic configuration. | | Manual SP fields | SSO Configuration — Service Provider | ACS URL, Entity ID (regex supported), and SP certificate. | | Allowed ACS Callback URLs | SSO Configuration — Service Provider — Advanced | Additional ACS URLs for multi-domain or multi-environment setups. | | SAML Assertion Subject Type | SSO Configuration — Service Provider — Advanced | User attribute to use as the SAML subject (User ID, Email, Phone, or custom). | | SAML Subject NameID Format | SSO Configuration — Service Provider — Advanced | NameID format sent to the SP. | | Default Relay State | SSO Configuration — Service Provider — Advanced | Post-login redirect destination for IdP-initiated SSO. | | Error Redirect URL | SSO Configuration — Service Provider — Advanced | Custom error page URL shown on login failures. | | Logout URL | SSO Configuration — Service Provider — Advanced | Destination after SLO completes. | | Force Authentication | SSO Configuration — Service Provider — Advanced | Forces the flow to run regardless of existing session state. | | Descope user attribute | SSO Mapping — User Attribute Mapping | Descope attribute to map to an SP attribute name. | | User name attribute | SSO Mapping — User Attribute Mapping | SP attribute name to receive the mapped Descope value. | | Descope Roles | SSO Mapping — Group Mapping | Descope roles to map to a SAML group attribute. | | Group attribute name | SSO Mapping — Group Mapping | SAML group attribute name to receive the mapped roles. | # Managing with SDKs (/identity-federation/applications/sdks) Learn how to manage Descope Applications with our Management SDK. # Managing with SDKs This guide goes over how you can install and use our backend Management SDK to manage your Federated Applications. This can include creating/deleting Federated Apps, as well as changing their configuration. ## Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ```sh title="Terminal" dotnet add package descope ``` ## Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key="xxxx") except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ```csharp // appsettings.json { "Descope": { "ProjectId": "__ProjectID__", "ManagementKey": "your-management-key" } } // Program.cs using Descope; using Microsoft.Extensions.Configuration; // ... In your setup code var config = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); var descopeProjectId = config["Descope:ProjectId"]; var descopeManagementKey = config["Descope:ManagementKey"]; var descopeConfig = new DescopeConfig(projectId: descopeProjectId); var descopeClient = new DescopeClient(descopeConfig) { ManagementKey = descopeManagementKey, }; ``` ## Load All Applications Load all Applications. ```javascript const resp = await descopeClient.management.ssoApplication.loadAll() if (!resp.ok) { console.log("Failed to load Applications.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded Applications.") console.log(resp.data) } ``` ```python try: resp = descope_client.mgmt.sso_application.load_all() print("Successfully loaded Applications.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to load Applications.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() res, err := descopeClient.Management.SSOApplication().LoadAll(ctx) if (err != nil){ fmt.Println("Failed to load Applications.", err) } else { fmt.Println("Successfully loaded Applications.") for _, permission := range res { fmt.Println(permission) } } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); // Load all Applications try { IdPApplications resp = ssoas.loadAll(); for (IdPApplications sso : resp.IdPApplications()) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // (none) — Loads all federated applications in the project. try { var apps = await descopeClient.Management.SsoApplication.LoadAll(); foreach (var app in apps) { // do something } } catch (DescopeException ex) { // Handle the error } ``` ## Load a Specific Application Load an Application by ID. ```javascript // Args: // id (str): The ID of the federated application to load. const id = "xxxxx" const resp = await descopeClient.management.ssoApplication.load(id) if (!resp.ok) { console.log("Failed to load Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded Application.") console.log(resp.data) } ``` ```python # Args: # id (str): The ID of the federated application to load. try: resp = descope_client.mgmt.sso_application.load(id="xxxxx") print("Successfully loaded Application.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to load Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // ID (str): The ID of the federated application to load. ID := "xxxxx" res, err := descopeClient.Management.SSOApplication().Load(ctx, ID) if (err != nil){ fmt.Println("Failed to load Application.", err) } else { fmt.Println("Successfully loaded Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.load(id); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // appID (string): The ID of the federated application to load. var appID = "app-id"; try { var appRes = await descopeClient.Management.SsoApplication.Load(id: appID); } catch (DescopeException ex) { // Handle the error } ``` ## Create OIDC Application Create a new OIDC Application with the given name. Application IDs are provisioned automatically but can be explicitly configured if needed. Both the name and ID must be unique per project. ```javascript // Args: // oidcApplicationOptions (OidcApplicationOptions): Options for the OIDC Application create and update const oidcApplicationOptions = { "name": "My OIDC Application", "loginPageUrl": "https://my-idp-application.com/login", // "id": (optional), "description": "This is my OIDC Application", "logo": "https://my-idp-application.com/logo", "enabled": true } const resp = await descopeClient.management.ssoApplication.createOidcApplication(oidcApplicationOptions) if (!resp.ok) { console.log("Failed to create OIDC Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created OIDC Application.") console.log(resp.data) } ``` ```python # Args: # name (str): The federated application's name. # login_page_url (str): The URL where login page is hosted. # id (str): Optional federated application ID. # description (str): Optional federated application description. # logo (str): Optional federated application logo. try: resp = descope_client.mgmt.sso_application.create_oidc_application( name="My OIDC Application", login_page_url="https://my-idp-application.com/login", description="This is my OIDC Application", logo="https://my-idp-application.com/logo" ) print("Successfully created OIDC Application.") except AuthException as error: print ("Failed to create OIDC Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // OidcApplicationOptions (&descope.OIDCApplicationRequest): Options for the OIDC Application create and update OidcApplicationOptions := &descope.OIDCApplicationRequest{ // ID: (optional), Name: "My OIDC Application", Description: "This is my OIDC Application", Enabled: true, Logo: "https://my-idp-application.com/logo", LoginPageURL: "https://my-idp-application.com/login" } res, err := descopeClient.Management.SSOApplication().CreateOIDCApplication(ctx, OidcApplicationOptions) if (err != nil){ fmt.Println("Failed to create OIDC Application.", err) } else { fmt.Println("Successfully created OIDC Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.createOIDCApplication(OIDCApplicationRequest); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // oidcOptions (OidcApplicationOptions): Full configuration of the new OIDC Federated Application. var oidcOptions = new OidcApplicationOptions("My OIDC App", "http://loginurl.com") { Enabled = true }; try { var appId = await descopeClient.Management.SsoApplication.CreateOidcApplication(options: oidcOptions); } catch (DescopeException ex) { // Handle the error } ``` ## Update OIDC Application Update an existing OIDC Application with the given parameters. All provided parameters are used as overrides to the existing application. Empty fields will override populated fields. ```javascript // Args: // oidcApplicationOptions (OidcApplicationOptions): Options for the OIDC Application create and update const oidcApplicationOptions = { "name": "My OIDC Application", "loginPageUrl": "https://my-idp-application.com/login", "id": "xxxxx", "description": "This is my OIDC Application", "logo": "https://my-idp-application.com/logo", "enabled": true } const resp = await descopeClient.management.ssoApplication.updateOidcApplication(oidcApplicationOptions) if (!resp.ok) { console.log("Failed to update OIDC Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated OIDC Application.") console.log(resp.data) } ``` ```python # Args: # id (str): The ID of the federated application to update. # name (str): Updated federated application name # login_page_url (str): The URL where login page is hosted. # description (str): Optional federated application description. # logo (str): Optional federated application logo. # enabled (bool): Optional (default True) does the federated application will be enabled or disabled. try: resp = descope_client.mgmt.sso_application.update_oidc_application( id="xxxxx", name="My OIDC Application", login_page_url="https://my-idp-application.com/login", description="This is my OIDC Application", logo="https://my-idp-application.com/logo", enabled=True ) print("Successfully updated OIDC Application.") except AuthException as error: print ("Failed to update OIDC Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // OidcApplicationOptions (&descope.OIDCApplicationRequest): Options for the OIDC Application create and update OidcApplicationOptions := &descope.OIDCApplicationRequest{ ID: "xxxxx", Name: "My OIDC Application", Description: "This is my OIDC Application", Enabled: true, Logo: "https://my-idp-application.com/logo", LoginPageURL: "https://my-idp-application.com/login" } res, err := descopeClient.Management.SSOApplication().UpdateOIDCApplication(ctx, OidcApplicationOptions) if (err != nil){ fmt.Println("Failed to update OIDC Application.", err) } else { fmt.Println("Successfully updated OIDC Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.updateOIDCApplication(OIDCApplicationRequest); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // oidcOptions (OidcApplicationOptions): New full configuration for the existing OIDC Federated App (overrides all fields as is). var oidcOptions = new OidcApplicationOptions("My OIDC App", "http://updated-loginurl.com") { Id = oidcAppId, Enabled = true }; try { await descopeClient.Management.SsoApplication.UpdateOidcApplication(options: oidcOptions); } catch (DescopeException ex) { // Handle the error } ``` ## Create SAML Application Create a new SAML Application with the given name. Application IDs are provisioned automatically but can be explicitly configured if needed. Both the name and ID must be unique per project. ```javascript // Args: // samlApplicationOptions (SamlApplicationOptions): Options for the SAML Application create and update const samlApplicationOptions = { "name": "My SAML Application", "loginPageUrl": "https://my-idp-application.com/login", // "id": (optional), "description": "This is my SAML Application", "logo": "https://my-idp-application.com/logo", "enabled": true, "useMetadataInfo": true, "metadataUrl": "https://myapp.com/metadata", // entityId?: (optional), // "acsUrl": (optional), // "certificate": (optional), // "attributeMapping": (optional), // "groupsMapping": (optional), // "acsAllowedCallbacks": (optional), // "subjectNameIdType": (optional), // "subjectNameIdFormat": (optional) } const resp = await descopeClient.management.ssoApplication.createSamlApplication(samlApplicationOptions) if (!resp.ok) { console.log("Failed to create SAML Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully created SAML Application.") console.log(resp.data) } ``` ```python # Args: # name (str): The federated application's name. # login_page_url (str): The URL where login page is hosted. # id (str): Optional federated application ID. # description (str): Optional federated application description. # logo (str): Optional federated application logo. # enabled (bool): Optional set the federated application as enabled or disabled. # use_metadata_info (bool): Optional determine if SP info should be automatically fetched from metadata_url or by specified it by the entity_id, acs_url, certificate parameters. # metadata_url (str): Optional SP metadata url which include all the SP SAML info. # entity_id (str): Optional SP entity id. # acs_url (str): Optional SP ACS (saml callback) url. # certificate (str): Optional SP certificate, relevant only when SAML request must be signed. # attribute_mapping (List[SAMLIDPAttributeMappingInfo]): Optional list of Descope (IdP) attributes to SP mapping. # groups_mapping (List[SAMLIDPGroupsMappingInfo]): Optional list of Descope (IdP) roles that will be mapped to SP groups. # acs_allowed_callbacks (List[str]): Optional list of urls wildcards strings represents the allowed ACS urls that will be accepted while arriving on the SAML request as SP callback urls. # subject_name_id_type (str): Optional define the SAML Assertion subject name type, leave empty for using Descope user-id or set to "email"/"phone". # subject_name_id_format (str): Optional define the SAML Assertion subject name format, leave empty for using "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". try: resp = descope_client.mgmt.sso_application.create_saml_application( name="My SAML Application", login_page_url="https://my-idp-application.com/login", description="This is my SAML Application", logo="https://my-idp-application.com/logo", enabled=True, use_metadata_info=True, metadata_url="https://myapp.com/metadata", acs_allowed_callbacks=["https://my-idp-application.com/", "https://my-idp-application.com/callback"] ) print("Successfully created SAML Application.") except AuthException as error: print ("Failed to create SAML Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // SAMLApplicationOptions (&descope.SAMLApplicationRequest): Options for the SAML Application create and update OidcApplicationOptions := &descope.SAMLApplicationRequest{ ID: "xxxxx", Name: "My OIDC Application", Description: "This is my OIDC Application", Enabled: true, Logo: "https://my-idp-application.com/logo", LoginPageURL: "https://my-idp-application.com/login", UseMetadataInfo: true, MetadataURL: "https://myapp.com/metadata", // EntityID: (optional), // AcsURL: (optional), // Certificate: (optional), // AttributeMapping: (optional), // GroupsMapping: (optional), // AcsAllowedCallbacks: (optional), // SubjectNameIDType: (optional), // SubjectNameIDFormat:(optional) } res, err := descopeClient.Management.SSOApplication().CreateSAMLApplication(ctx, OidcApplicationOptions) if (err != nil){ fmt.Println("Failed to create SAML Application.", err) } else { fmt.Println("Successfully created SAML Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.createSAMLApplication(SAMLApplicationRequest); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // samlOptions (SamlApplicationOptions): Configuration for the new SAML Federated Application. var samlOptions = new SamlApplicationOptions("samlApp", "http://loginurl.com") { Id = samlAppID, Enabled = true, EntityID = "EntityID", AcsURL = "http://dummy.com/acs", Certificate: "cert", AttributeMapping = new List { new ("attrName1", "attrType1", "attrValue1") }, GroupsMapping = new List { new("grpName1", "grpType1", "grpFilterType1", "grpValue1", new List { new("rl1", "rlName1") }) }, }; try { var appId = await descopeClient.Management.SsoApplication.CreateSAMLApplication(options: samlOptions); } catch (DescopeException ex) { // Handle the error } ``` ## Update SAML Application Update an existing SAML Application with the given parameters. All provided parameters are used as overrides to the existing application. Empty fields will override populated fields. ```javascript // Args: // samlApplicationOptions (SamlApplicationOptions): Options for the SAML Application create and update const samlApplicationOptions = { "name": "My SAML Application", "loginPageUrl": "https://my-idp-application.com/login", // "id": (optional), "description": "This is my SAML Application", "logo": "https://my-idp-application.com/logo", "enabled": true, "useMetadataInfo": true, "metadataUrl": "https://myapp.com/metadata", // entityId?: (optional), // "acsUrl": (optional), // "certificate": (optional), // "attributeMapping": (optional), // "groupsMapping": (optional), // "acsAllowedCallbacks": (optional), // "subjectNameIdType": (optional), // "subjectNameIdFormat": (optional) } const resp = await descopeClient.management.ssoApplication.updateSamlApplication(samlApplicationOptions) if (!resp.ok) { console.log("Failed to update SAML Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated SAML Application.") console.log(resp.data) } ``` ```python # Args: # id (str): The ID of the federated application to update. id = "xxxxx" # name (str): The federated application's name. # login_page_url (str): The URL where login page is hosted. # description (str): Optional federated application description. # logo (str): Optional federated application logo. # enabled (bool): Optional set the federated application as enabled or disabled. # use_metadata_info (bool): Optional determine if SP info should be automatically fetched from metadata_url or by specified it by the entity_id, acs_url, certificate parameters. # metadata_url (str): Optional SP metadata url which include all the SP SAML info. # entity_id (str): Optional SP entity id. # acs_url (str): Optional SP ACS (saml callback) url. # certificate (str): Optional SP certificate, relevant only when SAML request must be signed. # attribute_mapping (List[SAMLIDPAttributeMappingInfo]): Optional list of Descope (IdP) attributes to SP mapping. # groups_mapping (List[SAMLIDPGroupsMappingInfo]): Optional list of Descope (IdP) roles that will be mapped to SP groups. # acs_allowed_callbacks (List[str]): Optional list of urls wildcards strings represents the allowed ACS urls that will be accepted while arriving on the SAML request as SP callback urls. # subject_name_id_type (str): Optional define the SAML Assertion subject name type, leave empty for using Descope user-id or set to "email"/"phone". # subject_name_id_format (str): Optional define the SAML Assertion subject name format, leave empty for using "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified". try: resp = descope_client.mgmt.sso_application.update_saml_application( name="My updated SAML Application", login_page_url="https://my-idp-application.com/login", description="This is my updated SAML Application", logo="https://my-idp-application.com/logo", enabled=True, use_metadata_info=True, metadata_url="https://myapp.com/metadata", acs_allowed_callbacks=["https://my-idp-application.com/", "https://my-idp-application.com/callback"] ) print("Successfully updated SAML Application.") except AuthException as error: print ("Failed to update SAML Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // SAMLApplicationOptions (&descope.SAMLApplicationRequest): Options for the SAML Application create and update OidcApplicationOptions := &descope.SAMLApplicationRequest{ ID: "xxxxx", Name: "My OIDC Application", Description: "This is my OIDC Application", Enabled: true, Logo: "https://my-idp-application.com/logo", LoginPageURL: "https://my-idp-application.com/login", UseMetadataInfo: true, MetadataURL: "https://myapp.com/metadata", // EntityID: (optional), // AcsURL: (optional), // Certificate: (optional), // AttributeMapping: (optional), // GroupsMapping: (optional), // AcsAllowedCallbacks: (optional), // SubjectNameIDType: (optional), // SubjectNameIDFormat:(optional) } res, err := descopeClient.Management.SSOApplication().UpdateSAMLApplication(ctx, OidcApplicationOptions) if (err != nil){ fmt.Println("Failed to update SAML Application.", err) } else { fmt.Println("Successfully updated SAML Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.updateSAMLApplication(SAMLApplicationRequest); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // samlOptions (SamlApplicationOptions): Full configuration to apply to the existing SAML app (overrides all fields as is). var samlOptions = new SamlApplicationOptions("samlApp", "http://loginurl.com") { Id = samlAppID, Enabled = true, UseMetadataInfo = true, MetadataURL = "https://metadata.com", }; try { await descopeClient.Management.SsoApplication.UpdateSamlApplication(options: samlOptions); } catch (DescopeException ex) { // Handle the error } ``` ## Delete an Application Delete an existing Application. This action is irreversible. Use carefully. ```javascript // Args: // id (str): The ID of the federated application to delete. const id = "xxxxx" const resp = await descopeClient.management.ssoApplication.delete(id) if (!resp.ok) { console.log("Failed to delete Application.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted Application.") console.log(resp.data) } ``` ```python # Args: # id (str): The ID of the federated application to delete. try: resp = descope_client.mgmt.sso_application.delete(id="xxxxx") print("Successfully deleted Application.") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to delete Application.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // ID (str): The ID of the federated application to delete. ID := "xxxxx" res, err := descopeClient.Management.SSOApplication().Delete(ctx, ID) if (err != nil){ fmt.Println("Failed to delete Application.", err) } else { fmt.Println("Successfully deleted Application.") } ``` ```java SsoApplicationService ssoas = descopeClient.getManagementServices().getSsoApplicationService(); try { IdPApplications resp = ssoas.delete(id); // Do something } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // appID (string): The ID of the federated application to delete (irreversible). var appID = "app-id"; try { await descopeClient.Management.SsoApplication.Delete(id: appID); } catch (DescopeException ex) { // Handle the error } ``` # Authorization Server Endpoints (/identity-federation/inbound-apps/authorization-server) Descope OAuth 2.0 authorization server endpoints for Inbound Apps (authorize, token, revoke, and userinfo) with links to the API reference. # Authorization Server Endpoints When you configure an [Inbound App](/identity-federation/inbound-apps), Descope acts as the **OAuth 2.0 / OpenID Connect authorization server** for that client. Third-party applications, AI agents, and MCP clients call a shared set of HTTP endpoints under `/oauth2/v1/apps/` to start user login, exchange codes for tokens, refresh sessions, and read token claims. Each Inbound App also exposes a **Discovery URL** and **Issuer** in the Descope Console (see [Creating Inbound Apps](/identity-federation/inbound-apps/creating-inbound-apps#connection-information)). Standard OAuth libraries use those values to resolve the same endpoints listed below automatically. If you use a [custom domain](/how-to-deploy-to-production/custom-domain#configure-custom-domain), replace `https://api.descope.com` with your configured API hostname in every URL on this page. ## Base URL | Environment | Base URL | | ----------- | -------- | | Descope (default) | `https://api.descope.com` | | Custom domain | `https://` | All authorization server routes are rooted at: ```text {baseUrl}/oauth2/v1/apps/... ``` ## Endpoint Reference | Endpoint | Method | Purpose | API reference | | -------- | ------ | ------- | ------------- | | [`/oauth2/v1/apps/authorize`](/api/third-party-apps/authorization-get) | `GET` | Start the authorization code flow (browser redirect) | [Get authorization](/api/third-party-apps/authorization-get) | | [`/oauth2/v1/apps/authorize`](/api/third-party-apps/authorization-post) | `POST` | Start authorization with a JSON body (non-browser clients) | [Post authorization](/api/third-party-apps/authorization-post) | | [`/oauth2/v1/apps/token`](/api/third-party-apps/token-endpoint) | `POST` | Issue and refresh tokens; client credentials; JWT bearer; token exchange | [Token endpoint](/api/third-party-apps/token-endpoint) | | [`/oauth2/v1/apps/revoke`](/api/third-party-apps/revoke-token) | `POST` | Revoke access or refresh tokens | [Revoke token](/api/third-party-apps/revoke-token) | | [`/oauth2/v1/apps/userinfo`](/api/third-party-apps/user-info-get) | `GET` | Return claims for the bearer access token | [Get UserInfo](/api/third-party-apps/user-info-get) | | [`/oauth2/v1/apps/userinfo`](/api/third-party-apps/user-info-post) | `POST` | Return claims (POST variant for clients that require it) | [Post UserInfo](/api/third-party-apps/user-info-post) | ## Discovery and JWKs Inbound Apps are OpenID Connect-compatible. For each app, the Console provides: - **Discovery URL**: OpenID Provider metadata (`authorization_endpoint`, `token_endpoint`, `jwks_uri`, supported scopes, and grant types). - **Issuer**: Value used to validate `iss` on ID tokens and access tokens. Its host is the base URL shown above: your region's default Descope host (for example `https://api.descope.com`), or your [custom domain](/how-to-deploy-to-production/custom-domain) if you have one set up for your Descope project. Project-level well-known documents are also available when you need metadata scoped to the whole project: ```text __BaseURL__/v1/apps/{projectId}/.well-known/openid-configuration __BaseURL__/v1/apps/{projectId}/.well-known/oauth-authorization-server ``` ### MCP Server Resources [MCP servers](/agentic-identity-hub/core-components/mcp-servers) are defined as [MCP Server Resources](/resources#mcp-server-resources): the same OAuth resource model as API Resources. Descope uses one authorization server for all Inbound Apps; to authenticate against a **specific** Resource (including an MCP server), include the **`resource`** parameter ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)) on the [authorize](/api/third-party-apps/authorization-get) or [token](/api/third-party-apps/token-endpoint) request. Use the Resource identifier from the console, typically your MCP server URL, which also appears in the token `aud` claim. MCP clients discover that Resource through OAuth Protected Resource Metadata on your server; see [MCP discovery URL](/agentic-identity-hub/core-components/mcp-servers/discovery-url) for the client-side discovery flow. ## Token Endpoint The [token endpoint](/api/third-party-apps/token-endpoint) is the central exchange point for Inbound Apps. Send `application/x-www-form-urlencoded` (or JSON, per the API schema) with a `grant_type` and the parameters required for that grant. | Grant type | Typical use | Guide | | ---------- | ----------- | ----- | | `authorization_code` | User-delegated access after consent | [Authorization code flow](/identity-federation/inbound-apps/using-inbound-apps#authorization-code-flow) | | `refresh_token` | Renew an access token without re-consent | [Refreshing access tokens](/identity-federation/inbound-apps/using-inbound-apps#5-refreshing-access-tokens) | | `client_credentials` | Machine-to-machine Inbound App tokens | [Client credentials flow](/identity-federation/inbound-apps/using-inbound-apps#client-credentials-flow) | | `urn:ietf:params:oauth:grant-type:jwt-bearer` | Exchange a trusted external JWT for Descope tokens | [External token management](/identity-federation/inbound-apps/using-inbound-apps#external-token-management) | | `urn:ietf:params:oauth:grant-type:token-exchange` | Trade a subject token the caller already holds for one scoped to a specific downstream [Resource](/resources), governed by [Policies](/policies) (RFC 8693) | [Token exchange](#token-exchange) | Common optional parameters on the token endpoint include: - **`scope`**: Requested OAuth scopes (must be allowed on the Inbound App and, for user flows, approved in consent). - **`resource`**: RFC 8707 resource indicator (single Resource URI per request). Required when issuing tokens for a specific [API or MCP Server Resource](/resources); pass the same value on authorize and token requests. - **`audience`**: Target audience for issued tokens or exchanges. Example (authorization code exchange): ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=" \ -d "client_secret=" \ -d "code=" \ -d "redirect_uri=https://yourapp.com/callback" ``` See the [Token endpoint API reference](/api/third-party-apps/token-endpoint) for the full request and response schema. ### Client authentication Confidential clients have to prove who they are on every token request. Descope supports two methods. **Client secret.** The client sends `client_id` and `client_secret`, as in the example above. This is the default and needs no extra setup. **Private key JWT.** Instead of sending a shared secret, the client signs a short-lived JWT with its own private key and sends it as a `client_assertion` ([RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523)). Descope validates the signature against the public keys the client publishes, so no shared secret is ever transmitted or stored on the Descope side. ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=" \ -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ -d "client_assertion=" \ -d "code=" \ -d "redirect_uri=https://yourapp.com/callback" ``` Prefer private key JWT when you would rather not distribute a long-lived shared secret, or when a compliance requirement rules one out. It is also the stronger option for [Cross-App Access](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works#client-authentication-for-xaa), where the client handles assertions that can be redeemed for access tokens elsewhere. Private key JWT client authentication for Inbound Apps and [Agentic Clients](/agentic-identity-hub/core-components/clients) is behind a feature flag. Contact Descope to have it enabled for your project. Public (non-confidential) clients do not authenticate this way. They send `client_id` with no secret and rely on [PKCE](/identity-federation/inbound-apps/using-inbound-apps#using-pkce-public-clients---spas-mobile-apps) to protect the exchange. Descope authenticating **to** an external provider with a signed JWT is a separate feature. See [Private Key JWT for custom OAuth providers](/auth-methods/oauth/providers/custom-providers#private-key-jwt) and [OIDC SSO](/auth-methods/sso/oidc#private-key-jwt) for that direction. ### Token exchange Token exchange ([RFC 8693](https://www.rfc-editor.org/rfc/rfc8693)) trades a **subject token** the caller already holds for a new token scoped to a specific downstream [Resource](/resources). A client uses it to turn the token it authenticated with into an access token for your API or MCP server, or into an [ID-JAG](/agentic-identity-hub/enterprise-managed-authorization) for a third-party server. The same grant is available to [Agentic Clients](/agentic-identity-hub/core-components/clients), which use this same token endpoint. Which subjects may exchange for which Resources, and with which scopes, is controlled by [Policies](/policies). The policy's **subject** is the client or app, and its **target** is the Resource and the scopes it allows; the exchange fails when no active policy permits it. Pass the token being exchanged as `subject_token`, its type as `subject_token_type`, and the target Resource as `resource` ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707)): ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ -d "client_id=" \ -d "client_secret=" \ -d "subject_token=" \ -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \ -d "resource=https://your-api.example.com" \ -d "scope=orders.read" ``` ## Authorize Endpoint The [authorize endpoint](/api/third-party-apps/authorization-get) starts interactive login and consent. Browser-based apps redirect users with query parameters such as `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, PKCE fields (`code_challenge`, `code_challenge_method`), and optionally **`resource`** to target a specific [Resource](/resources) (required when authenticating to an MCP Server Resource). Example redirect: ```bash __BaseURL__/oauth2/v1/apps/authorize?\ client_id=&\ redirect_uri=https://yourapp.com/callback&\ response_type=code&\ scope=openid%20email%20profile&\ state= ``` When targeting an MCP Server Resource, add `resource` (URL-encoded Resource URI): ```bash __BaseURL__/oauth2/v1/apps/authorize?\ client_id=&\ redirect_uri=https://yourapp.com/callback&\ response_type=code&\ scope=mcp:tools.read&\ resource=https%3A%2F%2Fyour-mcp.example.com%2Fmcp&\ state= ``` After the user completes the [consent flow](/flows), Descope redirects back to `redirect_uri` with an authorization `code`. Exchange that code at the [token endpoint](/api/third-party-apps/token-endpoint), including the same `resource` value if you specified one at authorize time. ## Revoke Endpoint The [revoke endpoint](/api/third-party-apps/revoke-token) invalidates an access or refresh token ([RFC 7009](https://www.rfc-editor.org/rfc/rfc7009)). Call it when a user disconnects an integration, you rotate credentials, or you need to end a session without waiting for natural expiry. Send `application/x-www-form-urlencoded` (or JSON, per the API schema) with: - **`token`**: The access or refresh token to revoke. - **`token_type_hint`**: Optional hint: `access_token` or `refresh_token`. - **`client_id`** / **`client_secret`**: Inbound App credentials (required for confidential clients). Example (revoke a refresh token): ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "token=" \ -d "token_type_hint=refresh_token" \ -d "client_id=" \ -d "client_secret=" ``` Revoking a refresh token prevents future renewals at the [token endpoint](/api/third-party-apps/token-endpoint). Revoking an access token invalidates it for subsequent API calls. See the [Revoke token API reference](/api/third-party-apps/revoke-token) for the full request and response schema. ## User Info Endpoint The [UserInfo endpoint](/api/third-party-apps/user-info-get) returns OpenID Connect claims for the subject of a valid access token. Use it when your app needs profile data (`sub`, `email`, `name`) or custom claims mapped in the Inbound App beyond what is embedded in the JWT. For when to introspect vs validate locally, see [Token introspection](/sessions/introspection). Send the access token as a Bearer credential on the GET endpoint: the standard OIDC variant. A [POST variant](/api/third-party-apps/user-info-post) is available for clients that require it. Example (GET): ```bash curl "__BaseURL__/oauth2/v1/apps/userinfo" \ -H "Authorization: Bearer " ``` Example response (shape varies by scopes and Inbound App configuration): ```json { "sub": "U2lz...", "email": "user@example.com", "email_verified": true, "name": "Jane Doe" } ``` The access token must include the `openid` scope, and any scopes required for specific claims (such as `email` or `profile`). Claims returned depend on what the user approved during [consent](/flows) and how the Inbound App maps user attributes. See the [Get UserInfo](/api/third-party-apps/user-info-get) and [Post UserInfo](/api/third-party-apps/user-info-post) API references for the full request and response schema. ## Configure clients and scopes - Create and manage Inbound Apps in the [Console](https://app.descope.com/apps/inbound) or via [Management API](/api/management/third-party-apps/create-third-party-application). - Map API permissions to OAuth scopes on [Resources](/resources). - Implement resource servers that validate Descope JWTs in [Developing APIs with OAuth](/identity-federation/inbound-apps/developing-apis). ## Related - [Using Inbound Apps](/identity-federation/inbound-apps/using-inbound-apps): step-by-step flows for each grant type - [Inbound Apps API reference](/api/third-party-apps): interactive OpenAPI docs for every authorization server route - [Resources](/resources): define scopes and audiences your tokens target - [Agentic Identity Hub](/agentic-identity-hub): agents, MCP servers, Connections, and STS token exchange # Creating Inbound Apps (/identity-federation/inbound-apps/creating-inbound-apps) Step-by-step guide to setting up an Inbound App in Descope. # Creating Inbound Apps Inbound apps in Descope can be created in a few ways: - **Manually** through either an API/SDK or the Descope Console - **As code** with [Terraform](/managing-environments/terraform), using the Descope Terraform provider to manage Inbound Apps alongside the rest of your project configuration - **Automatically** using Dynamic Client Registration (DCR) In addition to how they are created, Inbound Apps can be created either as a **confidential** or **non-confidential** application. Creating the Inbound App does not by itself grant it access to anything. To let a manually created app reach a [Resource](/resources), an API or MCP server, you must also add a [policy](/policies) whose subject is this Inbound App and whose target is that Resource, with the scopes it may receive. The Console shows a reminder in the bottom-right corner after you create the app. This applies only to **manually created** apps. Clients registered through DCR automatically get access to the MCP server Resource they registered with; no policy required. ## Types of Inbound Apps Once an app is created, its type (confidential vs. non-confidential) cannot be changed. Also, confidential clients **cannot** be created through DCR at this time. **Confidential clients** - Designed for applications that can securely store a client secret (e.g., backend servers, server-side web apps). - May use PKCE in the authorization code flow (optional). - **Token refresh requires a `client_secret`**, ensuring stronger security. - Authenticate with a client secret, or with [private key JWT](/identity-federation/inbound-apps/authorization-server#client-authentication) if you would rather not distribute a shared secret. **Non-confidential (public) clients** - For applications that cannot securely store secrets (e.g., SPAs, mobile apps, desktop apps without secure storage). - Must rely on PKCE for secure authorization flows. - Do not use a `client_secret`. - Can refresh tokens without a `client_secret`, using only the refresh token. ## Method 1: Manual Inbound App Creation If you create an Inbound App using API/SDK, you can register the app as a `non-confidential` client. All apps created within the Descope Console are always registered as `confidential` clients. You can either create an inbound app using the Descope console, or using the [Descope Management API](#creating-an-app-with-an-api-or-sdk). To manually create an Inbound App in the Descope Console, follow these steps: 1. Navigate to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope console. 2. Click **`+ Add Inbound App`** to create a new Inbound Application. 3. Define the **App Name** and **Description** (both can be edited later). ![Creating an inbound app in Descope](/assets/create_inbound_app.webp) Once the application is created, you can configure its settings, including: - **App Details** - **Scopes** (Permissions & User Data) - **Connection Information** - **Consent Management** ### Inbound App Details This section allows you to configure key information about the inbound app. - **Logo (Optional)**: Upload a logo for the application. This will be displayed during user consent flows. - **Inbound App Name (Required)**: The name of the third-party application. - **Description (Optional)**: A short description of the app. - **App ID (System Generated)**: A unique identifier for the application (cannot be changed). ![Configuring an inbound app's details in Descope](/assets/inbound_app_details.webp) ### Scopes Create the [API Resource](/resources) first and define scopes there with [RBAC role mapping](/resources/scopes-and-roles#api-resources), then [associate the Resource](/resources/scopes-and-roles#api-resources) with this Inbound App. The Inbound App references that Resource and requests its scopes at authorize time. The console may also show scope configuration on the Inbound App for the linked Resource. ![Configuring an inbound app's scopes in Descope](/assets/inbound_app_scopes.webp) #### Permission Scopes Permission scopes allow the inbound app to perform specific actions on behalf of the user or tenant. On API Resources, these scopes are mapped to [Descope RBAC roles](/authorization/role-based-access-control), ensuring that access is granted only to authorized users. - **Name (Required)**: The unique identifier for the permission scope. - **Description (Required)**: A brief summary of the permission. - **Roles (Optional)**: Assign roles if **RBAC is enabled**, restricting scope access to users with the appropriate role. #### User Information Scopes These scopes define what **user data** is shared with the third-party application. The data can include built-in Descope attributes (e.g., email, name) or custom attributes that your organization defines. - **Name (Required)**: The unique identifier for the user data scope. - **Description (Required)**: A summary of the data being shared with the third-party application. - **User Attribute (Required)**: The specific **user attribute** mapped to this scope. Access to this data follows Descope's role-based authorization ([RBAC](/authorization/role-based-access-control)) model, ensuring proper permissions are enforced. #### Include All Authorization Info in Access Tokens In the **Scopes** section, you can enable / disable the **Always include all authorization info in access tokens** setting to control which authorization data is embedded in the issued JWTs. When **enabled**, access tokens include **all tenants, roles, and permissions** for the user in the JWT, regardless of which scopes were requested or approved. Scope consent still controls what the client may **request**; the token is not limited to your scope-to-role mapping. When **disabled** (default), tokens only include authorization derived from the approved scopes and scope configuration above. ![Include all authorization info in access tokens](/assets/inbound-scopes-settings.webp) #### User Consent and Role-Based Restrictions The end user will not be able to update user consent through the flow if they do not have the appropriate role associated with the scope they are requesting. This means: - If a scope requires a role that the user does not have, they will **not** be able to grant consent for it. - Only users with the correct RBAC roles will have the ability to approve or manage consent for those specific scopes. By enforcing role-based user consent, Descope ensures that only authorized users can share sensitive data or grant permissions, maintaining strong access control and compliance. ### Grant Types #### Client-Initiated Backchannel Authentication (CIBA) Inbound Apps in Descope also support **Client-Initiated Backchannel Authentication (CIBA)**, an OAuth 2.0 extension designed for **decoupled** or **headless** experiences where the device initiating the request cannot easily present a browser-based login or consent screen. Instead of redirecting the user, Descope sends them an out-of-band message (for example, an email) with a link to log in and approve the request on a separate device. **With CIBA enabled on an inbound app:** 1. A client application sends a **backchannel authentication request** to Descope on behalf of a user. 2. Descope sends the user an **out-of-band message** (for example, an email) that contains a link to log in and approve or deny the request. 3. The user authenticates and approves the request using a standard login/consent experience. 4. The client periodically polls the Descope `/token` endpoint using an `auth_req_id` until the user has approved, at which point Descope returns the usual OAuth token set (access token, refresh token, ID token). #### CIBA Configuration At the inbound app level, CIBA is controlled through the app's **CIBA settings**, which include: - **Enable CIBA** - Turns backchannel authentication on for this inbound app. - **Connector** - Which email provider configuration Descope should use to send CIBA approval emails, plus an optional fallback provider. - **Template** - The HTML template to use for the out-of-band approval message. - **Link Expiration** - How long the link in the out-of-band message remains valid before expiring. ![CIBA configuration panel](/assets/ciba-configuration.webp) Once CIBA is enabled and templates are configured, clients can use the inbound app's **backchannel authentication endpoint** (exposed via the inbound app's well-known) to start CIBA flows and then poll the `/token` endpoint until the user completes authentication. #### Configuring the CIBA Flow After the initial CIBA connection settings, you can also configure **which Descope flow** should run when a user approves the backchannel authentication request. In the **Flows** section of the inbound app's CIBA settings, you have two options: - **Use the pre-built CIBA flow (recommended)**: Descope provides a best-practice flow for authenticating the user and completing the CIBA approval process; click **Generate Flow** to create it. - **Build your own flow and select it**: If you need custom screens or additional approval logic, you can create your own flow and select that flow for CIBA. If you choose a custom flow, it must include all required CIBA behavior: - **CIBA User Code Verification** action to verify the CIBA user code and ensure the user is signed in before approving the request. - **CIBA approval action** that records whether the user approved or denied the pending CIBA request. - A **completion path** for both outcomes: - **Approve**: completes the CIBA transaction so `/token` polling can return tokens. - **Deny**: rejects the CIBA transaction so polling clients receive a denial/failed result. Without the CIBA approval/consent action in the flow, the backchannel request remains incomplete and the client polling `/token` will not receive a successful token response. ![CIBA flow configuration panel](/assets/ciba-flow.webp) ### Connection Information This section contains configuration details needed to integrate the inbound app with Descope. If you have a custom domain configured, the system-generated URLs will use your custom domain instead of `api.descope.com`. - **Flow Hosting URL (Required)**: The URL where the consent flow is hosted. It can be: - `api.descope.com` - Your [custom domain](/how-to-deploy-to-production/custom-domain#configure-custom-domain) - A self-hosted instance of the consent flow. - **Approved Callback URLs (Optional)**: The URLs Descope is allowed to redirect users to after authentication. - **Client ID (System Generated)**: The unique identifier for the application. - **Client Secret (System Generated)**: The shared secret used for authentication. - **Discovery URL (System Generated)**: Provides OpenID Connect configuration details, including supported scopes, public keys, and endpoints. - **Issuer (System Generated)**: The issuer URL used for verifying identity tokens. - **Authorization URL (System Generated)**: The endpoint for initiating authentication (`__BaseURL__/oauth2/v1/apps/authorize`). See [Get authorization](/api/third-party-apps/authorization-get) in the API reference. - **Access Token URL (System Generated)**: The endpoint for retrieving access tokens (`__BaseURL__/oauth2/v1/apps/token`). See [Token endpoint](/api/third-party-apps/token-endpoint) in the API reference. For a full list of authorization server routes, grant types, and examples, see [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server). ![Configuring an inbound app's connection information in Descope](/assets/inbound_app_connection_information.webp) ### Session Management #### Token Format You can set a specific JWT template for the user inbound app tokens, as well as the M2M inbound app tokens created with client credentials flow. ![Token Format](/assets/token-format-inbound-app.webp) #### Token Expiration You can also set a specific expiration time for the access and refresh tokens created for the inbound app. ![Token Expiration](/assets/token-expiration-inbound-app.webp) ### Creating an App with an API or SDK Visit our [Management API docs](/api/management/third-party-apps/create-third-party-application) or [SDK guide](/identity-federation/inbound-apps/sdks) to create and manage inbound apps programmatically. This is currently the only way for you to create a non-confidential inbound app, without using DCR. ## Method 2: Dynamic Client Registration (DCR) Dynamic Client Registration allows OAuth clients to register themselves with your OAuth server automatically, which is particularly useful for protecting [MCP (Model Context Protocol)](/mcp) servers. This feature enables seamless integration with MCP clients like Cursor and Claude Desktop that need to dynamically register as OAuth clients. ### Enabling Dynamic Client Registration To enable DCR for your project: 1. Navigate to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope console. 2. In the top right, click on the **Inbound App Settings** icon. ![Inbound App Settings](/assets/inbound-app-settings.webp) 3. Toggle **Enable dynamic client registration** for your project. ![Enabling Dynamic Client Registration](/assets/enable-dcr.webp) Inbound apps created via Dynamic Client Registration (DCR) are automatically registered as `non-confidential` clients. Once enabled, you can configure the following DCR settings: #### Approved Scopes List Defining mandatory and optional scopes is useful for allowing the end user to control which scopes they grant to OAuth clients that dynamically register with your Descope project. Configure the list of scopes that are approved for granting as part of the DCR process: - **Name**: The name of the scope. This is what will be included in the scope parameter of the OAuth request, and in the OAuth tokens - **Description**: A brief description of what the scope allows - **Roles**: Map the scope to specific Descope roles for RBAC enforcement - **Mandatory**: Toggle whether this scope is required for all DCR registrations ![Approved Scopes List](/assets/approved-scopes-list.webp) We recommend setting as few scopes as possible to mandatory, and allowing the end user to grant more scopes if they choose to. If you want to force the end user to grant consent to all scopes, you can set all of them to be mandatory. #### Empty Scope Handling By default, DCR handles empty scope requests automatically. This is useful when your OAuth client doesn't request any scopes automatically. You can toggle this behavior on or off based on your security requirements. When your OAuth client [authorizes](/identity-federation/inbound-apps/using-inbound-apps#authorization-code-flow) with an inbound app created with DCR, without any scopes, the end user will simply be able to register the app, but the token will not include any scopes, and thus will not have access to any protected APIs that have scope validation. The consent screen will simply ask the user to "authorize" the app, without any additional information. #### Flow Hosting URL Set the Flow Hosting URL to the URL of the consent flow you wish to run for DCR registrations. - **Flow Hosting URL (Required)**: The URL where the consent flow is hosted. It can be: - `api.descope.com` - Your [custom domain](/how-to-deploy-to-production/custom-domain#configure-custom-domain) - A self-hosted instance of the consent flow. ![Flow Hosting URL](/assets/flow-hosting-url-inbound-app.webp) #### Approved Redirect URLs To restrict registration to specific applications, you can configure approved redirect URLs. This ensures that only applications with matching redirect URLs can register through the DCR process, providing an additional layer of security. For example, if you want to allow only certain OAuth clients to connect, you can use a pattern like: ``` cursor://anysphere.cursor-retrieval/oauth/*/callback ``` In this example, the `*` acts as a wildcard for the client-specific portion of the redirect URL. If you are using Cursor MCP, the `*` would match the `mcpServer` name defined in your Cursor `mcp.json` file. This pattern can be adapted for any OAuth client, not just MCP Clients. The Cursor example above demonstrates how you might use a wildcard to securely allow a family of related clients. ### The `/register` Endpoint If you don't enable DCR for your project, you will not see a `/register` endpoint in your Inbound App well known configuration. When DCR is enabled, clients can register themselves using the `/register` endpoint. This endpoint accepts a POST request with the following parameters: #### Required Parameters - **client_name** (string): The name of the client application - **redirect_uris** (array of strings): Array of redirect URIs that the client will use. Must contain at least one valid URL. #### Optional Parameters - **client_uri** (string): URL of the client's home page - **logo_uri** (string): URL of the client's logo - **logo_content** (string): Base64-encoded logo content - **scope** (string): Space-separated list of requested scopes - **description** (string): Description of the client application - **token_endpoint_auth_method** (string): Authentication method for the token endpoint - **grant_types** (array of strings): Array of grant types the client will use - **response_types** (array of strings): Array of response types the client will use - **consent_flow_id** (string): ID of the consent flow to use for this application - **login_page_url** (string): Custom login page URL for the application #### Example Registration Request ```json { "client_name": "My MCP Client", "redirect_uris": ["cursor://anysphere.cursor-retrieval/oauth/myserver/callback"], "scope": "read:user write:user", "client_uri": "https://myclient.com", "logo_uri": "https://myclient.com/logo.png", "description": "A client application for MCP server integration", "token_endpoint_auth_method": "client_secret_basic", "grant_types": ["authorization_code"], "response_types": ["code"], "permissions_scopes": [ { "name": "read:user", "description": "Read user information", "roles": ["user"] } ], "attributes_scopes": [ { "name": "profile", "description": "Access to user profile information", "user_attribute": "email" } ] } ``` #### Registration Response Upon successful registration, the endpoint returns: ```json { "client_id": "generated_client_id", "client_secret": "generated_client_secret", "client_id_issued_at": 1640995200, "client_secret_expires_at": 0, "redirect_uris": ["cursor://anysphere.cursor-retrieval/oauth/myserver/callback"], "scope": "read:user write:user", "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "client_secret_basic" } ``` The client_secret is only returned once during registration. Make sure to store it securely as it cannot be retrieved later. #### Registration Validation The `/register` endpoint validates requests against your DCR configuration: - **redirect_uris**: Must contain at least one valid URL and match patterns in your approved redirect URLs - **scope**: Must be from your approved scopes list - **client_name**: Must be provided and non-empty If validation fails, the endpoint returns an appropriate error response with details about what needs to be corrected. ## Building a Consent Flow Inbound Apps include additional flow components to allow your end users to easily provide consent to your OAuth server. ### Inbound App Flow Components There are two components in Descope flows created specifically for Inbound Apps: - **Inbound App Logo** - Displays the app's configured logo and connection arrows. - **Inbound App Scopes** - Automatically shows the scopes configured for the app, ensuring users can review permissions before proceeding. ![Inbound application flow components in Descope](/assets/inbound_app_flow_components.webp) ### Implementing a Consent Flow This is mandatory for [FHIR](/healthcare/smart-fhir) and highly recommended for [MCP](/mcp) integrations. To integrate a consent screen into your flow: 1. Use a [subflow](/flows/intro-to-flows/subflows) to keep your authentication flow consistent. 2. After authentication, check if the user has already granted consent using `thirdPartyApp.user.consented`. - If consent is given, proceed to the next step. - If not, display the consent screen. ![Checking if the user has granted consent](/assets/inbound_app_consent_condition.webp) 3. Use the `Update User Consent` action to save the user's response. Here you can also pass in the amount of time that the user's consent is valid for, by either specifying a number or a dynamic value that you ask the user to provide in the consent screen. ![Update User Consent](/assets/update-consent.webp) 4. If the user denies consent, prompt them to review or return. 5. Customize the flow as needed for your app's logic and design. #### Example Consent Flow For more consent-based flow examples, visit our [Flow Template library](https://app.descope.com/flows) in the Descope Console. ![Example of a completed consent flow](/assets/3rd_party_app_completed_consent_flow.webp) This setup ensures a smooth user experience while enforcing consent-based access control. ## Managing User Consent You will not be able to create an inbound app token, without the end user providing consent in the flow defined in your [Flow Hosting URL](#connection-information). This will result in an error if you do not possess the consent screen and action within your flow. The **Consent** tab provides visibility into which users have authorized the inbound app and what permissions they granted. - **Consent ID**: A unique identifier for the user's consent. - **Scopes**: The granted permissions. - **Associated User**: The ID of the user who granted consent. - **Associated Tenant**: The tenant ID, if applicable. - **Granting User**: The user who authorized the app (e.g., an admin granting consent for a team). - **Expiration Time**: The time that the user's consent expires. - **Creation Time**: Timestamp when consent was given. ![Viewing inbound app consents in Descope](/assets/inbound-app-consents.webp) ## Managing Inbound Apps in the Console In the Descope Console, you can easily search and filter through all configured inbound apps. Supported filters include: - **Name** - the name of the inbound app (if using MCP the name of the MCP client) - **Description** - the description of the inbound app - **ID** - the ID of the inbound app - **Version** - the version of the inbound app - **Status** - the status of the inbound app (verified or unverified) - **Created Time** - the time the inbound app was created - **Client ID** - the client ID of the inbound app - **Created Method** - either manually via an API/SDK or via the Descope Console, or automatically via [Dynamic Client Registration](/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr) ![Inbound App Management](/assets/inbound-app-management.webp) From the Console, you can: - View and manage user consents - Create new versions - Revoke or disable specific apps - Link to relevant flows or tenants ## Next Steps Once your Inbound App is configured, integrate it into your external applications by using Descope's OAuth authentication flows. - [Using Inbound Apps](/identity-federation/inbound-apps/using-inbound-apps) - Learn how to authenticate users with Descope as an IdP. - [Developing APIs to Support OAuth Scopes & Permissions](/identity-federation/inbound-apps/developing-apis) - Secure your APIs with OAuth and manage user consent. # Developing APIs with OAuth (/identity-federation/inbound-apps/developing-apis) Guide to designing and implementing APIs that enforce OAuth-based authentication and authorization. # Developing APIs with OAuth To fully leverage Inbound Apps, your APIs should be designed to enforce OAuth scopes and permissions effectively. This ensures secure and granular access control, allowing AI agents, partner applications, and users to interact with your APIs while respecting consented permissions. ## Designing an OAuth-specific API Below is a generic OpenAPI spec for constructing an API that integrates Descope as an OAuth provider, manages user consent and scopes, and enforces general authorization using OAuth tokens. This OpenAPI spec: - Defines an authentication mechanism using Descope as an OAuth Provider with Inbound Apps. - Includes endpoints that validate and enforce scopes. - Supports role-based authorization with OAuth scopes. - Implements OAuth 2.0 Bearer Token Authentication. ### OpenAPI 3.0 Specification (YAML) You can test this API spec with Swagger [here](https://editor.swagger.io/). ```yaml openapi: 3.0.3 info: title: Example API with Descope Inbound Apps description: | This API uses Descope as an OAuth provider to authenticate users, manage consent and scopes, and enforce authorization for API access. version: 1.0.0 servers: - url: https://api.yourservice.com description: Production Server components: securitySchemes: OAuth2: type: oauth2 description: "Use Descope as an OAuth 2.0 provider for authentication." flows: authorizationCode: authorizationUrl: __BaseURL__/oauth2/v1/apps/authorize tokenUrl: __BaseURL__/oauth2/v1/apps/token scopes: contacts.read: "Read user's contacts" contacts.write: "Modify user's contacts" profile: "Access user's basic profile information" admin: "Full administrative access" schemas: ErrorResponse: type: object properties: error: type: string description: Error message explaining why the request failed. security: - OAuth2: [] # Require authentication by default paths: /user/profile: get: summary: Get User Profile description: Retrieve authenticated user's profile information. operationId: getUserProfile security: - OAuth2: [profile] responses: "200": description: Successfully retrieved user profile. content: application/json: schema: type: object properties: id: type: string name: type: string email: type: string "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" /contacts: get: summary: List User's Contacts description: Retrieve a list of contacts for the authenticated user. operationId: getContacts security: - OAuth2: [contacts.read] responses: "200": description: Successfully retrieved contacts. content: application/json: schema: type: array items: type: object properties: id: type: string name: type: string email: type: string "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "403": description: Forbidden - Missing required scope content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" post: summary: Add a New Contact description: Create a new contact for the authenticated user. operationId: createContact security: - OAuth2: [contacts.write] requestBody: required: true content: application/json: schema: type: object properties: name: type: string email: type: string responses: "201": description: Contact successfully created. content: application/json: schema: type: object properties: id: type: string name: type: string email: type: string "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "403": description: Forbidden - Missing required scope content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" /admin/users: get: summary: Get All Users (Admin Only) description: Retrieve a list of all users in the system. Requires admin scope. operationId: getAllUsers security: - OAuth2: [admin] responses: "200": description: Successfully retrieved users. content: application/json: schema: type: array items: type: object properties: id: type: string name: type: string email: type: string "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" "403": description: Forbidden - Requires admin scope content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" ``` ### How This OpenAPI Spec Works The `authorizationUrl` and `tokenUrl` values map to Descope's [authorization server endpoints](/identity-federation/inbound-apps/authorization-server). See the [API reference](/api/third-party-apps/authorization-get) and [token endpoint](/api/third-party-apps/token-endpoint) for parameters and responses. 1. **OAuth2 Authentication** - Uses Descope as an OAuth provider with an **authorization code flow**. - Enforces **OAuth scopes** at the API level. 2. **Authorization Enforcement** - **User Profile (`profile` scope)** → Required for retrieving user details. - **Contacts (`contacts.read`, `contacts.write` scopes)** → Controls read/write access. - **Admin Actions (`admin` scope)** → Only users with the `admin` scope can access system-wide data. 3. **Consent-Driven Access** - Users must grant consent before third-party applications can access their data. ## Best Practices for Securing OAuth-based APIs When developing APIs that rely on OAuth for authentication and authorization, consider the following best practices to ensure secure and compliant access control. ### 1. Enforce Scope-Based Access Control Scopes define what an application can do on behalf of a user. When an inbound app requests an access token, the API must verify that the token includes the required scopes for the requested operation. Scope enforcement can be handled in two ways: - **At the API level** - Using middleware within your application to validate scopes before processing requests. See the example below. - **At the API Gateway level** - Many API gateways natively support JWT validation and can enforce scopes before requests reach your backend. For more details on using API gateways to validate Descope tokens, see our [OIDC JWT authorizers documentation](/sessions/validation/jwt-authorizers). #### Example: Validating Scopes in an API Request (FastAPI) This example shows how you would typically enforce OAuth scopes in a FastAPI application using Descope JWTs. ```python from fastapi import FastAPI, Depends, HTTPException, Header from descope import DescopeClient from descope.exceptions import DescopeException # Initialize Descope client DESCOPE_PROJECT_ID = "__ProjectID__" descope_client = DescopeClient(project_id=DESCOPE_PROJECT_ID) app = FastAPI() # Function to validate session and extract scopes def get_token_scopes(authorization: str = Header(None)): if not authorization or not authorization.startswith("Bearer "): raise HTTPException(status_code=401, detail="Missing or invalid Authorization header") token = authorization.split("Bearer ")[1] try: # Validate the session with Descope session_data = descope_client.validate_session(token) return session_data.get("claims", {}).get("scope", "").split() except DescopeException: raise HTTPException(status_code=401, detail="Invalid or expired token") # Function to enforce required scopes def require_scope(required_scope: str): def check_scope(token_scopes: list = Depends(get_token_scopes)): if required_scope not in token_scopes: raise HTTPException(status_code=403, detail="Insufficient permissions") return check_scope # Protected API endpoint @app.get("/contacts", dependencies=[Depends(require_scope("contacts.read"))]) async def get_contacts(): return {"message": "Returning user's contacts"} ``` This ensures that only tokens with the `contacts.read` scope can call the `/contacts` API. #### Example: Conditional Data Filtering Based on Scopes Instead of blocking access entirely (as in scope validation), you can also use fine-grained filtering to adjust the response based on the user's scopes. This is typically used when some data should always be accessible, but specific data requires additional permissions (e.g., private files, admin-only records, sensitive fields). Unlike general scope validation (require_scope), which denies access with a 403 Forbidden error if the required scope is missing, this method still allows access but limits the data returned. In this example, all users can retrieve a list of files, but only those with the `files.read_private` scope can see private files. ```python @app.get("/files") async def list_files(token_scopes: list = Depends(get_token_scopes)): files = [{"id": 1, "name": "Private Doc", "shared": False}] # If the token lacks 'files.read_private', exclude private files if "files.read_private" not in token_scopes: files = [f for f in files if f["shared"]] return files ``` This ensures that users or applications with only `files.read_public` cannot access private data. ### 2. Use Role-Based Access Control (RBAC) with OAuth Scopes Descope enables mapping RBAC roles to OAuth scopes, ensuring that API permissions align with organizational policies. This approach allows applications to enforce fine-grained access control while maintaining role-based governance. Multiple roles can be mapped to a singular scope on an [API Resource](/resources/scopes-and-roles#api-resources) in the Resources dashboard, therefore it's advantageous to use both scopes and roles for comprehensive access control. #### Why Use Both Scopes and Roles? While **scopes** and **roles** both control access, they serve different purposes, and using them together provides a more secure and scalable authorization model. 1. **Scopes define action-based access** - OAuth scopes specify what an application or user can do within an API (e.g., `contacts.read`, `contacts.write`). They are best suited for enforcing API-level access control, particularly for third-party applications that request permissions dynamically. 2. **Roles define user-based access** - Roles represent who the user is within an organization (e.g., `editor`, `admin`, `manager`). They provide a structured way to manage permissions internally, ensuring that only authorized users can access certain features or data. 3. **Using both ensures security and flexibility** - Scopes enforce OAuth-based permissions, while roles help maintain business logic and organizational policies. Relying solely on scopes makes it difficult to manage internal user permissions, while using only roles makes it harder to enforce fine-grained API access, especially for external applications. #### Example: Mapping Roles to Scopes in API Requests When a user authenticates, their access token includes both scopes and roles. For example, a user with the `editor` role may receive the following token payload: ```json { "sub": "user123", "scope": "contacts.read contacts.write", "roles": ["editor"] } ``` #### Enforcing Access Control with Scopes and Roles The necessary steps therefore, that you will need to take to properly protect your APIs with OAuth scopes and roles are the following: 1. **Validate the OAuth scope** - Ensure that the token includes the necessary scope for the requested API action. 2. **Check the user's role** - Confirm that the user has a role that grants access to the resource or functionality. 3. **Apply business logic** - Use the combination of scopes and roles to enforce **least privilege access** while maintaining organizational security policies. By following these best practices, your API will be well-equipped to handle user authentication, enforce secure access, and integrate seamlessly with external services while maintaining compliance with the OAuth industry standards. # Use Cases for Inbound Apps (/identity-federation/inbound-apps/inbound-apps-use-cases) A list of popular use cases for inbound apps, and how they can be implemented. # Use Cases for Inbound Apps with Descope Inbound apps in Descope enable third-party applications, services, and automated systems to securely access APIs and perform actions on behalf of users or organizations. By leveraging OAuth, inbound apps can authenticate using client credentials flow, enforce fine-grained authorization through scopes and roles, and integrate with external systems that require centralized authentication and access control. This document outlines key use cases for inbound apps: ## Managing Multi-Tenant Authentication for SaaS Applications ### Overview For multi-tenant SaaS applications, inbound apps provide tenant-specific authentication and authorization, ensuring: - Each tenant has its own identity and access control policies - Tenant-specific OAuth settings are applied dynamically - Applications can enforce different roles and scopes per tenant ### Example Use Case A SaaS CRM application needs to: 1. Allow companies to integrate their own identity providers 2. Ensure users from different tenants cannot access each other's data 3. Dynamically assign scopes based on tenant policies Using inbound apps: - Each tenant's identity provider is registered as an inbound app - Tenant-specific OAuth policies are enforced via scopes - Access tokens include the tenant ID, ensuring strict data isolation ## OAuth Provider Implementation Patterns ### Building an OAuth Provider for Marketplaces: The GitHub Model #### Overview Marketplaces need to integrate with multiple third-party applications while maintaining secure authentication. GitHub Marketplace exemplifies this pattern, where external applications request access to user repositories through OAuth. #### How It Works 1. Applications register as OAuth clients, defining required scopes 2. Users authorize access through consent screens 3. OAuth tokens are issued with specific permissions 4. The marketplace enforces scope restrictions on all API calls #### Example: Developer Tools Marketplace A developer tools marketplace uses Descope as its OAuth provider: - Third-party tools register as inbound apps - Users grant permission through standardized consent flows - The marketplace enforces uniform security controls across all integrations - Users manage app permissions through a centralized dashboard ### Implementing MCP Server Authorization Inbound apps can be used to implement MCP server authorization, and protect MCP servers from unauthenticated access. To learn more about how to implement MCP server authorization, see the [MCP](/mcp) page. ## Machine-to-Machine Authentication ### Overview A critical use case for inbound apps is providing secure authentication for non-interactive systems like partner services, AI agents, and automation tools. ### Implementation with Client Credentials Flow Learn more about client credentials flow in the [OAuth 2.0 specification](https://tools.ietf.org/html/rfc6749#section-4.4). Unlike user-centric authentication, machine-to-machine scenarios rely on the OAuth client credentials flow: ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -d "grant_type=client_credentials" \ -d "client_id=your-client-id" \ -d "client_secret=your-client-secret" \ -d "scope=api.read api.write" ``` ### Example: Logistics API A logistics company secures its shipment tracking API: - Partner systems authenticate using client credentials - Each partner receives scopes limited to their shipments - AI agents use the same protocol to fetch data for predictions - All access is logged and can be audited centrally ## Authenticating CLI Applications with PKCE #### Overview Inbound apps can also be used to authenticate users from a command-line interface (CLI). In this case, the CLI acts as a public client and typically uses the OAuth 2.0 authentication code flow (w/ PKCE). A CLI tool starts the PKCE flow, opens a browser window for the user to log in, and exchanges the authorization code for an access token and refresh token. The CLI stores the tokens locally and uses the access token when calling backend APIs. #### Example Use Case A developer is building a CLI tool for their service and needs to: 1. Allow end-users to log in using the same Descope flows as the web app. 2. Store an access token and refresh token locally for API calls. 3. Refresh tokens without requiring a client secret. #### Using Inbound Apps: - Create a new [inbound app with non-confidential client](/identity-federation/inbound-apps/creating-inbound-apps#creating-an-app-with-an-api-or-sdk). - The CLI initiates the PKCE flow and obtains tokens without a client secret. - The CLI refreshes tokens successfully, again without a client secret, as expected by the OAuth 2.0 specification for public clients. - When using inbound apps, the client must request the appropriate set of scopes. If your resource server does not rely on scopes, the CLI can simply request the `openid` scope. By default, inbound apps created in the Descope Console are confidential clients, which means a client secret must be supplied when refreshing tokens. This does not work for CLI applications that do not have a client secret. ## Conclusion Inbound apps in Descope provide a flexible, secure way to authenticate third-party applications, AI agents, and machine-to-machine systems across multiple scenarios: - **User-delegated access** for marketplace integrations - **Service-to-service communication** for backend systems - **Multi-tenant isolation** for SaaS applications - **Model access control** for MCP servers By leveraging OAuth's standardized flows and Descope's implementation, organizations can enforce consistent security policies, simplify credential management, and provide seamless integration experiences for both users and automated systems. # Inbound Apps (/identity-federation/inbound-apps) Discover how to configure inbound apps in Descope to streamline user consent, permissions, and integration with third-party platforms. # Inbound Apps Inbound Apps in Descope allow users to sign in to third-party applications using Descope as their identity provider (IdP) via OAuth 2.0. By enabling secure authentication, users retain control over their identity while organizations manage consent, permissions, and API access efficiently. With Inbound Apps, organizations can secure APIs, simplify integrations, and enhance security for external services, including: - AI-powered assistants and chatbots authenticating via OAuth-based tokens - Partner applications managing user sessions while enforcing consent-driven access - Machine-to-machine (M2M) workflows securely exchanging API requests without manual authentication By centralizing authentication and consent management, Descope simplifies external integrations while enhancing security, compliance, and user experience. ## Why Use Inbound Apps? ### Simplify User Consent and Scope Management When a user authenticates via an Inbound App, Descope enforces a consent flow that ensures only the approved data and permissions are granted to third-party applications. Organizations can configure time-based consent, allowing users or themselves to set expiration periods for granted permissions and requiring users to re-consent after a specified duration. **Example:** A freight management platform grants third-party logistics providers access to shipment data but restricts access to financial details based on OAuth scopes. ### Enable AI Agents and Automated Workflows Inbound Apps allow AI-driven assistants to interact securely with APIs while ensuring access is limited to authorized resources. **Example:** An AI-powered document processor analyzes customer forms without storing credentials by using a scoped OAuth token generated via an Inbound App. ### Automate Machine-to-Machine (M2M) Authentication Inbound Apps facilitate secure M2M authentication, ensuring that backend services can communicate without manual login processes. **Example:** A cloud monitoring service requests OAuth tokens from an Inbound App to collect usage analytics without human intervention. ## How Inbound Apps Work 1. A third-party application redirects users to Descope's authorization URL for authentication. 2. The user logs in through Descope, approves the requested OAuth scopes, and grants consent for a specified duration. 3. Descope issues an authorization code and redirects the user back to the application. 4. The application exchanges the authorization code for an access token using Descope's [token endpoint](/api/third-party-apps/token-endpoint). 5. The access token is used to authenticate API requests, ensuring that users only access authorized resources. For a step-by-step guide on implementing Inbound Apps, see [Configuring an Inbound App](/identity-federation/inbound-apps/creating-inbound-apps). ## Key Features of Inbound Apps - **OAuth 2.0 & 2.1** Inbound Apps use OAuth 2.0 and 2.1 to provide secure authentication, supporting modern authentication standards. - **Customizable Consent Flows** Define granular time-based permissions for user data and actions via OAuth scopes, ensuring data privacy and least-privilege access. - **Automated API Access** Use OAuth tokens to authenticate AI agents, M2M workflows, and backend applications, reducing manual authentication overhead. - **Role and Scope Association** Define scopes on [API Resources](/resources) and map them to [Descope RBAC roles](/resources/scopes-and-roles#api-resources) so end users can only consent to permissions that their roles allow. ## Next Steps For detailed implementation guides, refer to the following resources: - [Resources](/resources): API and MCP Server resource definitions - [Creating Inbound Apps](/identity-federation/inbound-apps/creating-inbound-apps) - Learn how to set up an Inbound App in Descope. - [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server) - OAuth `/authorize`, `/token`, `/revoke`, and `/userinfo` routes with [API reference](/api/third-party-apps) - [Using Inbound Apps](/identity-federation/inbound-apps/using-inbound-apps) - Learn how to use Inbound Apps in your application. - [Developing APIs with OAuth and Inbound Apps](/identity-federation/inbound-apps/developing-apis) - Learn how to develop APIs that properly support OAuth scopes and permissions. - [Use Cases for Inbound Apps](/identity-federation/inbound-apps/inbound-apps-use-cases) - Use cases for inbound apps, including multi-tenant authentication, agentic auth, and an OAuth marketplace. To experience Inbound Apps in action, along with a full working sample app protected with Inbound Apps, see the [10x-CRM Sample App](https://10x-crm.app). # Managing with SDKs (/identity-federation/inbound-apps/sdks) Learn how to manage inbound applications using the Descope backend SDKs. # Inbound Apps with SDKs Use the Descope Management SDK to create, update, patch, delete, and load [Inbound Apps](/identity-federation/inbound-apps) (third-party applications in the [Management API](/api/management/third-party-apps/create-third-party-application)), as well as manage secrets and consents. The Management SDK requires a [management key](https://app.descope.com/settings/company/managementkeys). For an overview of inbound app concepts and console setup, see [Creating Inbound Apps](/identity-federation/inbound-apps/creating-inbound-apps). The preferred method of defining scopes for your backend services is to use a [Resource](/resources) and define a [policy](/policies) for access to it. You can still define `permissionsScopes` and `attributesScopes` on the inbound app itself if you prefer. ## Application Management Create, update, load, and delete inbound apps, and manage client secrets. ### Create Inbound Application This operation creates a new inbound application with the provided details. Using the SDK or Management API is one way to register a **non-confidential** inbound app without [Dynamic Client Registration (DCR)](/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr). Create/update field support varies by SDK. Python, Go, and Ruby accept additional options such as `forcePkce` / `force_pkce`, `defaultAudience` / `default_audience`, `customAttributes` / `custom_attributes`, and JWT Bearer settings. The Node.js typed options currently cover the core fields below; use the [Management API](/api/management/third-party-apps/create-third-party-application) for the full request schema. Reference these custom attributes in the app's consent flow as [dynamic values](/flows/dynamic-keys#inbound-app-and-mcp-client-custom-attributes), using `thirdPartyApp.customAttributes.`. ```javascript // Args: // name (String): Name (required) // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL // approvedCallbackUrls (String[]): Optional list of approved callback URLs // permissionsScopes (InboundAppScope[]): Array of permission scopes, can be empty (required) // attributesScopes (InboundAppScope[]): Optional array of attribute scopes const { data: { id, cleartext: secret }, } = await descopeClient.management.inboundApplication.createApplication({ name: 'my new app', description: 'my desc', logo: 'data:image/png;..', approvedCallbackUrls: ['example.com'], permissionsScopes: [ { name: 'read_support', description: 'read for support', values: ['Support'], }, ], attributesScopes: [ { name: 'read_email', description: 'read user email', values: ['email'], }, ], loginPageUrl: 'http://example.com/login', }); ``` ```python # Args: # name (str): Name (required) # login_page_url (str): Login page URL (required in the Python SDK) # description (str): Optional description # logo (str): Optional logo URL # approved_callback_urls (List[str]): Optional list of approved callback URLs # permissions_scopes (List[dict]): Optional permission scopes (name, description, values) # attributes_scopes (List[dict]): Optional attribute scopes (name, description, values) # jwt_bearer_settings (dict): Optional JWT Bearer validation settings # custom_attributes (dict): Optional custom attributes on the app # force_pkce (bool): Optional; require PKCE on the authorization-code flow # default_audience (str): Optional default aud: "projectId", "clientId", or "" (both) try: resp = descope_client.mgmt.third_party_application.create( name='my new app', description='my desc', logo='data:image/png;..', approved_callback_urls=['example.com'], permissions_scopes=[ { 'name': 'read_support', 'description': 'read for support', 'values': ['Support'], }, ], attributes_scopes=[ { 'name': 'read_email', 'description': 'read user email', 'values': ['email'], }, ], login_page_url='http://example.com/login', ) app_id = resp['id'] secret = resp['cleartext'] except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context — use context.Background() if none // req (*descope.ThirdPartyApplicationRequest): // Name, LoginPageURL (typical required fields) // Description, Logo, ApprovedCallbackUrls // PermissionsScopes, ScopeClaimMapping (optional) // JWTBearerSettings, CustomAttributes (optional) // ForcePkce (bool), DefaultAudience ("projectId" | "clientId" | "") ctx := context.Background() req := &descope.ThirdPartyApplicationRequest{ Name: "my new app", Description: "my desc", Logo: "data:image/png;..", LoginPageURL: "http://example.com/login", ApprovedCallbackUrls: []string{"example.com"}, PermissionsScopes: []*descope.ThirdPartyApplicationScope{ { Name: "read_support", Description: "read for support", Values: []string{"Support"}, }, }, // ForcePkce: true, // DefaultAudience: "clientId", } appID, secret, err := descopeClient.Management.ThirdPartyApplication().CreateApplication(ctx, req) if err != nil { // Handle the error } ``` ```java // Args: // name (String): Name of the inbound app (required) // id (String): Optional custom app ID // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL // approvedCallbackUrls (String[]): Optional list of approved callback URLs // permissionsScopes (InboundAppScope[]): Optional array of permission scopes // attributesScopes (InboundAppScope[]): Optional array of attribute scopes // Additional API fields (session settings, CIBA, audience, etc.) may be available via the Management API try { InboundAppCreateResponse response = inboundAppsService.createApplication( InboundAppRequest.builder() .name("My Inbound App") // .id("optional-custom-id") // .description("optional-description") // .logo("optional-logo") // .loginPageUrl("optional-loginPageUrl") // .approvedCallbackUrls(new String[] { "https://example.com/callback1" }) // .permissionsScopes(new InboundAppScope[] { // InboundAppScope.builder() // .name("scope-name") // .description("optional description") // .build() // }) // .attributesScopes(new InboundAppScope[] { // InboundAppScope.builder() // .name("attribute-scope-name") // .description("optional description") // .build() // }) .build() ); String appId = response.getId(); String secret = response.getSecret(); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # name: Name (required) # id: Optional custom app ID # description: Optional description # logo: Optional logo URL # login_page_url: Optional login page URL # approved_callback_urls: Optional list of approved callback URLs # permissions_scopes: Optional array of permission scopes # attributes_scopes: Optional array of attribute scopes # jwt_bearer_settings: Optional JWT Bearer validation settings # custom_attributes: Optional custom attributes on the app # force_pkce: Optional; require PKCE on the authorization-code flow # default_audience: Optional default aud ("projectId", "clientId", or similar) resp = descope_client.create_application( name: 'my new app', description: 'my desc', logo: 'data:image/png;..', approved_callback_urls: ['example.com'], permissions_scopes: [ { name: 'read_support', description: 'read for support', values: ['Support'], }, ], attributes_scopes: [ { name: 'read_email', description: 'read user email', values: ['email'], }, ], login_page_url: 'http://example.com/login', ) id = resp['id'] secret = resp['cleartext'] ``` ```csharp // Args: // createRequest (CreateThirdPartyApplicationRequest): Application configuration var createRequest = new CreateThirdPartyApplicationRequest { Name = "my new app", Description = "my desc", Logo = "data:image/png;..", LoginPageUrl = "http://example.com/login", ApprovedCallbackUrls = new List { "example.com" }, PermissionsScopes = new List { new() { Name = "read_support", Description = "read for support", Values = new List { "Support" } }, }, AttributesScopes = new List { new() { Name = "read_email", Description = "read user email", Values = new List { "email" } }, }, }; try { var createResponse = await descopeClient.Mgmt.V1.Thirdparty.App.Create.PostAsync(createRequest); var appId = createResponse.Id; var secret = createResponse.Cleartext; } catch (DescopeException ex) { // Handle the error } ``` ### Update Inbound Application This operation updates an existing inbound application, overwriting all fields with the provided values. All provided parameters are used as overrides to the existing application. Empty fields will override populated fields. ```javascript // Args: // id (String): App ID (required) // name (String): Name (required) // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL // approvedCallbackUrls (String[]): Optional list of approved callback URLs // permissionsScopes (InboundAppScope[]): Array of permission scopes, can be empty (required) // attributesScopes (InboundAppScope[]): Optional array of attribute scopes await descopeClient.management.inboundApplication.updateApplication({ id: 'app-id', name: 'my updated app', description: 'my desc', logo: 'data:image/png;..', approvedCallbackUrls: ['example.com'], permissionsScopes: [ { name: 'read_support', description: 'read for support', values: ['Support'], }, ], attributesScopes: [ { name: 'read_email', description: 'read user email', values: ['email'], }, ], loginPageUrl: 'http://example.com/login', }); ``` ```python # Args: # id (str): App ID (required) # name (str): Name (required) # login_page_url (str): Login page URL (required in the Python SDK) # description (str): Optional description # logo (str): Optional logo URL # approved_callback_urls (List[str]): Optional list of approved callback URLs # permissions_scopes (List[dict]): Optional permission scopes # attributes_scopes (List[dict]): Optional attribute scopes # jwt_bearer_settings, custom_attributes, force_pkce, default_audience: same as Create try: descope_client.mgmt.third_party_application.update( id='app-id', name='my updated app', description='my desc', logo='data:image/png;..', approved_callback_urls=['example.com'], permissions_scopes=[ { 'name': 'read_support', 'description': 'read for support', 'values': ['Support'], }, ], attributes_scopes=[ { 'name': 'read_email', 'description': 'read user email', 'values': ['email'], }, ], login_page_url='http://example.com/login', ) except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // req (*descope.ThirdPartyApplicationRequest): Full application configuration (overrides all fields). // Same optional fields as Create (JWTBearerSettings, CustomAttributes, ForcePkce, DefaultAudience, ScopeClaimMapping, etc.) ctx := context.Background() req := &descope.ThirdPartyApplicationRequest{ ID: "app-id", Name: "my updated app", Description: "my desc", Logo: "data:image/png;..", LoginPageURL: "http://example.com/login", ApprovedCallbackUrls: []string{"example.com"}, PermissionsScopes: []*descope.ThirdPartyApplicationScope{ {Name: "read_support", Description: "read for support", Values: []string{"Support"}}, }, } err := descopeClient.Management.ThirdPartyApplication().UpdateApplication(ctx, req) if err != nil { // Handle the error } ``` ```java // Args: // id (String): App ID (required) // name (String): Name (required) // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL // approvedCallbackUrls (String[]): Optional list of approved callback URLs // permissionsScopes (InboundAppScope[]): Array of permission scopes, can be empty (required) // attributesScopes (InboundAppScope[]): Optional array of attribute scopes try { inboundAppsService.updateApplication( InboundAppRequest.builder() .id("app-id") .name("Updated Name") // .description("optional-description") // .logo("optional-logo") // .loginPageUrl("optional-loginPageUrl") // .approvedCallbackUrls(new String[] { "https://example.com/callback1" }) // .permissionsScopes(new InboundAppScope[] { // InboundAppScope.builder().name("scope-name").build() // }) // .attributesScopes(new InboundAppScope[] { // InboundAppScope.builder().name("attribute-scope-name").build() // }) .build() ); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # id: App ID (required) # name: Name (required) # description: Optional description # logo: Optional logo URL # login_page_url: Optional login page URL # approved_callback_urls: Optional list of approved callback URLs # permissions_scopes: Array of permission scopes, can be empty (required) # attributes_scopes: Optional array of attribute scopes # jwt_bearer_settings, custom_attributes, force_pkce, default_audience: same as Create descope_client.update_application( id: 'app-id', name: 'my updated app', description: 'my desc', logo: 'data:image/png;..', approved_callback_urls: ['example.com'], permissions_scopes: [ { name: 'read_support', description: 'read for support', values: ['Support'] }, ], attributes_scopes: [ { name: 'read_email', description: 'read user email', values: ['email'] }, ], login_page_url: 'http://example.com/login', ) ``` ```csharp // Args: // updateRequest (UpdateThirdPartyApplicationRequest): Full application configuration (overrides all fields) var updateRequest = new UpdateThirdPartyApplicationRequest { Id = "app-id", Name = "my updated app", Description = "my desc", Logo = "data:image/png;..", LoginPageUrl = "http://example.com/login", ApprovedCallbackUrls = new List { "example.com" }, }; try { await descopeClient.Mgmt.V1.Thirdparty.App.Update.PostAsync(updateRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Patch Inbound Application This operation updates only the provided fields of an inbound application. ```javascript // Args: // id (String): App ID (required) // name (String): Optional name // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL // approvedCallbackUrls (String[]): Optional list of approved callback URLs // permissionsScopes (InboundAppScope[]): Optional array of permission scopes // attributesScopes (InboundAppScope[]): Optional array of attribute scopes await descopeClient.management.inboundApplication.patchApplication({ id: 'app-id', description: 'my updated desc', logo: 'data:image/png;..', loginPageUrl: 'http://example.com/login', }); ``` ```python # Args: # id (str): App ID (required) # Additional fields are optional and only provided fields are updated try: descope_client.mgmt.third_party_application.patch( id='app-id', description='my updated desc', logo='data:image/png;..', login_page_url='http://example.com/login', ) except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // req (*descope.ThirdPartyApplicationRequest): App ID plus any fields to update ctx := context.Background() req := &descope.ThirdPartyApplicationRequest{ ID: "app-id", Description: "my updated desc", Logo: "data:image/png;..", LoginPageURL: "http://example.com/login", } err := descopeClient.Management.ThirdPartyApplication().PatchApplication(ctx, req) if err != nil { // Handle the error } ``` ```java // Args: // id (String): App ID (required) // name (String): Optional name // description (String): Optional description // logo (String): Optional logo URL // loginPageUrl (String): Optional login page URL try { inboundAppsService.patchApplication( InboundAppRequest.builder() .id("app-id") // .name("Patched Name") // .description("optional-description") // .logo("optional-logo") // .loginPageUrl("optional-loginPageUrl") .build() ); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # id: App ID (required) # Additional fields are optional and only provided fields are updated descope_client.patch_application( id: 'app-id', description: 'my updated desc', logo: 'data:image/png;..', login_page_url: 'http://example.com/login', ) ``` ```csharp // Args: // patchRequest (PatchThirdPartyApplicationRequest): App ID plus any fields to update var patchRequest = new PatchThirdPartyApplicationRequest { Id = "app-id", Description = "my updated desc", Logo = "data:image/png;..", LoginPageUrl = "http://example.com/login", }; try { await descopeClient.Mgmt.V1.Thirdparty.App.PatchPath.PostAsync(patchRequest); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Inbound Application This operation deletes an inbound application by its ID. This action is irreversible. Use carefully. ```javascript // Args: id (String): App ID await descopeClient.management.inboundApplication.deleteApplication('app-id'); ``` ```python try: descope_client.mgmt.third_party_application.delete(id='app-id') except AuthException as error: # Handle the error print(error) ``` ```go ctx := context.Background() err := descopeClient.Management.ThirdPartyApplication().DeleteApplication(ctx, "app-id") if err != nil { // Handle the error } ``` ```java try { inboundAppsService.deleteApplication("app-id"); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.delete_application('app-id') ``` ```csharp try { await descopeClient.Mgmt.V1.Thirdparty.App.DeletePath.PostAsync( new DeleteThirdPartyApplicationRequest { Id = "app-id" } ); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Inbound Applications (Batch) This operation deletes multiple inbound applications in a single request. This action is irreversible. Use carefully. ```javascript // Args: // appIds (String[]): Array of inbound app IDs to delete await descopeClient.management.inboundApplication.deleteApplicationBatch(['app-id-1', 'app-id-2']); ``` ```python # Args: # ids (List[str]): Array of inbound app IDs to delete try: descope_client.mgmt.third_party_application.delete_batch(ids=['app-id-1', 'app-id-2']) except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // appIDs ([]string): Array of inbound app IDs to delete ctx := context.Background() err := descopeClient.Management.ThirdPartyApplication().DeleteApplicationBatch(ctx, []string{"app-id-1", "app-id-2"}) if err != nil { // Handle the error } ``` ### Load Inbound Application This operation loads a specific inbound application by its ID. ```javascript // Args: id (String): App ID const { data: app } = await descopeClient.management.inboundApplication.loadApplication('app-id'); ``` ```python try: resp = descope_client.mgmt.third_party_application.load(id='app-id') app = resp['app'] except AuthException as error: # Handle the error print(error) ``` ```go ctx := context.Background() app, err := descopeClient.Management.ThirdPartyApplication().LoadApplication(ctx, "app-id") if err != nil { // Handle the error } ``` ```java try { InboundApp app = inboundAppsService.loadApplication("app-id"); } catch (DescopeException de) { // Handle the error } ``` ```ruby resp = descope_client.load_application('app-id') app = resp['app'] ``` ```csharp try { var loadResponse = await descopeClient.Mgmt.V1.Thirdparty.App.Load.GetWithIdAsync("app-id"); } catch (DescopeException ex) { // Handle the error } ``` ### Load Inbound Application by Client ID This operation loads a specific inbound application by its client ID. ```java try { InboundApp app = inboundAppsService.loadApplicationByClientId("client-id"); } catch (DescopeException de) { // Handle the error } ``` ```csharp try { var loadResponse = await descopeClient.Mgmt.V1.Thirdparty.App.Load.GetWithClientIdAsync("client-id"); } catch (DescopeException ex) { // Handle the error } ``` ### Get Inbound Application Secret This operation retrieves the application's secret by its ID. ```javascript // Args: id (String): App ID const { data: { cleartext: secret }, } = await descopeClient.management.inboundApplication.getApplicationSecret('app-id'); ``` ```python try: resp = descope_client.mgmt.third_party_application.get_secret(id='app-id') secret = resp['cleartext'] except AuthException as error: # Handle the error print(error) ``` ```go ctx := context.Background() secret, err := descopeClient.Management.ThirdPartyApplication().GetApplicationSecret(ctx, "app-id") if err != nil { // Handle the error } ``` ```java try { String secret = inboundAppsService.getApplicationSecret("app-id"); } catch (DescopeException de) { // Handle the error } ``` ```ruby resp = descope_client.get_application_secret('app-id') secret = resp['cleartext'] ``` ```csharp try { var secretResponse = await descopeClient.Mgmt.V1.Thirdparty.App.Secret.GetAsync(config => { config.QueryParameters.Id = "app-id"; }); var secret = secretResponse.Cleartext; } catch (DescopeException ex) { // Handle the error } ``` ### Rotate Inbound Application Secret This operation rotates the application's secret and returns the new secret. ```javascript // Args: id (String): App ID const { data: { cleartext: newSecret }, } = await descopeClient.management.inboundApplication.rotateApplicationSecret('app-id'); ``` ```python try: resp = descope_client.mgmt.third_party_application.rotate_secret(id='app-id') new_secret = resp['cleartext'] except AuthException as error: # Handle the error print(error) ``` ```go ctx := context.Background() newSecret, err := descopeClient.Management.ThirdPartyApplication().RotateApplicationSecret(ctx, "app-id") if err != nil { // Handle the error } ``` ```java try { String newSecret = inboundAppsService.rotateApplicationSecret("app-id"); } catch (DescopeException de) { // Handle the error } ``` ```ruby resp = descope_client.rotate_application_secret('app-id') new_secret = resp['cleartext'] ``` ```csharp try { var rotateResponse = await descopeClient.Mgmt.V1.Thirdparty.App.Rotate.PostAsync( new RotateThirdPartyApplicationSecretRequest { Id = "app-id" } ); var newSecret = rotateResponse.Cleartext; } catch (DescopeException ex) { // Handle the error } ``` ### Load All Inbound Applications This operation loads all inbound applications in the project. ```javascript const { data: apps } = await descopeClient.management.inboundApplication.loadAllApplications(); apps.forEach((app) => { // Do something }); ``` ```python try: resp = descope_client.mgmt.third_party_application.load_all() apps = resp['apps'] for app in apps: # Do something pass except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // options (*descope.ThirdPartyApplicationSearchOptions): Optional pagination (Page, Limit) ctx := context.Background() apps, total, err := descopeClient.Management.ThirdPartyApplication().LoadAllApplications(ctx, &descope.ThirdPartyApplicationSearchOptions{ Page: 0, Limit: 100, }) if err != nil { // Handle the error } else { for _, app := range apps { // Do something } } ``` ```java try { InboundApp[] apps = inboundAppsService.loadAllApplications(); for (InboundApp app : apps) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ```ruby resp = descope_client.load_all_applications apps = resp['apps'] apps.each do |app| # Do something end ``` ```csharp try { var loadAllResponse = await descopeClient.Mgmt.V1.Thirdparty.Apps.Load.GetAsync(config => { config.QueryParameters.Page = 0; config.QueryParameters.Limit = 100; }); foreach (var app in loadAllResponse.Apps) { // Do something } } catch (DescopeException ex) { // Handle the error } ``` ## Consent Management Search and revoke user or tenant consents granted to inbound apps. For the Console consent view, see [Managing User Consent](/identity-federation/inbound-apps/creating-inbound-apps#managing-user-consent). ### Delete User Consents for Inbound App This operation deletes user consents for an inbound app. At least one identifying field must be set. ```javascript // Args: // consentIds (String[]): Optional array of consent IDs to delete // appId (String): Optional inbound app ID // userIds (String[]): Optional array of user IDs // At least one of consentIds, appId, or userIds must be provided await descopeClient.management.inboundApplication.deleteConsents({ userIds: ['user-id-1'], }); ``` ```python # Args: # consent_ids (List[str]): Optional array of consent IDs to delete # app_id (str): Optional inbound app ID # user_ids (List[str]): Optional array of user IDs # tenant_id (str): Optional tenant ID # At least one identifying field must be provided try: descope_client.mgmt.third_party_application.delete_consents(user_ids=['user-id-1']) except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // options (*descope.ThirdPartyApplicationConsentDeleteOptions): At least one of ConsentIDs, AppID, or UserIDs must be set ctx := context.Background() err := descopeClient.Management.ThirdPartyApplication().DeleteConsents(ctx, &descope.ThirdPartyApplicationConsentDeleteOptions{ UserIDs: []string{"user-id-1"}, }) if err != nil { // Handle the error } ``` ```java // Args: // consentIds (String[]): Optional array of consent IDs to delete // appId (String): Optional inbound app ID // userIds (String[]): Optional array of user IDs // tenantId (String): Optional tenant ID // At least one identifying field must be provided try { inboundAppsService.deleteConsents( InboundAppConsentDeleteOptions.builder() .userIds(new String[] { "user-id-1" }) // .consentIds(new String[] { "consent-id-1", "consent-id-2" }) // .appId("app-id") // .tenantId("tenant-id") .build() ); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Args: # consent_ids: Optional array of consent IDs to delete # app_id: Optional inbound app ID # user_ids: Optional array of user IDs # tenant_id: Optional tenant ID # At least one identifying field must be provided descope_client.delete_consents(app_id: 'app-id', user_ids: ['user-id-1']) ``` ```csharp // Args: // ConsentIds (List): Optional array of consent IDs to delete // AppId (String): Optional inbound app ID // UserIds (List): Optional array of user IDs // At least one identifying field must be provided try { await descopeClient.Mgmt.V1.Thirdparty.Consents.DeletePath.PostAsync( new DeleteThirdPartyApplicationConsentsRequest { UserIds = new List { "user-id-1" }, } ); } catch (DescopeException ex) { // Handle the error } ``` ### Delete Tenant Consents for Inbound App This operation deletes tenant consents for an inbound app. At least one identifying field must be set. ```javascript // Args: // consentIds (String[]): Optional array of tenant consent IDs to delete // appId (String): Optional inbound app ID // tenantId (String): Optional tenant ID // At least one of consentIds, appId, or tenantId must be provided await descopeClient.management.inboundApplication.deleteTenantConsents({ tenantId: 'tenant-id', }); ``` ```python # Args: # tenant_id (str): Tenant ID # Note: The current Python SDK accepts only tenant_id for this operation. try: descope_client.mgmt.third_party_application.delete_tenant_consents(tenant_id='tenant-id') except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // options (*descope.ThirdPartyApplicationTenantConsentDeleteOptions): At least one of ConsentIDs, AppID, or TenantID must be set ctx := context.Background() err := descopeClient.Management.ThirdPartyApplication().DeleteTenantConsents(ctx, &descope.ThirdPartyApplicationTenantConsentDeleteOptions{ TenantID: "tenant-id", }) if err != nil { // Handle the error } ``` ```java // Args: // consentIds (String[]): Optional array of tenant consent IDs to delete // appId (String): Optional inbound app ID // tenantId (String): Optional tenant ID // At least one identifying field must be provided try { inboundAppsService.deleteTenantConsents( InboundAppTenantConsentDeleteOptions.builder() .tenantId("tenant-id") // .consentIds(new String[] { "consent-id-1", "consent-id-2" }) // .appId("app-id") .build() ); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.delete_tenant_consents(tenant_id: 'tenant-id') ``` ```csharp // Args: // ConsentIds (List): Optional array of tenant consent IDs to delete // AppId (String): Optional inbound app ID // TenantId (String): Optional tenant ID // At least one identifying field must be provided try { await descopeClient.Mgmt.V1.Thirdparty.Consents.Delete.Tenant.PostAsync( new DeleteThirdPartyApplicationTenantConsentsRequest { TenantId = "tenant-id", } ); } catch (DescopeException ex) { // Handle the error } ``` ### Search Consents for Inbound Apps This operation searches consents for inbound apps. All fields are optional and can be used to filter results. The Ruby SDK does not currently support searching consents. Use the [Search consents Management API](/api/management/third-party-apps/search-third-party-application-consents) instead. ```javascript // Args: // appId (String): Optional inbound app ID to filter by // userId (String): Optional user ID to filter by // consentId (String): Optional consent ID to filter by // page (Number): Optional page number for pagination const { data: consents } = await descopeClient.management.inboundApplication.searchConsents({ appId: 'app-id', page: 2, }); ``` ```python # Args: # app_id (str): Optional inbound app ID to filter by # user_id (str): Optional user ID to filter by # consent_id (str): Optional consent ID to filter by # tenant_id (str): Optional tenant ID to filter by # page (int): Optional page number for pagination try: resp = descope_client.mgmt.third_party_application.search_consents( app_id='app-id', page=2, ) consents = resp['consents'] except AuthException as error: # Handle the error print(error) ``` ```go // Args: // ctx: context.Context // options (*descope.ThirdPartyApplicationConsentSearchOptions): Optional filters (AppID, UserID, ConsentID, Page) ctx := context.Background() consents, total, err := descopeClient.Management.ThirdPartyApplication().SearchConsents(ctx, &descope.ThirdPartyApplicationConsentSearchOptions{ AppID: "app-id", Page: 2, }) if err != nil { // Handle the error } ``` ```java // Args: // appId (String): Optional inbound app ID to filter by // userId (String): Optional user ID to filter by // consentId (String): Optional consent ID to filter by // tenantId (String): Optional tenant ID to filter by // page (String): Optional page token for pagination try { InboundAppConsentSearchResponse res = inboundAppsService.searchConsents( InboundAppConsentSearchOptions.builder() .appId("app-id") .page("page-token") // .userId("user-id") // .consentId("consent-id") // .tenantId("tenant-id") .build() ); // res.getConsents(), res.getTotal() } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // AppId (String): Optional inbound app ID to filter by // UserId (String): Optional user ID to filter by // ConsentId (String): Optional consent ID to filter by // Page (int): Optional page number for pagination try { var searchResponse = await descopeClient.Mgmt.V1.Thirdparty.Consents.Search.PostAsync( new SearchThirdPartyApplicationConsentsRequest { AppId = "app-id", Page = 2, } ); } catch (DescopeException ex) { // Handle the error } ``` # Using Inbound Apps (/identity-federation/inbound-apps/using-inbound-apps) Learn how to integrate inbound apps with Descope to streamline OAuth authentication, manage user consent, and securely connect third-party applications. # Using Inbound Apps After configuring an **Inbound App** in Descope, you can integrate it into third-party applications, OAuth clients (like NextAuth), and your backend services. All flows use Descope's shared [authorization server endpoints](/identity-federation/inbound-apps/authorization-server) (`/oauth2/v1/apps/authorize`, `/oauth2/v1/apps/token`, and related routes). See the [API reference](/api/third-party-apps) for request and response schemas. Inbound Apps support three main OAuth flows: - **Authorization Code Flow** — Users log in interactively and grant consent. - **Client Credentials Flow** — Services obtain tokens without user interaction. - **Device Authorization Flow** — Obtain tokens for devices from an external device. For a complete working implementation with both **Authorization Code** and **Client Credentials** flows, refer to the [Descope 3rd-Party Sample App](https://github.com/descope-sample-apps/3rd-party-sample-app). ## Authorization Code Flow This flow is recommended when a user is present, such as in web apps, mobile apps, or third-party integrations that require explicit user consent and the running of a [flow](/flows). ### How It Works - Your app redirects the user to Descope's [`/authorize` endpoint](/api/third-party-apps/authorization-get). - The user signs in and approves the requested scopes. - Descope redirects back to your `redirect_uri` with an authorization code. - Your app exchanges that code for tokens using the [`/token` endpoint](/api/third-party-apps/token-endpoint). #### Scopes When including `scopes` in your `/authorize` request, the scopes you include must be pre-defined in the [Inbound App configuration](/identity-federation/inbound-apps/creating-inbound-apps#scopes). Here is an example of how to include scopes in your `/authorize` request: ```bash __BaseURL__/oauth2/v1/apps/authorize? client_id= &redirect_uri=https://yourapp.com/callback &response_type=code &scope=full_access &state= ``` In this example, the scope `full_access` will be included in the consent screen, and the `scopes` claim of your OAuth token. ![consent-screen-with-scopes](/assets/consent-screen-with-scopes.webp) ### 1. Initiating Login with Descope This URL should be automatically constructed if you're using any standard OAuth Client SDK or library. The third-party application redirects users to Descope's `/authorize` endpoint, prompting them to authenticate and approve the requested permissions. ```bash __BaseURL__/oauth2/v1/apps/authorize? client_id= &redirect_uri=https://yourapp.com/callback &response_type=code &scope=openid email profile &state= ``` This is the general process that will occur when a user initates login: - The user logs in via the Descope Consent Flow. - The user approves the requested scopes on the **consent screen**. - Descope redirects the user back to the callback URL with an authorization code. When using Inbound Apps, if you do not provide user consent to the client requested scopes, the flow will return an error and an OAuth token will not be issued. #### Example: NextAuth Application Acting As OAuth Client Here is an example of how you can configure [Auth.js](https://authjs.dev/) to act as an OAuth client for Descope, instead of manually constructing the `/authorize` request yourself: ```ts import NextAuth from "next-auth"; import { OIDCConfig } from "next-auth/providers"; export const baseUrl = "__BaseURL__"; export const clientId = process.env.CLIENT_ID ?? ""; export const clientSecret = process.env.CLIENT_SECRET ?? ""; const DescopeOAuthApps = (): OIDCConfig => ({ id: "customapp", name: "Custom App", type: "oidc", authorization: { params: { scope: "openid email profile", prompt: "consent" } }, client: { token_endpoint_auth_method: "client_secret_post" }, checks: ["pkce", "state"], }); export const { signIn, signOut, auth } = NextAuth({ providers: [ { ...DescopeOAuthApps(), clientId, clientSecret, }, ], }); ``` ### 2. Handling the OAuth Callback After successful authentication and consent, Descope redirects the user to the `redirect_uri`, appending an authorization code. Example response to the callback URL: ```bash https://yourapp.com/callback?code=&state= ``` Once the application receives the authorization code, it must be exchanged for an access token by making a `POST` request to the Descope `/token` endpoint. ### 3. Exchanging Authorization Code for an Access Token When you initially grant, modify, or revoke consent in some way, this will be reflected in the [audit trail](/audit-trails-and-integrations/audit-events). However, additional authorization code exchanges that don't involve consent changes will not be reflected in the audit trail. There are two ways to exchange the authorization code for an access token: #### Using a Client Secret (Confidential Clients - Backend Apps) Recommended for server-side apps that can securely store secrets. ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=" \ -d "client_secret=" \ -d "code=" \ -d "redirect_uri=https://yourapp.com/callback" ``` #### Using PKCE (Public Clients - SPAs, Mobile Apps) Recommended for apps that cannot securely store secrets. ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "client_id=" \ -d "code=" \ -d "redirect_uri=https://yourapp.com/callback" \ -d "code_verifier=" ``` ### 4. Using the Token with an API If successful, Descope responds with an **access token**, as well as an **id token** and **refresh token**: ```json { "access_token": "eyJhbGciOiJIUz...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "abc123", "id_token": "eyJhbGciOiJIUz..." } ``` With this response, three types of tokens will be returned: - **`access_token`** → Used to authenticate API requests. - **`id_token`** → Contains user identity claims (for OpenID Connect). - **`refresh_token`** → Used to obtain a new access token when it expires. Once authenticated, the **access token** is used to authorize API calls. ```bash curl -X GET "https://api.yourservice.com/user/profile" \ -H "Authorization: Bearer " ``` To validate the access token with Descope on every request and load **current** user claims, attributes, and roles (not only what is embedded in the JWT), use the Inbound App [UserInfo endpoint](/sessions/introspection#inbound-app-userinfo). If the user revokes consent or if the user consent expires, once the access token expires, it will no longer be able to be refreshed. It is therefore recommended to keep your [session expiry window](/management/project-settings#session-token-timeout) short. ### 5. Refreshing Access Tokens Inbound App token refresh will not be reflected in the [audit trail](/audit-trails-and-integrations/audit-events). Access tokens have a limited lifetime, and will expire after a certain period of time. To refresh an access token, use the `/token` endpoint with the `refresh_token` grant type. #### Using a Client Secret (Confidential Clients - Backend Apps) If the app was created as a confidential client, you must provide the client secret to refresh the token. ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=" \ -d "client_secret=" \ -d "refresh_token=" ``` #### Using PKCE (Public Clients - SPAs, Mobile Apps) In order to refresh a token as a public client, the app must have been created as a [non-confidential client](/identity-federation/inbound-apps/creating-inbound-apps#types-of-inbound-apps). ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "client_id=" \ -d "refresh_token=" ``` When using inbound apps, confidential clients can refresh access tokens using their client secret, while non-confidential clients (such as those created via DCR) do not require a client secret for token refresh. ## Client Credentials Flow Client credentials exchanges for inbound app tokens will not be reflected in the [audit trail](/audit-trails-and-integrations/audit-events). In **Inbound Apps**, the **client credentials grant** lets a backend service obtain a user's JWT without requiring user interaction. Instead of creating a brand-new token, the service can request one directly using its own credentials. This approach is especially useful in machine-to-machine (M2M) scenarios, where services need to act on behalf of a user or system account without any manual sign-in. ### How It Works in Inbound Apps 1. The backend service authenticates itself with the `/token` endpoint using its client ID and client secret. 2. Descope verifies the credentials and issues a user JWT tied to the specified client ID (service account). #### Example: Fetching a Token Using Client Credentials ```bash curl -X POST \ https:///oauth2/v1/apps/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=" \ -d "client_secret=" \ -d "scope=openid email profile" ``` #### Example: Next.js Backend Route Using Client Credentials ```ts import { auth, baseUrl, clientId, clientSecret } from "@/auth"; import { NextResponse } from "next/server"; export const GET = auth(async function GET(req) { if (req.auth) { const body = "grant_type=client_credentials" + "&client_id=" + clientId + "&client_secret=" + clientSecret + "&scope=openid email profile"; const res = await fetch(`${baseUrl}/oauth2/v1/apps/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, }); return NextResponse.json(await res.json(), { status: res.status }); } return NextResponse.json({ message: "Not authenticated" }, { status: 401 }); }); ``` ## Getting Outbound App Tokens via Inbound Apps This currently only works for Inbound App access tokens that are associated with a user (i.e. created with Authorization Code flow). Inbound app tokens created with the Client Credentials flow cannot be used to retrieve Outbound App tokens. Once the user is authenticated and authorized via an Inbound App, you can use the access token to **retrieve an Outbound App token** for use with third-party services or internal tools. The inbound app access token will need to contain the `outbound.token.fetch` scope in order to be able to exchange the token for outbound app tokens. To learn how to use an Inbound App access token to retrieve Outbound App tokens, see [Using Outbound Apps](/identity-federation/outbound-apps/using-outbound-apps). This allows your inbound application to act as a bridge between an authenticated user identity and the tools your MCP server needs to access. ## External Token Management This feature is currently in **beta**. Please contact [Descope Support](/support) to request access to this feature. Once enabled, a new **External Token Management** section will appear inside each Inbound App setting page, allowing you to register external identity providers as trusted issuers. Inbound Apps optionally support the **JWT Bearer Grant Type**, which enables your application to authenticate or exchange an external OIDC token (such as a token issued by another IdP) for a new Descope-issued access token. When enabled, this allows Descope to validate an external JWT, authenticate the user, and issue an equivalent token through your Inbound App — enabling smooth token handoff and interoperability between identity systems. ### How It Works Once configured, your Inbound App can accept an external OIDC token and exchange it using the `/token` endpoint with the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type. #### General Flow 1. Your external system issues an OIDC-compliant JWT. 2. Your backend sends that JWT to Descope's Inbound App `/token` endpoint. 3. Descope validates the JWT against the configured External Token Issuer. 4. If the token is trusted and valid, Descope returns a new OAuth token set (access, ID, refresh). ### Configuring External Token Validation To configure external token validation for JWT Bearer authentication: In the Descope Console, under your Inbound App, you will see a new **External Token Management** section (after Descope Support enables this feature). Here you can register one or more **trusted issuers**. ![External Token Management](/assets/add-inbound-app-jwt-bearer.webp) For each issuer, you must provide: | Field | Required | Description | | --------------------------------------- | ----------- | ------------------------------------------------------------------------------------- | | **Issuer URL** | Required | The expected `iss` value in incoming tokens. | | **JWKs URL** | Required | URL hosting the JSON Web Key Set used to validate signatures. | | **Sign Algorithm** | Optional | Restrict acceptable algorithms (e.g., `RS256`). If omitted, defaults to JWK metadata. | | **User Information Endpoint URL** | Optional | Used for additional user info retrieval (similar to OIDC UserInfo). | | **User Information LoginID Field Name** | Optional | Field from the UserInfo response to use as the Descope Login ID. | Once saved, Descope will accept JWTs from the configured issuer. ### Using the JWT Bearer Grant Type Once an issuer is configured, clients can authenticate or exchange external tokens using: ``` grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer ``` #### Use Case: Partner Integration with External Identity Providers A common scenario for JWT Bearer grant type is when you need to integrate with partner organizations or third-party services that have their own identity providers. For example: **Scenario**: Your SaaS platform needs to allow users from partner companies (e.g., Enterprise customers with their own Okta or Azure AD) to access your services. Instead of requiring these users to create separate accounts or go through a full OAuth redirect flow, you can: 1. The partner's IdP authenticates their user and issues a JWT token. 2. Your backend service receives this external JWT (e.g., from a partner API call or a trusted service). 3. Your service exchanges the partner's JWT for a Descope token using the JWT Bearer grant type. 4. The user can now access your platform using the Descope token, while maintaining their identity from the partner's system. This approach is particularly useful for: - **B2B integrations** where partner organizations manage their own identities - **Microservices architectures** where different services use different IdPs and need token interoperability - **Legacy system migrations** where you need to support both old and new identity systems during transition - **Federated access** scenarios where you want to accept tokens from trusted external systems without requiring users to authenticate separately #### Example: Exchanging an External Token for a Descope Token ```bash curl -X POST "__BaseURL__/oauth2/v1/apps/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ -d "client_id=" \ -d "assertion=" ``` If the assertion is valid and signed by a configured issuer, Descope will issue a new OAuth token response: ```json { "access_token": "eyJhbGciOiJIUz...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "abc123", "id_token": "eyJhbGciOiJIUz..." } ``` #### Notes * The external JWT must be **unexpired** and signed using keys obtained from the JWKs URL. * Scopes can optionally be included if your inbound app defines supported scopes. * This mechanism can replace or complement traditional client credential or authorization-code flows depending on your architecture. # Connecting Outbound Apps (/identity-federation/outbound-apps/connect) Learn how to connect users to third-party services with Descope Outbound Apps using JavaScript SDKs or Flow Actions. # Connect to Outbound Apps Once you've [created an Outbound App](/identity-federation/outbound-apps#creating-an-outbound-app), you can start connecting your users to third-party services through Descope Outbound Apps. You can choose between: 1. **Using Frontend SDKs** - For custom UI integrations in your application 2. **Using Descope Flows** - For low-code integration with pre-built UI components 3. **Using APIs** - For non-JavaScript platforms ## Connecting to Outbound Apps Descope Outbound Apps support two main connection types: ### 1. OAuth Provider Connection - Use the SDKs, Flows, or API to initiate the OAuth flow and connect a user to a third-party provider. - Use the **Outbound App / Connect** (user-level) or **Outbound App / Tenant Connect** (tenant-level) action in your flow, or call the corresponding API. - Descope manages the consent, token storage, and refresh. ### 2. API Key Connection - For services that use static API keys, you can collect and store a key for each user or tenant. - Use the **Outbound App / Connect API key** (user-level) or **Outbound App / Connect tenant API key** (tenant-level) action in your flow, or call the corresponding API. - The process is similar to connecting an OAuth provider: the user provides their API key, and Descope stores it securely in the token vault. ## Connecting with Frontend SDKs Install the appropriate Descope SDK for your application: ```bash # For vanilla JavaScript applications npm install @descope/web-js-sdk # For React applications npm install @descope/react-sdk # For Next.js applications npm install @descope/nextjs-sdk ``` ### Using the `connect()` Function You must be logged in to use the `connect()` function. If you signed in with a client SDK, the JS SDKs will automatically rely on the same session already created. Otherwise, you can use the optional `token` parameter to pass in a valid authentication token. The `outbound.connect()` function initiates a redirect to the authorization endpoint of your Outbound App provider, handling all OAuth redirect mechanics for you. #### Parameters - **providerId** (required): The ID of the Outbound App you've configured in Descope - **options** (optional): Configuration for the connection: - **redirectURL**: Custom URL to redirect after authentication - **scopes**: Array of specific scopes to request - **token** (optional): Authentication token (if not already set in the SDK) #### Code Examples ```jsx import { useDescope } from '@descope/react-sdk'; function ConnectButton() { const { sdk } = useDescope(); const handleConnect = async () => { try { await sdk.outbound.connect('', { redirectURL: 'https://app.example.com/post-connection', }); } catch (error) { console.error('Error connecting to Google:', error); } }; return ( ); } export default ConnectButton; ``` ```jsx 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; export default function ConnectButton() { const { sdk } = useDescope(); const handleConnect = async () => { try { await sdk.outbound.connect( 'google', { // You can specify a relative path in Next.js redirectURL: '/auth/callback' } ); } catch (error) { console.error('Error connecting to Google:', error); } }; return ( ); } ``` ```javascript import { DescopeSdk } from '@descope/web-js-sdk'; const sdk = DescopeSdk({ projectId: '__ProjectID__' }); async function connectToGoogle() { try { const resp = await sdk.outbound.connect('google-calendar'); // The response contains the authorization URL, but the SDK // will automatically redirect the browser console.log('Redirecting to:', resp.data.url); } catch (error) { console.error('Error connecting to Google:', error); } } // Call this function when your user clicks a "Connect to Google" button connectToGoogle(); ``` ### Customizing the Connection You can provide additional options to the `connect` function to customize the authorization request: ```javascript // Works with any of the SDKs await sdk.outbound.connect( 'google-calendar', { // Override the redirect URL (must be registered in your Outbound App) redirectURL: 'https://your-app.com/post-connection', // Specify particular scopes (overrides the default scopes) scopes: [ 'https://www.googleapis.com/auth/calendar', 'https://www.googleapis.com/auth/contacts.readonly' ] } ); ``` If you don't specify the `scopes` parameter, the connection will use the default scopes you configured in the Descope Console for this outbound app. This makes it easy to maintain consistent scope permissions across your application without hardcoding them in your frontend code. ### Handling the Redirect After successful authentication, the provider redirects back to your application. Descope will automatically handle the redirect back to where you set your redirect URL shown above. At this point, Descope will now be managing the tokens for this user. You can use the [Management API](/identity-federation/outbound-apps/using-outbound-apps#fetching-outbound-apps-tokens) to retrieve the tokens for backend operations. ## Connecting with Descope Flows For a low-code approach with pre-built UI components, you can use Descope Flows to manage the connection process. Descope Flows provide: - Pre-built UI components for connection buttons - Managed redirect handling - Easy customization options ### Flow Components for Outbound Apps There are two main components: 1. **Outbound App Button** - A button component that initiates the connection 2. **Outbound App Connect Action** - The action that processes the connection request ![An example of configuring the outbound app button within Descope flows](/assets/outbound-app-flow-button.webp) ### Implementation Options If you're using the Descope Flows, you must first sign in using the flow, before you can connect to an outbound app. The previous authenticated session will be used to establish a connection and associate the outbound app tokens with the respective user. You can implement outbound app connections in flows in two ways: #### Option 1: Using the Outbound App Button Place an Outbound App button on your flow screen that users can click to initiate the connection. The button can be customized with text and styling options, and it automatically displays the outbound app's logo. #### Option 2: Using the Connect Action Directly If you want to use your own UI elements, you can trigger the `Outbound App Connect` action directly from your application. You can set a default outbound app in the action's configuration to bypass the need for Descope's UI components. In addition to this, you can also configure custom scopes, that will override the default scopes configured in your [Outbound App Settings](/identity-federation/outbound-apps/creating-outbound-apps#scopes). ## Connecting with APIs If you're using the APIs, you must first sign in and create a session for the user. You will then use the refresh token of the user to initiate the connection. For non-JavaScript platforms, you can use this API directly to initiate connections: ```http POST __BaseURL__/v1/outbound/oauth/connect Authorization: Bearer __ProjectID__: Content-Type: application/json { "appId": "", "options": { "redirectUrl": "https://your-redirect-url.com", "scopes": ["scope1", "scope2"] // Optional } } ``` https://api.descope.com might be different for your project depending on your region or base URL. Read more [here](/how-to-deploy-to-production/custom-domain). The `redirectUrl` parameter in the options object is required and specifies where the user will be redirected after the authorization process completes. The `scopes` parameter is optional and allows you to request specific permissions different from the default scopes configured in your Outbound App. After calling this endpoint, the server will respond with a redirect URL that you should redirect your user to in order to begin the OAuth consent flow. ## Viewing and Managing Tokens in the Console After connecting users to an outbound app, you can view and manage their tokens directly in the Descope Console under the **Token Management** tab for your outbound app. ![Token Management dashboard in the Descope Console](/assets/outbound-app-dashboard.webp) For each user or tenant-level token, you can: - **View the access token** (and refresh token, if applicable) - **Manually refresh the access token** - **Delete the token** This provides a convenient way to audit, troubleshoot, or revoke access for specific users or tenants without writing any code. ## Deleting Outbound App Tokens You can delete outbound app tokens in two ways: ### 1. Using the Descope Console Navigate to the **Token Management** tab for your outbound app in the Descope Console. Find the user or tenant whose token you want to delete, and use the delete option in the dashboard (see image above). ### 2. Using the REST API or SDK Token deletion is supported in the NodeJS, Python, Java, Go, PHP, and Ruby SDKs. See [Managing with SDKs](/identity-federation/outbound-apps/sdks) for installation and client setup. #### Delete a Specific Token by ID ```js await descopeClient.management.outboundApplication.deleteTokenById('token-id-123'); ``` ```python descope_client.mgmt.outbound_application.delete_token(token_id='token-id-123') ``` ```java import com.descope.sdk.mgmt.OutboundAppsService; outboundAppsService.deleteOutboundAppTokenById("token-id-123"); ``` ```go err := descopeClient.Management.OutboundApplication().DeleteTokenByID(ctx, "token-id-123") if err != nil { // Handle error } ``` ```php $descopeSDK->management->outboundApps->deleteTokenById('token-id-123'); ``` ```ruby client.delete_outbound_app_token_by_id(token_id: 'token-id-123') ``` ```bash curl -X DELETE "__BaseURL__/v1/mgmt/outbound/token?id=token-id-123" \ -H "Authorization: Bearer " ``` #### Delete Tokens by App ID and User ID Both `appId` and `userId` are optional, but at least one must be provided. Passing only an app ID deletes the stored tokens for every user connected to that app. ```js await descopeClient.management.outboundApplication.deleteUserTokens('google-contacts', 'user-123'); ``` ```python descope_client.mgmt.outbound_application.delete_user_tokens( app_id='google-contacts', user_id='user-123' ) ``` ```java import com.descope.sdk.mgmt.OutboundAppsService; DeleteOutboundAppUserTokensRequest deleteRequest = new DeleteOutboundAppUserTokensRequest(); deleteRequest.setAppId("google-contacts"); deleteRequest.setUserId("user-123"); outboundAppsService.deleteOutboundAppUserTokens(deleteRequest); ``` ```go err := descopeClient.Management.OutboundApplication().DeleteUserTokens(ctx, "google-contacts", "user-123") if err != nil { // Handle error } ``` ```php $descopeSDK->management->outboundApps->deleteUserTokens('google-contacts', 'user-123'); ``` ```ruby client.delete_outbound_app_user_tokens(app_id: 'google-contacts', user_id: 'user-123') ``` ```bash curl -X DELETE "__BaseURL__/v1/mgmt/outbound/user/tokens?appId=google-contacts&userId=user-123" \ -H "Authorization: Bearer " ``` ## Importing Pre-existing OAuth Tokens These operations are only available with the Rest API at this time. If you already hold valid OAuth tokens for your users or tenants—for example, when migrating from another system—you can import them directly into Descope's token vault without requiring users to re-run the OAuth flow. This is a management-key-only operation intended for backend migrations. Batch endpoints are **all-or-nothing**: if any token fails validation, the entire batch is rejected and no tokens are committed. Fix the reported failures and retry the full batch. ### Import a Single User Token ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/oauthtoken/upload" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "appId": "", "userId": "", "accessToken": "", "refreshToken": "", "accessTokenExpiry": 1700000000, "accessTokenType": "Bearer", "scopes": ["scope1", "scope2"], "externalIdentifier": "", "idToken": "", "grantedBy": "", "verifyRefresh": false }' ``` ### Import a Single Tenant Token ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/oauthtoken/upload" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "appId": "", "tenantId": "", "accessToken": "", "refreshToken": "", "accessTokenExpiry": 1700000000, "accessTokenType": "Bearer", "scopes": ["scope1", "scope2"], "externalIdentifier": "", "idToken": "", "grantedBy": "", "verifyRefresh": false }' ``` ### Batch Import User Tokens ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/oauthtoken/batch/upload" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "tokens": [ { "appId": "", "userId": "", "accessToken": "", "refreshToken": "", "accessTokenExpiry": 1700000000, "accessTokenType": "Bearer", "scopes": ["scope1", "scope2"], "externalIdentifier": "", "idToken": "", "grantedBy": "" } ] }' ``` ### Batch Import Tenant Tokens ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/oauthtoken/batch/upload" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "tokens": [ { "appId": "", "tenantId": "", "accessToken": "", "refreshToken": "", "accessTokenExpiry": 1700000000, "accessTokenType": "Bearer", "scopes": ["scope1", "scope2"], "externalIdentifier": "", "idToken": "", "grantedBy": "" } ] }' ``` For full request/response schemas, see the [API reference](/api/management/outbound-apps/upload-outbound-app-user-oauth-token). ## Next Steps After a user connects to an outbound app, you can: 1. Use the [SDKs or APIs](/identity-federation/outbound-apps/using-outbound-apps#fetching-outbound-apps-tokens) to retrieve tokens for backend operations. 2. Connect to multiple providers for the same user. 3. Create multiple outbound connections for a user [with different scopes](/identity-federation/outbound-apps/using-outbound-apps#scope-management). For working code examples, see our [Outbound Apps Examples](/identity-federation/outbound-apps/examples). # Creating an Outbound App (/identity-federation/outbound-apps/creating-outbound-apps) Learn how to create an Outbound App with Descope, a secure way to connect your users to third-party services. ## Creating an Outbound App To configure an outbound application within Descope, navigate to the [Outbound Apps](https://app.descope.com/apps/outbound) section of the console and select `+ Outbound App`. You can then select a custom connection, or one of our preconfigured applications within the outbound app library. ![Creating an outbound app in Descope](/assets/creating-an-outbound-app.webp) Once you have created your outbound application, you can configure it. The configuration is split into several sections: `Outbound App Details`, `Account Information`, `Additional Settings`, and `Token Management`. This example will walk through configuring `Google Contacts` as an outbound application. ## Custom API Key Apps In addition to connecting to OAuth providers, you can create a **Custom API Key App**. This allows you to store and manage static API keys (or other secrets) for each user or tenant, associated with a specific app. - When creating an Outbound App, select the "Custom API Key" option. - You can then use Descope Flows or APIs to collect and store API keys for each user or tenant. - These keys are securely stored and can be retrieved or rotated as needed. This is ideal for integrating with services that do not support OAuth, or for managing long-lived tokens. ### Outbound App Details Within this section, you can find and configure the information regarding your application below. - **Logo (Optional)**: You can upload a logo for the outbound app by clicking the edit button on the logo. The "Connect To" consent button within your flows for the outbound app will automatically utilize the uploaded logo. - **Outbound Name (Required)**: This is the configurable name of the application. - **Description (Optional)**: This is the chosen description of your application, and it is editable. - **Outbound App ID (Required)**: This is configurable only during the creation of the outbound application. Once the initial configuration is complete, this field will not be editable. ![Configuring an outbound app's details in Descope](/assets/outbound-app-details.webp) ### Account Information Within this section, you will define the connection settings and scopes for your outbound application. #### Connection Settings Here, you will configure the connection to the outbound app provider. This example shows one of the preconfigured applications (Google Contacts). - **Client ID (Required)**: The ID from the provider when creating your authentication account - **Client Secret (Required)**: The secret from the provider when creating your authentication account - **Callback Domain (Optional)**: The domain to use for callbacks. This will default to the configured domain within project settings. - **Callback URL for app registration (Pre-defined)**: To use a custom domain in your OAuth verification screen, configure a custom domain on the Project page. It is important to note that the callback URL for outbound applications differs from the callback URL of OAuth authentication methods and will require you to add the additional URL to your application's callback domains list. ![Configuring an outbound app's connection settings in Descope](/assets/outbound-app-connection-settings.webp) #### Scopes Within this section, you will configure the scopes to associate with the outbound application. These configured scopes must also be configured on your provider's application. Where applicable, within non-custom applications, Descope prepopulates example scopes for the outbound application; however, Descope also links to the provider's scope documentation to allow you to review and refine the scopes requested further. **Example: Google Contacts Application** By default, Google Contacts prompts for full access: - **Scope**: `https://www.googleapis.com/auth/contacts` - **Description**: _See, edit, download, and permanently delete your contacts_ However, you can switch to read-only access by using: - **Scope**: `https://www.googleapis.com/auth/contacts.readonly` This adjustment is shown in the example screenshot below: ![Configuring an outbound app's scopes in Descope](/assets/outbound-app-scopes.webp) In order to use the **Google Contacts** APIs with this scope however, you must also configure the `https://www.googleapis.com/auth/contacts.readonly` scope on your Google application. See the example below: ![Configuring additional scopes on a Google Oauth application to be used with outbound apps in Descope](/assets/outbound-google-contacts-example.webp) ### Additional Settings Within the additional settings section, you can configure more details for how your application behaves on successful consent, authorization, and token endpoint configuration. When using an application from the outbound app library, the authorization and token endpoint are prepopulated. These prepopulated endpoints should work as intended and not need to be altered during the app creation. - **Redirect URL (Optional)**: The default redirect URL after a successful connection. This value will be overridden when using flows or specifying the redirect URL in the API/SDK call. - **Authorization Endpoint (Required)**: The endpoint to request authorization from the user. - **Token Endpoint (Required)**: The endpoint to exchange the authorization code for an access token. ![Configuring an outbound app's additional settings in Descope](/assets/outbound-app-additional-settings.webp) ## Managing Outbound Apps You can create, update, delete, and load outbound applications programmatically using the Descope Node SDK or directly via the REST API. ### Create an Outbound Application ```js // Create an outbound application const { id } = await descopeClient.management.outboundApplication.createApplication({ name: 'my new app', description: 'my desc', // ...other fields (see schema below) }); ``` ```http POST /v1/mgmt/outbound/app/create Authorization: Bearer __ProjectID__: Content-Type: application/json { "name": "my new app", "description": "my desc" // ...other fields (see schema below) } ``` ### Update an Outbound Application ```js // Update an outbound application (overrides all fields) await descopeClient.management.outboundApplication.updateApplication({ id: 'my-app-id', name: 'my updated app', // ...other fields }); ``` ```http POST /v1/mgmt/outbound/app/update Authorization: Bearer __ProjectID__: Content-Type: application/json { "id": "my-app-id", "name": "my updated app" // ...other fields } ``` ### Delete an Outbound Application ```js // Delete an outbound application by id await descopeClient.management.outboundApplication.deleteApplication('my-app-id'); ``` ```http POST /v1/mgmt/outbound/app/delete Authorization: Bearer __ProjectID__: Content-Type: application/json { "id": "my-app-id" } ``` ### Load an Outbound Application ```js // Load an outbound application by id const app = await descopeClient.management.outboundApplication.loadApplication('my-app-id'); ``` ```http GET /v1/mgmt/outbound/app/{id} Authorization: Bearer __ProjectID__: ``` ### Load All Outbound Applications ```js // Load all outbound applications const appsRes = await descopeClient.management.outboundApplication.loadAllApplications(); appsRes.data.forEach((app) => { // do something }); ``` ```http GET /v1/mgmt/outbound/apps Authorization: Bearer __ProjectID__: ``` # Example Tools (/identity-federation/outbound-apps/examples) See how to use Descope Outbound Apps from the backend to fetch access tokens for providers like Salesforce, HubSpot, and Google Calendar. # Example Tools using Outbound Apps Here are examples of how backend services can use **Descope Outbound Apps** to retrieve user-specific tokens for third-party platforms like **Salesforce**, **HubSpot**, and **Google Calendar**. Each example assumes the user has already connected via the outbound app connect flow, either using the [Frontend SDKs](/identity-federation/outbound-apps/connect#connecting-with-frontend-sdks) or [Descope Flows](/identity-federation/outbound-apps/connect#connecting-with-descope-flows). ## Fetching Outbound App Tokens To use tokens from your backend, you'll need to fetch them using the Descope Management API. Below are examples in different languages: ### Node.js / JavaScript ```javascript // Node.js example using fetch API async function getOutboundToken(appId, userId, scopes = []) { const projectId = process.env.DESCOPE_PROJECT_ID; const managementKey = process.env.DESCOPE_MANAGEMENT_KEY; const response = await fetch('__BaseURL__/v1/mgmt/outbound/app/user/token', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${projectId}:${managementKey}` }, body: JSON.stringify({ appId, userId, scopes, options: { withRefreshToken: false, forceRefresh: false } }) }); if (!response.ok) { throw new Error(`Failed to fetch token: ${response.status} ${response.statusText}`); } const data = await response.json(); return data.token; } ``` ### Python ```python import requests def get_outbound_token(app_id, user_id, scopes=None): project_id = "__ProjectID__" management_key = "YOUR_MANAGEMENT_KEY" url = "__BaseURL__/v1/mgmt/outbound/app/user/token" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {project_id}:{management_key}" } payload = { "appId": app_id, "userId": user_id, "scopes": scopes or [], "options": { "withRefreshToken": False, "forceRefresh": False } } response = requests.post(url, headers=headers, json=payload) if response.status_code != 200: raise Exception(f"Failed to fetch token: {response.status_code} {response.text}") data = response.json() return data["token"] ``` ## 🔗 Salesforce Opportunities Tool Use Descope Outbound Apps to fetch Salesforce access tokens, then query opportunities from the Salesforce API. ```javascript // Fetch the token from Descope Management API const token = await getOutboundToken("salesforce", "user-123", ["full"]); const accessToken = token.accessToken; const instanceUrl = process.env.SALESFORCE_INSTANCE_URL; const query = `SELECT Id, Name, StageName, CloseDate FROM Opportunity LIMIT 5`; const endpoint = `${instanceUrl}/services/data/v57.0/query/?q=${encodeURIComponent(query)}`; const response = await fetch(endpoint, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const data = await response.json(); ``` ## 📅 Google Calendar Events Tool Fetch a user's calendar events with Descope-managed tokens: ```javascript // Fetch the token from Descope Management API const token = await getOutboundToken( "google-calendar", "", ["https://www.googleapis.com/auth/calendar"] ); const accessToken = token.accessToken; const now = new Date().toISOString(); const url = `https://www.googleapis.com/calendar/v3/calendars/primary/events?timeMin=${now}&maxResults=5&orderBy=startTime&singleEvents=true`; const response = await fetch(url, { headers: { 'Authorization': `Bearer ${accessToken}` } }); const events = await response.json(); ``` ## 📇 HubSpot Contacts Tool (Python Example) Use the Outbound App token to get CRM contact records from HubSpot: ```python # Fetch the token from Descope Management API token = get_outbound_token("hubspot", "user-123", ["contacts"]) access_token = token["accessToken"] import requests url = "https://api.hubapi.com/crm/v3/objects/contacts?limit=10" headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json" } response = requests.get(url, headers=headers) contacts = response.json() ``` ## 🛠️ Slack Bot + User Token Example Use Descope Outbound Apps to fetch **both Slack bot and user tokens** to build bots that can also perform user-authorized actions: ### What You Receive from Descope When you call: ```ts const token = await getOutboundToken( "slack", "", ["chat:write"], { userScopes: ["channels:history"] } ); ``` Descope will return: ```json { "accessToken": "", "expiresAt": 1712345678, "refreshToken": "", "scopes": ["chat:write"], "userToken": "", "userScopes": ["channels:history"], "provider": "slack" } ``` - accessToken: Slack Bot User OAuth token, scoped to the bot-level permissions you requested (e.g., chat:write). - userToken: Slack User OAuth token, scoped to the user-level permissions you requested (e.g., channels:history). - scopes / userScopes: Reflect the scopes granted during the outbound app OAuth exchange. - refreshToken: Present only if your Slack app and Descope outbound app are configured for refresh tokens. - expiresAt: Unix timestamp for expiry, based on Slack’s token behavior. - provider: Always "slack" for clarity. ### Example: Posting message as the bot, Reading User Channels ```javascript // Fetch Slack bot + user tokens const token = await getOutboundToken( "slack", "", ["chat:write"], { userScopes: ["channels:history"] } ); // Post a message as the bot const postResponse = await fetch("https://slack.com/api/chat.postMessage", { method: "POST", headers: { "Authorization": `Bearer ${token.accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ channel: "", text: "Hello from your Slack bot!" }) }); // Read user channels using the user token const channelsResponse = await fetch("https://slack.com/api/conversations.list", { headers: { "Authorization": `Bearer ${token.userToken}` } }); const channelsData = await channelsResponse.json(); console.log(channelsData); ``` With this flow, you can easily combine bot functionality (posting messages, responding to events) with user-authorized actions (fetching channels, reading conversations, posting as user with additional scopes) within your Slack-integrated flows using Descope Outbound Apps. ## Important Notes - Store your Management Key securely - it has admin permissions to your Descope project - Use environment variables to manage your Project ID and Management Key - Token requests should only occur server-side, never in client code - Set appropriate scopes when requesting tokens for better security Coming soon: More examples for platforms like Slack, Notion, Zoom, and GitHub! # Outbound Apps (/identity-federation/outbound-apps) Discover how Descope's Outbound Apps enable seamless integration with third-party platforms, enhancing user experiences with additional OAuth consents. # Outbound Apps Outbound Apps and [Connections](/agentic-identity-hub/core-components/connections) provide the same capability (a token vault for OAuth tokens and API keys at the user or tenant level, with Descope managing storage and refresh), but they are **separate objects in the Console**. Outbound Apps live under **Connect → Outbound Apps**; Connections live under **Agentic Identity Hub → Connections**. An Outbound App you create here does not appear under Connections, and vice versa. Use **Outbound Apps** for vaulting third-party tokens in a traditional application; use **Connections** when building agents and MCP servers. Descope Outbound Apps are a secure, flexible way to manage third-party integrations for your users and tenants. They are **not** [Resources](/resources); Outbound Apps store credentials for APIs that Descope does not protect with your project JWTs. Outbound Apps can: - **Connect to third-party OAuth providers** (like Google, Microsoft, LinkedIn, etc.) and manage the full OAuth lifecycle, including consent, token refresh, and scope management. - **Act as a token vault** for both OAuth tokens and static API keys, allowing you to store, retrieve, and manage secrets for any external service, whether it uses OAuth or not. ## Use Cases - **Incremental OAuth Scopes** Start with minimal permissions (e.g., `openid`, `email`, `profile`) during authentication, then request additional scopes (like access to calendars, contacts, or posting capabilities) only when needed. This keeps your initial login experience simple and user-friendly. - **AI Agents & MCP Server Integrations** If you're building AI agents or MCP servers that need access to external services (e.g., Google Calendar, Salesforce), Outbound Apps handle secure storage, token refresh, and access control. Your tools can reliably retrieve user/tenant-scoped OAuth tokens or API keys whenever they need to call third-party APIs. - **Fine-Grained Access Control** Define and enforce who is allowed to retrieve tokens for a given Outbound App using Descope's [Policies](/agentic-identity-hub/policies). Prevent unauthorized agents or users from accessing sensitive third-party services, even if the connection has already been established. ## How It Works 1. **Create an Outbound App** - Choose a preconfigured OAuth provider, or create a custom app for any service (including API key storage). 2. **Connect Users or Tenants** - Use Descope Flows, SDKs, or APIs to connect users to OAuth providers _or_ to collect and store API keys. 3. **Token Vault** - Descope securely stores and manages all tokens and API keys, making them available for backend or agent use. ## Next Steps - [Creating an Outbound App](/identity-federation/outbound-apps/creating-outbound-apps) - [Connecting Users & Adding API Keys](/identity-federation/outbound-apps/connect) - [Using Outbound App Tokens & Keys](/identity-federation/outbound-apps/using-outbound-apps) ## Creating an Outbound App To set up an outbound application in Descope, follow the steps in our [Creating an Outbound App](/identity-federation/outbound-apps/creating-outbound-apps) guide. ## Token Management Within the token management tab, you can view details of the users who have granted consent to the outbound app. - **ID**: System-generated ID paring that user's consent to the application. - **App ID**: The configured application ID which coincides with the token ID. - **Associated User**: The user ID of the user who's associated with the consent. - **Scopes**: The consented scopes correlate to the user's consent to the application. - **Access Token Expiration**: Expiration of the current access token for the user's consent. - **Refresh Token**: Boolean indicating whether a refresh token is available. - **Last Refreshed**: The last time the user's access token was refreshed. - **Last Refresh Error**: If applicable, the last error encountered while trying to refresh the user's access token. - **Token Subject**: The user reference on the provider side. For this example, it is associated with the unique user ID of the user's Google account. - **Access Token Type**: Specifies the format or method the access token uses, such as Bearer or MAC, which determines how it is used for authentication and authorization. - **Tenant ID**: The tenant ID of the tenant associated with the consent. ![Viewing an outbound applications token management Descope](/assets/outbound-app-token-management.webp) ## Connecting to Your Outbound Apps There are multiple ways you can connect your users to your outbound apps: - **[Frontend SDKs](/identity-federation/outbound-apps/connect#connecting-with-frontend-sdks)**: Implement OAuth connections using our JavaScript SDKs (Web, React, Next.js) - **[Descope Flows](/identity-federation/outbound-apps/connect#connecting-with-descope-flows)**: Use our no-code flow editor with pre-built OAuth components - **[APIs](/identity-federation/outbound-apps/connect#connecting-with-apis)**: Use our REST API to initiate connections from non-JavaScript platforms Learn more about implementation details and best practices in our [Connection Guide](/identity-federation/outbound-apps/connect). ## Using Your Outbound App Tokens Once your users are connected to outbound apps, you can start leveraging the tokens to access third-party APIs: - **[Token Management](/identity-federation/outbound-apps/using-outbound-apps#fetching-outbound-apps-tokens)**: Fetch and refresh user tokens securely through Descope's API - **[API Integration](/identity-federation/outbound-apps/using-outbound-apps#using-tokens-with-third-party-apis)**: Use tokens to make authenticated requests to third-party providers - **[Best Practices](/identity-federation/outbound-apps/using-outbound-apps#token-management-best-practices)**: Implement proper error handling and scope management - **[Examples](/identity-federation/outbound-apps/examples)**: Real-world implementations including AI agent tool calling Learn more about token usage and implementation patterns in our [Usage Guide](/identity-federation/outbound-apps/using-outbound-apps). # Managing with SDKs (/identity-federation/outbound-apps/sdks) Learn how to manage outbound applications using the Descope backend SDKs. # Outbound Apps with SDKs You can use the Descope management SDK to fetch, delete, and manage [Outbound App](/identity-federation/outbound-apps) tokens. The management SDK requires a management key, which can be generated from the [Company Settings page](https://app.descope.com/settings/company/managementkeys) of the Descope console. ### Install SDK ```bash npm install @descope/node-sdk ``` ```bash pip install descope ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```bash go get -u github.com/descope/go-sdk ``` ```bash composer require descope/descope-php ``` ```bash gem install descope ``` ### Import and initialize Management SDK ```typescript import DescopeClient from '@descope/node-sdk'; // Initialized using environment variables DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY const descopeClient = DescopeClient(); // Or directly const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: 'management-key' }); ``` ```python from descope import DescopeClient # Initialized using environment variables DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY descope_client = DescopeClient() # Or directly descope_client = DescopeClient( project_id="__ProjectID__", management_key="management-key" ) ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); // Set up the outbound apps service OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); ``` ```go import ( "context" "github.com/descope/go-sdk/descope/client" ) descopeClient, err := client.NewWithConfig(&client.Config{ ProjectID: "__ProjectID__", ManagementKey: "management-key", }) if err != nil { // Handle the error } ctx := context.Background() outboundApps := descopeClient.Management.OutboundApplication() ``` ```php use Descope\SDK\DescopeSDK; $descopeSDK = new DescopeSDK([ 'projectId' => '__ProjectID__', 'managementKey' => 'management-key', ]); // Outbound app methods live on the management component $outboundApps = $descopeSDK->management->outboundApps; ``` ```ruby require 'descope' # Outbound app methods are mixed directly into the client, # so you call them as client. rather than through a sub-object. client = Descope::Client.new( project_id: '__ProjectID__', management_key: ENV['MGMT_KEY'] ) ``` ## Fetch Outbound App Token Fetch an access token for a specific outbound app and user. This is useful when you need to make API calls to third-party services on behalf of a user. ```typescript // Args: // appId (string): Outbound app ID (required) // userId (string): User ID (required) // tenantId (string): Optional tenant ID // options (object): Optional request options try { const token = await descopeClient.management.outboundApplication.fetchToken( 'app-id', 'user-id' ); const accessToken = token.data.accessToken; } catch (error) { // Handle the error } ``` ```python # Args: # app_id (str): Outbound app ID (required) # user_id (str): User ID (required) # tenant_id (str): Optional tenant ID # options (dict): Optional request options try: token = descope_client.mgmt.outbound_application.fetch_token( app_id="app-id", user_id="user-id" ) access_token = token["token"]["accessToken"] except Exception as e: # Handle the error print(f"Error: {e}") ``` ```java // Args: // appId (String): Outbound app ID (required) // userId (String): User ID (required) try { FetchLatestOutboundAppUserTokenRequest request = new FetchLatestOutboundAppUserTokenRequest(); request.setAppId("app-id"); request.setUserId("user-id"); FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchLatestOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); } catch (DescopeException de) { // Handle the error } ``` ```go // Args: // AppID (string): Outbound app ID (required) // UserID (string): User ID (required) // TenantID (string): Optional tenant ID // Options (*descope.OutboundAppUserTokenOptions): Optional request options token, err := outboundApps.FetchLatestUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "app-id", UserID: "user-id", }) if err != nil { // Handle the error } accessToken := token.AccessToken ``` ```php // Args (array keys): // appId (string): Outbound app ID (required) // userId (string): User ID (required) // tenantId (string): Optional tenant ID // options (array): Optional request options try { $response = $outboundApps->fetchLatestUserToken([ 'appId' => 'app-id', 'userId' => 'user-id', ]); $accessToken = $response['token']['accessToken']; } catch (AuthException $e) { // Handle the error } ``` ```ruby # Not supported in the Ruby SDK. Fetch the token by its exact scopes instead, # or call POST /v1/mgmt/outbound/app/user/token/latest directly. ``` ## Fetch Outbound App Token by Scopes Fetch an access token with specific scopes for a user. ```typescript // Args: // appId (string): Outbound app ID (required) // userId (string): User ID (required) // scopes (string[]): List of scopes to request (required) // options (object): Optional request options // tenantId (string): Optional tenant ID try { const token = await descopeClient.management.outboundApplication.fetchTokenByScopes( 'app-id', 'user-id', ['read', 'write'] ); const accessToken = token.data.accessToken; } catch (error) { // Handle the error } ``` ```python # Args: # app_id (str): Outbound app ID (required) # user_id (str): User ID (required) # scopes (list): List of scopes to request (required) # options (dict): Optional request options # tenant_id (str): Optional tenant ID try: token = descope_client.mgmt.outbound_application.fetch_token_by_scopes( app_id="app-id", user_id="user-id", scopes=["read", "write"] ) access_token = token["token"]["accessToken"] except Exception as e: # Handle the error print(f"Error: {e}") ``` ```java // Args: // appId (String): Outbound app ID (required) // userId (String): User ID (required) // scopes (List): List of scopes to request (required) try { FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest(); request.setAppId("app-id"); request.setUserId("user-id"); request.setScopes(Arrays.asList("read", "write")); FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); } catch (DescopeException de) { // Handle the error } ``` ```go // Args: // AppID (string): Outbound app ID (required) // UserID (string): User ID (required) // Scopes ([]string): List of scopes to request // TenantID (string): Optional tenant ID // Options (*descope.OutboundAppUserTokenOptions): Optional request options token, err := outboundApps.FetchUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "app-id", UserID: "user-id", Scopes: []string{"read", "write"}, Options: &descope.OutboundAppUserTokenOptions{ WithRefreshToken: false, ForceRefresh: false, }, }) if err != nil { // Handle the error } accessToken := token.AccessToken ``` ```php // Args: // $appId (string): Outbound app ID (required) // $userId (string): User ID (required) // $scopes (?array): Optional list of scopes to request // $withRefreshToken (bool): Include the refresh token, defaults to false // $forceRefresh (bool): Force a refresh, defaults to false // $tenantId (?string): Optional tenant ID try { $response = $outboundApps->fetchUserToken( 'app-id', 'user-id', ['read', 'write'] ); $accessToken = $response['token']['accessToken']; } catch (AuthException $e) { // Handle the error } ``` ```ruby # Args: # app_id (String): Outbound app ID (required) # user_id (String): User ID (required) # scopes (Array): Optional list of scopes to request # with_refresh_token (Boolean): Include the refresh token, defaults to false # force_refresh (Boolean): Force a refresh, defaults to false # tenant_id (String): Optional tenant ID begin result = client.fetch_outbound_app_user_token( app_id: 'app-id', user_id: 'user-id', scopes: %w[read write] ) access_token = result['token']['accessToken'] rescue Descope::ArgumentException => e # Raised when app_id or user_id is empty rescue Descope::AuthException => e # Handle the error end ``` ## Delete Outbound App Token by Token ID This operation deletes a specific outbound app token by its token ID. ```typescript // Args: // id (string): The token ID to delete try { await descopeClient.management.outboundApplication.deleteTokenById('token-id'); } catch (error) { // Handle the error } ``` ```python # Args: # token_id (str): The token ID to delete try: descope_client.mgmt.outbound_application.delete_token(token_id="token-id") except Exception as e: # Handle the error print(f"Error: {e}") ``` ```java // Args: // tokenId (String): The token ID to delete try { outboundAppsService.deleteOutboundAppTokenById("token-id"); } catch (DescopeException de) { // Handle the error } ``` ```go // Args: // id (string): The token ID to delete (required) err := outboundApps.DeleteTokenByID(ctx, "token-id") if err != nil { // Handle the error } ``` ```php // Args: // $tokenId (string): The token ID to delete (required) try { $outboundApps->deleteTokenById('token-id'); } catch (AuthException $e) { // Handle the error } ``` ```ruby # Args: # token_id (String): The token ID to delete (required) begin client.delete_outbound_app_token_by_id(token_id: 'token-id') rescue Descope::ArgumentException => e # Raised when token_id is empty rescue Descope::AuthException => e # Handle the error end ``` ## Delete All Outbound App Tokens for a User This operation deletes all outbound app tokens for a given user and app. ```typescript // Args: // appId (string): Optional outbound app ID // userId (string): Optional user ID try { // Delete this user's tokens for this app await descopeClient.management.outboundApplication.deleteUserTokens('app-id', 'user-id'); // Delete all tokens for the app await descopeClient.management.outboundApplication.deleteUserTokens('app-id'); // Delete all tokens for the user await descopeClient.management.outboundApplication.deleteUserTokens(undefined, 'user-id'); } catch (error) { // Handle the error } ``` ```python # Args: # app_id (str): Optional outbound app ID # user_id (str): Optional user ID try: # Delete this user's tokens for this app descope_client.mgmt.outbound_application.delete_user_tokens( app_id="app-id", user_id="user-id" ) # Delete all tokens for the app descope_client.mgmt.outbound_application.delete_user_tokens(app_id="app-id") # Delete all tokens for the user descope_client.mgmt.outbound_application.delete_user_tokens(user_id="user-id") except Exception as e: # Handle the error print(f"Error: {e}") ``` ```java // Args: // appId (String): Optional outbound app ID // userId (String): Optional user ID try { outboundAppsService.deleteOutboundAppUserTokens( DeleteOutboundAppUserTokensRequest.builder() .appId("app-id") .userId("user-id") .build() ); } catch (DescopeException de) { // Handle the error } ``` ```go // Args: // appID (string): Optional outbound app ID // userID (string): Optional user ID // Returns an error if both are empty. // Delete this user's tokens for this app err := outboundApps.DeleteUserTokens(ctx, "app-id", "user-id") // Delete all tokens for the app err = outboundApps.DeleteUserTokens(ctx, "app-id", "") // Delete all tokens for the user err = outboundApps.DeleteUserTokens(ctx, "", "user-id") if err != nil { // Handle the error } ``` ```php // Args: // $appId (?string): Optional outbound app ID // $userId (?string): Optional user ID try { // Delete this user's tokens for this app $outboundApps->deleteUserTokens('app-id', 'user-id'); // Delete all tokens for the app $outboundApps->deleteUserTokens('app-id', null); // Delete all tokens for the user $outboundApps->deleteUserTokens(null, 'user-id'); } catch (AuthException $e) { // Handle the error } ``` ```ruby # Args: # app_id (String): Optional outbound app ID # user_id (String): Optional user ID # Raises Descope::ArgumentException if both are empty. begin # Delete this user's tokens for this app client.delete_outbound_app_user_tokens(app_id: 'app-id', user_id: 'user-id') # Delete all tokens for the app client.delete_outbound_app_user_tokens(app_id: 'app-id') # Delete all tokens for the user client.delete_outbound_app_user_tokens(user_id: 'user-id') rescue Descope::ArgumentException => e # Raised when both app_id and user_id are empty rescue Descope::AuthException => e # Handle the error end ``` # Using Outbound Apps (/identity-federation/outbound-apps/using-outbound-apps) Learn how to integrate outbound apps with Descope to securely manage third-party OAuth tokens, handle token refresh, and access external APIs. # Using Outbound Apps For hands-on examples of outbound apps in action, check out our [Examples Guide](/identity-federation/outbound-apps/examples), which includes tool-calling examples for AI agents. After configuring an **Outbound App** in Descope and connecting your users to it, you can start using the tokens to access third-party APIs on behalf of your users. With Outbound Apps, you can [fetch the tokens](/api/management/outbound-apps/fetch-outbound-app-user-token) and use them to make authenticated requests to third-party provider APIs, enabling seamless integration without exposing sensitive credentials to your application. ## Fetching Outbound Apps Tokens This section covers how to fetch outbound app tokens for your users and tenants. To retrieve a user's token for an outbound app, you can use either the REST API or one of our SDKs. You'll either need your [Project ID](https://app.descope.com/settings/project) and [Management Key](https://docs.descope.com/company-settings#management-keys), or a user/tenant scoped [Inbound App](/identity-federation/inbound-apps) token. For more information on how to authenticate your requests, see the [Authentication for Token Fetching](#authentication-for-token-fetching) section below. ### Authentication for Token Fetching Your Inbound App access token must include the `outbound.token.fetch` scope to be able to fetch outbound app tokens. This scope must be originally requested by the OAuth client and consented to by the user. You can authenticate requests to fetch outbound app tokens using either: - A [Management Key](/management#management-keys), formatted as `Bearer __ProjectID__:` - An [Inbound App](/identity-federation/inbound-apps) token, formatted as `Bearer __ProjectID__:` (from the user's Inbound App authentication) You should use Management Keys when: - Your backend environment is secure and can safely store secrets. - You want full administrative control over token access across users and tenants. Otherwise, you should use Inbound App tokens when: - You're operating from a **public or frontend-facing client** that cannot store secrets securely. - You're building an **MCP server or tool execution layer** and want to use Descope [Policies](/agentic-identity-hub/policies) to determine—in real time—whether a user or tenant is authorized to retrieve an outbound token. - If the access control rule **denies** access (e.g., the user's role does not permit the requested tool), Descope will block the outbound token request, even if the user has already connected to the provider. Descope provides two methods for fetching user-level outbound app tokens, depending on whether you know the exact scopes you need: Fetching outbound app tokens with a Management Key is supported in the NodeJS, Python, Java, Go, PHP, and Ruby SDKs. Support varies per operation, so check [Managing with SDKs](/identity-federation/outbound-apps/sdks) before you build. ### Fetch Latest User Token (Recommended) ```js // Fetch latest user token const latestUserToken = await descopeClient.management.outboundApplication.fetchToken( 'google-contacts', 'user-123' ); const accessToken = latestUserToken.data.accessToken; ``` ```python # Fetch latest user token latest_user_token = descope_client.mgmt.outbound_application.fetch_token( app_id="google-contacts", user_id="user-123" ) access_token = latest_user_token["token"]["accessToken"] ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch latest token FetchLatestOutboundAppUserTokenRequest request = new FetchLatestOutboundAppUserTokenRequest(); request.setAppId("google-contacts"); request.setUserId("user-123"); FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchLatestOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch latest user token token, err := descopeClient.Management.OutboundApplication().FetchLatestUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "google-contacts", UserID: "user-123", TenantID: "tenant-id", // optional }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch latest user token $response = $descopeSDK->management->outboundApps->fetchLatestUserToken([ 'appId' => 'google-contacts', 'userId' => 'user-123', 'tenantId' => 'tenant-id', // optional ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "userId": "xxxxx", "tenantId": "optional-tenant-id", "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the outbound app. * **`userId`** (required): The user ID for whom to fetch the token. * **`tenantId`** (optional): The tenant ID of the user, if a user has multiple tokens associated with different tenants. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ### Fetch Token with Specific Scopes Use this method when you need a token with specific scopes. **Important**: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a `404 Token not found` error. ```js // Fetch user token with specific scopes const userToken = await descopeClient.management.outboundApplication.fetchTokenByScopes( 'google-contacts', 'user-123', ['https://www.googleapis.com/auth/contacts.readonly'] ); const accessToken = userToken.data.accessToken; ``` ```python # Fetch user token with specific scopes user_token = descope_client.mgmt.outbound_application.fetch_token_by_scopes( app_id="google-contacts", user_id="user-123", scopes=["https://www.googleapis.com/auth/contacts.readonly"] ) access_token = user_token["token"]["accessToken"] ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch token with specific scopes FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest(); request.setAppId("google-contacts"); request.setUserId("user-123"); request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly")); FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch user token with specific scopes token, err := descopeClient.Management.OutboundApplication().FetchUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "google-contacts", UserID: "user-123", Scopes: []string{"https://www.googleapis.com/auth/contacts.readonly"}, }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch user token with specific scopes $response = $descopeSDK->management->outboundApps->fetchUserToken( 'google-contacts', 'user-123', ['https://www.googleapis.com/auth/contacts.readonly'] ); $accessToken = $response['token']['accessToken']; ``` ```ruby # Fetch user token with specific scopes result = client.fetch_outbound_app_user_token( app_id: 'google-contacts', user_id: 'user-123', scopes: ['https://www.googleapis.com/auth/contacts.readonly'] ) access_token = result['token']['accessToken'] ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/token" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "userId": "xxxxx", "tenantId": "optional-tenant-id", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ], "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the outbound app. * **`userId`** (required): The user ID for whom to fetch the token. * **`tenantId`** (optional, required if no userId): The tenant ID of the user, if a user has multiple tokens associated with different tenants. * **`scopes`** (required): An array of exact scopes that match the original token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ## Fetching Tenant-Level Tokens In addition to user-specific tokens, you can also fetch tenant-level tokens for outbound apps. These are useful when you need to access APIs on behalf of a tenant rather than a specific user. Descope provides the same two methods for tenant-level outbound app tokens as user-level tokens. Depending on whether you know the exact scopes of the token you need, you can use the following methods: ### Fetch Latest Tenant Token (Recommended) This method is recommended when you don't know the exact scopes or want the most recent valid token for the tenant, regardless of scopes. ```js // Fetch latest tenant token const tenantToken = await descopeClient.management.outboundApplication.fetchTenantToken( 'google-contacts', 'tenant-123' ); const accessToken = tenantToken.data.accessToken; ``` ```python # Fetch latest tenant token tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token( app_id="google-contacts", tenant_id="tenant-123" ) access_token = tenant_token["token"]["accessToken"] ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch latest tenant token FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest(); request.setAppId("google-contacts"); request.setTenantId("tenant-123"); FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchLatestOutboundAppTenantToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch latest tenant token token, err := descopeClient.Management.OutboundApplication().FetchLatestTenantToken(ctx, &descope.FetchOutboundAppTenantTokenRequest{ AppID: "google-contacts", TenantID: "tenant-123", }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch latest tenant token $response = $descopeSDK->management->outboundApps->fetchLatestTenantToken([ 'appId' => 'google-contacts', 'tenantId' => 'tenant-123', ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/token/latest" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "tenantId": "tenant-123", "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Latest Tenant Token Request Parameters The tenant-level APIs use a similar request schema: * **`appId`** (required): The ID of the outbound app. * **`tenantId`** (optional, required if no userId): The tenant ID if you're fetching a tenant-level token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ### Fetch Tenant Token with Specific Scopes Use this method when you need a tenant token with specific scopes. **Important**: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a `404 Token not found` error. ```js // Fetch tenant token with specific scopes const tenantToken = await descopeClient.management.outboundApplication.fetchTenantTokenByScopes( 'google-contacts', 'tenant-123', ['https://www.googleapis.com/auth/contacts.readonly'] ); const accessToken = tenantToken.data.accessToken; ``` ```python # Fetch tenant token with specific scopes tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token_by_scopes( app_id="google-contacts", tenant_id="tenant-123", scopes=["https://www.googleapis.com/auth/contacts.readonly"] ) access_token = tenant_token["token"]["accessToken"] ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch tenant token with specific scopes FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest(); request.setAppId("google-contacts"); request.setTenantId("tenant-123"); request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly")); FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantTokenByScopes(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch tenant token with specific scopes token, err := descopeClient.Management.OutboundApplication().FetchTenantToken(ctx, &descope.FetchOutboundAppTenantTokenRequest{ AppID: "google-contacts", TenantID: "tenant-123", Scopes: []string{"https://www.googleapis.com/auth/contacts.readonly"}, }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch tenant token with specific scopes $response = $descopeSDK->management->outboundApps->fetchTenantToken([ 'appId' => 'google-contacts', 'tenantId' => 'tenant-123', 'scopes' => ['https://www.googleapis.com/auth/contacts.readonly'], ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/token" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "tenantId": "tenant-123", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ], "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Specific Tenant Token Request Parameters The tenant-level APIs use a similar request schema: * **`appId`** (required): The ID of the outbound app. * **`tenantId`** (optional, required if no userId): The tenant ID if you're fetching a tenant-level token. * **`scopes`** (required): An array of exact scopes that match the original token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ## Outbound App Token Response The `refresh_token` will not be returned unless `withRefreshToken` is set to **true** in the request. The response will include the user/tenant token details, similar to the example below: ```json { "token": { "id": "xxxx", "appId": "google-contacts", "userId": "xxxx", "tokenSub": "", "accessToken": "ya29.xxxx", "accessTokenType": "Bearer", "accessTokenExpiry": "1741107113", "hasRefreshToken": true, "refreshToken": "xxxx", "lastRefreshTime": "1741103514", "lastRefreshError": "", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ] } } ``` ## Using Tokens with Third-Party APIs Once you have the access token, you can use it to make authenticated requests to the third-party provider's API. ### Example: Google Contacts API The following example shows how to fetch a user's contacts using the Google Contacts API with a token obtained from an outbound app. ```python import json import requests access_token = "ya29.xxxx" user_mail = "user@example.com" request_url = "https://people.googleapis.com/v1/people/me/connections" params = { 'personFields': 'names,emailAddresses', # Specify valid fields 'pageSize': 100 # Adjust as needed } headers = {"Authorization": f"Bearer {access_token}"} response = requests.get(request_url, headers=headers, params=params) # Check the response status code if response.status_code == 200: try: # Parse the JSON response response_array = response.json() if 'connections' in response_array and response_array['connections']: # Pretty print the JSON response print('Response:', json.dumps(response_array['connections'], indent=4)) else: print('No contacts found.') except requests.exceptions.JSONDecodeError: print('Error decoding JSON:', response.text) else: print(f'Error: {response.status_code}') print('Response Text:', response.text) ``` For more detailed examples and AI agent implementations, see our [Examples Guide](/identity-federation/outbound-apps/examples). ## Token Management Best Practices ### Error Handling When working with outbound app tokens, you may encounter different types of errors. Here's what each error code means and how to handle them: #### Common Error Codes | Status Code | Meaning | Common Causes | |-------------|---------|---------------| | **401** | Unauthorized | Invalid management key or project ID | | **403** | Forbidden | Insufficient permissions or invalid tenant access | | **404** | Token not found | User never connected to the app, token was cleared, or wrong scopes provided | | **500** | Server error | Invalid HTTP method (not POST) or malformed JSON payload | #### Error Handling Example ```python import requests from requests.exceptions import RequestException def fetch_outbound_token(app_id, user_id, scopes=None): """Fetch an outbound app token with proper error handling.""" headers = {"Authorization": f"Bearer {PROJECT_ID}:{MANAGEMENT_KEY}"} try: if scopes: # Use specific scopes endpoint url = "__BaseURL__/v1/mgmt/outbound/app/user/token" data = {"appId": app_id, "userId": user_id, "scopes": scopes} else: # Use latest token endpoint url = "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" data = {"appId": app_id, "userId": user_id} response = requests.post(url, headers=headers, json=data, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if response.status_code == 404: # Token not found - either never existed or was cleared recently print("Token not found. The user may not have connected to this app, " "or the token may have been cleared.") return None elif response.status_code == 500: # Server error - issue with HTTP request method or JSON payload print("Server error. Check your request method (should be POST) " "and ensure your JSON payload is properly formatted.") return None else: print(f"HTTP error occurred: {e}") return None except requests.exceptions.Timeout: print("Request timed out") return None except RequestException as e: print(f"Request failed: {e}") return None def make_api_request(access_token, url, params=None): """Make a request to a third-party API with proper error handling.""" headers = {"Authorization": f"Bearer {access_token}"} try: response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if response.status_code == 401: print("Access token may be invalid or expired") elif response.status_code == 403: print("Insufficient permissions for this request") else: print(f"HTTP error occurred: {e}") return None except requests.exceptions.Timeout: print("Request timed out") return None except RequestException as e: print(f"Request failed: {e}") return None ``` ### Scope Management Outbound apps can store multiple tokens for the same user, each with different scopes. When connecting to outbound apps, **always request the minimum number of scopes necessary for your use case**. Granting only the least privileges possible to your application or agent helps reduce security risks if a token is ever compromised. By limiting access, you minimize the potential impact of a breach and follow the principle of least privilege, which is a security best practice for all integrations and automations. # Exchange Key (/api/access-keys/exchange-key) ### Exchange API key for access token This API Endpoint will take an API key for the project and provide an access token to be used for accessing the application. The session token JWT token will be valid for the configured [Session Token Timeout](/project-settings#session-token-timeout), and its expiration time will be provided in the `expiration` field of the response object. # Enchanted Link Authentication API Overview (/api/enchanted-link) Use the Descope REST API to build enchanted link authentication for your application. # Enchanted Link APIs ## Overview The Enchanted Link APIs enable users to sign in by clicking a link delivered to their email address. The email includes 3 different links, and the user must click the correct one based on the 2-digit number displayed when initiating the authentication process. Read about Enchanted Link implementation types [here](/auth-methods/enchanted-link/with-sdks/client). This implementation type has three phases: 1. Initiate the process and send the enchanted link — this is triggered from the application sign-in or sign-up screen. 2. Poll for token verification and acquire the session and refresh tokens. 3. Verify the token. The "Verify the token" phase happens in parallel and independent from the polling phase. As soon as it completes, the polling phase returns the user's sign-in details (session and refresh tokens). ## Use Cases 1. [Sign up](/api/enchanted-link/sign-up) a new user 2. [Sign in](/api/enchanted-link/sign-in) an existing user 3. [Sign in with auto sign-up](/api/enchanted-link/sign-in-auto-sign-up) if the user does not exist 4. [Update user's email address](/api/enchanted-link/update-email) 5. [Verify Token](/api/enchanted-link/verify-token) 6. [Poll Session](/api/enchanted-link/poll-session) ## Examples ### Example - sign up 1. Trigger the sign-up process with the [Sign-Up](/api/enchanted-link/sign-up) endpoint. On success, the response body includes `pendingRef` and `linkId`. Show the `linkId` to the end user. An email is generated with 3 links and random numbers — one of them matches the `linkId`. 2. Poll for token verification with the [Poll Session](/api/enchanted-link/poll-session) endpoint. Use the `pendingRef` to identify the enchanted link you are waiting on. Once the user clicks the correct link and you call the Verify Token endpoint (step 3), polling returns the session and refresh tokens. 3. When the user clicks the correct link, call the [Verify Token](/api/enchanted-link/verify-token) endpoint. On success, the endpoint returns a 200 status with an empty body. This example applies to all other use cases. # Poll Session (/api/enchanted-link/poll-session) ### Poll user session for successful completion of token verification This endpoint is used to wait for the enchanted link verification by the end user. Use this endpoint in a poling way, until it returns a successful JWT, or timeout error. The response object includes the session JWT `sessionJwt` and refresh JWT `refreshJwt` when this endpoint completes successfully. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. # Sign-In with Auto Sign-Up (/api/enchanted-link/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending an enchanted link via email Initiate a process that implements both sign-in and sign-up using a single endpoint. If the email address is already registered (the end user has already registered) the user will be signed in. If the email address is not registered (the end user is not yet registered) the user will be signed up. Descope will generate and deliver 3 clickable links to the email address specified, each is numbered with random 2 digit number. When you initiate the enchanted link, the `linkId` will be returned. This `linkId` needs to be displayed to the user to indicate which link for the user to click once they receive the email. Only when the correct link is clicked will the user be successfully verified and logged in. Each clickable link is made up of two parts - the URI you provide in the `URI` field and the enchanted link token generated by Descope. For example, if `URI=https://app.mycompany.com/enchantedlink/verify`, the clickable enchanted link will be `https://app.mycompany.com/enchantedlink/verify?t=enchanted-link-token.` Enchanted links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/enchantedlink), so sending multiple enchanted links (for example, when an end user tries to sign-up a second or third time) does not invalidate links that have already been sent. The return body will include `linkId` and `pendigRef`. The `linkId` (a 2 digit number) should be presented to the user, so they will know which link to click in the delivered email. The endpoint will return a failure code if the email address is already registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps 1. Verify the enchanted link token using the [Verify Token](/api/enchanted-link/verify-token) endpoint. 2. Poll for the successful completion of the token verification using the [Poll Session](/api/enchanted-link/poll-session) endpoint, providing the `pendingRef` returned by the this endpoint. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/enchanted-link/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/enchanted-link/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-In (/api/enchanted-link/sign-in-enchanted-link-sms) Login a user based on an enchanted link that will be sent by SMS # Sign-In (/api/enchanted-link/sign-in) ### Sign-in existing user by sending an enchanted link via email Initiate a sign-in process by sending an enchanted link to a new end user. Descope will generate and deliver 3 clickable links to the email address specified, each is numbered with random 2 digit number. When you initiate the enchanted link, the `linkId` will be returned. This `linkId` needs to be displayed to the user to indicate which link for the user to click once they receive the email. Only when the correct link is clicked will the user be successfully verified and logged in. Each clickable link is made up of two parts - the URI you provide in the `URI` field and the enchanted link token generated by Descope. For example, if `URI=https://app.mycompany.com/enchantedlink/verify`, the clickable enchanted link will be `https://app.mycompany.com/enchantedlink/verify?t=enchanted-link-token.` Enchanted links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/enchantedlink), so sending multiple enchanted links (for example, when an end user tries to sign-up a second or third time) does not invalidate links that have already been sent. The return body will include `linkId` and `pendigRef`. The `linkId` (a 2 digit number) should be presented to the user, so they will know which link to click in the delivered email. The endpoint will return a failure code if the email address is already registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps 1. Verify the enchanted link token using the [Verify Token](/api/enchanted-link/verify-token) endpoint. 2. Poll for the successful completion of the token verification using the [Poll Session](/api/enchanted-link/poll-session) endpoint, providing the `pendingRef` returned by the this endpoint. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/enchanted-link/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/enchanted-link/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/enchanted-link/sign-up-enchanted-link-sms) Create a new user based on the given identifier or phone # Sign-In (with User Creation) (/api/enchanted-link/sign-up-or-in-enchanted-link-sms) Login in using phone. If the user does not exist, a new user will be created with the given phone # Sign-Up (/api/enchanted-link/sign-up) ### Sign-up new end user by sending an enchanted link via email Initiate a sign-up process by sending an enchanted link to a new end user. Descope will generate and deliver 3 clickable links to the email address specified, each is numbered with random 2 digit number. When you initiate the enchanted link, the `linkId` will be returned. This `linkId` needs to be displayed to the user to indicate which link for the user to click once they receive the email. Only when the correct link is clicked will the user be successfully verified and logged in. Each clickable link is made up of two parts - the URI you provide in the `URI` field and the enchanted link token generated by Descope. For example, if `URI=https://app.mycompany.com/enchantedlink/verify`, the clickable enchanted link will be `https://app.mycompany.com/enchantedlink/verify?t=enchanted-link-token.` Enchanted links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/enchantedlink), so sending multiple enchanted links (for example, when an end user tries to sign-up a second or third time) does not invalidate links that have already been sent. The return body will include `linkId` and `pendigRef`. The `linkId` (a 2 digit number) should be presented to the user, so they will know which link to click in the delivered email. The endpoint will return a failure code if the email address is already registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps 1. Verify the enchanted link token using the [Verify Token](/api/enchanted-link/verify-token) endpoint. 2. Poll for the successful completion of the token verification using the [Poll Session](/api/enchanted-link/poll-session) endpoint, providing the `pendingRef` returned by the this endpoint. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-In](/api/enchanted-link/sign-in) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/enchanted-link/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Email (/api/enchanted-link/update-email) ### Update email of end user by sending enchanted link via email Update the email address of an existing end user by sending an enchanted link to the new email address. Descope will generate and deliver 3 clickable links to the email address specified, each is numbered with random 2 digit number. Only the right link (based on the number returned will be successfully verified when clicked) Each clickable link is made up of two parts - the URI you provide in the `URI` field and the enchanted link token generated by Descope. For example, if `URI=https://app.mycompany.com/enchantedlink/verify`, the clickable enchanted link will be `https://app.mycompany.com/enchantedlink/verify?t=enchanted-link-token.` Enchanted links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/enchantedlink), so sending multiple enchanted links (for example, when an end user tries to sign-up a second or third time) does not invalidate links that have already been sent. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. Once the token is successfully verified - the email address will be updated. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Steps 1. Verify the enchanted link token using the [Verify Token](/api/enchanted-link/verify-token) endpoint. 2. Poll for the successful completion of the token verification using the [Poll Session](/api/enchanted-link/poll-session) endpoint, providing the `pendingRef` returned by the this endpoint. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Use to update phone and validate via enchanted link using SMS (/api/enchanted-link/update-user-phone-enchanted-link-sms) Use to update phone and validate via enchanted link using SMS # Verify Token (/api/enchanted-link/verify-token) ### Verify Enchanted Link token from user Verify that the enchanted link token in the URL clicked by the end user matches and has not expired. This endpoint completes the enchanted link flow for: * sign up * [Sign-Up via email](/api/enchanted-link/sign-up) * sign-in * [Sign-In via email](/api/enchanted-link/sign-in) * sign-in with auto sign-up * [Sign-In with Auto Sign-up via email](/api/enchanted-link/sign-in-auto-sign-up) * Update data * [update email](/api/enchanted-link/update-email) ### Next Steps Poll for the successful completion of the token verification using the [Poll Session](/api/enchanted-link/poll-session) endpoint, providing the `pendingRef` returned by the this endpoint. The response object will be empty when this endpoint completes successfully. The session information will be returned by the the [Poll Session](/api/enchanted-link/poll-session) endpoint. ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Get project federated CM configuration (/api/get-fed-cm-config/get-fed-cm-config) Get project federated CM configuration # Magic Link Authentication API Overview (/api/magic-link) Use the Descope REST API to build magic link authentication for your application. # Magic Link APIs ## Overview The Magic Link APIs enable users to sign in by clicking a link delivered to their email or phone. Read about Magic Link implementation types [here](/auth-methods/magic-link/with-sdks/client#introduction). This authentication method has two phases: 1. Initiate the process and send the magic link — this is triggered from the application sign-in or sign-up screen. 2. Verify the token and return the session and refresh tokens (signed-in user). ## Use Cases 1. [Sign up](/api/magic-link/email/sign-up) a new user 2. [Sign in](/api/magic-link/email/sign-in) an existing user 3. [Sign in with auto sign-up](/api/magic-link/email/sign-in-auto-sign-up) if the user does not exist 4. [Update user's email address](/api/magic-link/email/update-email) 5. Update user's phone number ([SMS](/api/magic-link/sms)) ## Examples ### Example - sign up over email 1. Trigger the sign-up process with the [Sign-Up](/api/magic-link/email/sign-up) endpoint. On success, a magic link with a token is generated and delivered to the user's email. 2. When the user clicks the magic link, call the [Verify Magic Link](/api/magic-link/verification/verify-token) endpoint. Once the token is validated, the endpoint returns the session and refresh tokens. # OIDC Authorize Entra MFA (/api/federated-apps/oidc-auth-z-endpoint-entra-mfa) OIDC Entra MFA authorization endpoint # OIDC Finish Authorize (/api/federated-apps/oidc-auth-z-endpoint-finish-get) OIDC GET authorization endpoint finish # OIDC Finish Authorize (/api/federated-apps/oidc-auth-z-endpoint-finish-post) OIDC POST authorization endpoint finish # OIDC Authorize (/api/federated-apps/oidc-auth-z-endpoint-get-start) OIDC GET authorization endpoint start # OIDC Authorize (/api/federated-apps/oidc-auth-z-endpoint-post-start) OIDC POST authorization endpoint start # OIDC Device (/api/federated-apps/oidc-device-endpoint) OIDC device endpoint (sso app) # OIDC End Session (/api/federated-apps/oidc-end-session-endpoint-get) OIDC end session GET endpoint # OIDC End Session (/api/federated-apps/oidc-end-session-endpoint-post) OIDC end session POST endpoint # OIDC Introspect (/api/federated-apps/oidc-introspection-endpoint) OIDC token introspection endpoint (RFC 7662) # OIDC Authorize (/api/federated-apps/oidc-project-id-auth-z-endpoint-get-start) OIDC GET authorization endpoint start (by projectId, for an imported client_id) # OIDC Authorize (/api/federated-apps/oidc-project-id-auth-z-endpoint-post-start) OIDC POST authorization endpoint start (by projectId, for an imported client_id) # OIDC Device (/api/federated-apps/oidc-project-id-device-endpoint) OIDC device endpoint (by projectId, for an imported client_id) # OIDC End Session (/api/federated-apps/oidc-project-id-end-session-endpoint-get) OIDC end session GET endpoint (by projectId, for an imported client_id) # OIDC End Session (/api/federated-apps/oidc-project-id-end-session-endpoint-post) OIDC end session POST endpoint (by projectId, for an imported client_id) # OIDC Token (/api/federated-apps/oidc-project-id-token-endpoint) OIDC token endpoint (by projectId, for an imported client_id) # OIDC Revoke (/api/federated-apps/oidc-revocation-endpoint) OIDC revoke endpoint # OIDC Token (/api/federated-apps/oidc-token-endpoint) OIDC token endpoint # OIDC UserInfo (/api/federated-apps/oidc-user-info-endpoint-get) OIDC Get UserInfo endpoint # OIDC UserInfo (/api/federated-apps/oidc-user-info-endpoint-post) OIDC POST UserInfo endpoint # OIDC PAR (/api/federated-apps/oidcpar-endpoint) Pushed Authorization Request endpoint for federated OIDC apps (RFC 9126). # OIDC Authorize Entra MFA (/api/federated-apps/oidcsso-app-auth-z-endpoint-entra-mfa) OIDC Entra MFA authorization endpoint (SSO App) # OIDC Authorize (/api/federated-apps/oidcsso-app-auth-z-endpoint-get-start) OIDC GET authorization endpoint start (sso app) # OIDC Authorize (/api/federated-apps/oidcsso-app-auth-z-endpoint-post-start) OIDC POST authorization endpoint start (sso app) # OIDC End Session (/api/federated-apps/oidcsso-app-end-session-endpoint-get) OIDC end session GET endpoint (sso app) # OIDC Introspect (/api/federated-apps/oidcsso-app-introspection-endpoint) OIDC token introspection endpoint (RFC 7662, sso app) # OIDC PAR (SSO app) (/api/federated-apps/oidcsso-app-par-endpoint) Pushed Authorization Request endpoint for a federated OIDC SSO application (RFC 9126). # OIDC Revoke (/api/federated-apps/oidcsso-app-revocation-endpoint) OIDC revoke endpoint (sso app) # OIDC Token (/api/federated-apps/oidcsso-app-token-endpoint) OIDC token endpoint (sso app) # OIDC UserInfo (/api/federated-apps/oidcsso-app-user-info-endpoint-get) OIDC Get UserInfo endpoint (sso app) # OIDC UserInfo (/api/federated-apps/oidcsso-app-user-info-endpoint-post) OIDC POST UserInfo endpoint (sso app) # OIDC End Session (/api/federated-apps/oidsso-app-c-end-session-endpoint-post) OIDC end session POST endpoint (sso app) # SAML IDP Finish (/api/federated-apps/samlidp-finish-endpoint) SAML IDP finish endpoint # SAML IDP Initiate POST (/api/federated-apps/samlidp-initiate-http-post-binding) SAML IDP Initiate HTTP POST binding login flow # SAML IDP Initiate Redirect (/api/federated-apps/samlidp-initiate-http-redirect-binding) SAML IDP Initiate HTTP redirect binding login flow # SAML IDP POST Binding (/api/federated-apps/samlidphttp-post-binding) SAML IDP HTTP POST binding login flow # SAML IDP Redirect Binding (/api/federated-apps/samlidphttp-redirect-binding) SAML IDP http redirect binding login flow # WS-Fed IDP Finish (/api/federated-apps/ws-fed-idp-finish-endpoint) WS-Fed IDP finish endpoint after authentication # WS-Fed IDP Initiate (/api/federated-apps/ws-fed-idp-initiate-get) WS-Fed IDP-initiated sign-in (GET) # WS-Fed IDP Initiate (/api/federated-apps/ws-fed-idp-initiate-post) WS-Fed IDP-initiated sign-in (POST) # WS-Fed IDP Passive (/api/federated-apps/ws-fed-idp-passive-get) WS-Fed IDP passive sign-in endpoint (GET) # WS-Fed IDP Passive (/api/federated-apps/ws-fed-idp-passive-post) WS-Fed IDP passive sign-in endpoint (POST) # nOTP Authentication API Overview (/api/notp) Use the Descope REST API to build an authentication process that relies on nOTP and WhatsApp. # nOTP APIs ## Overview The nOTP (no-tee-pee) APIs require two phases. The first phase calls the API endpoint to initiate the process (sign-up, sign-in, etc.), and the second phase presents the user with the QR code received from the first step. On success, the first phase returns a QR code image and a redirect URL in the response. Either display the QR code to the user or redirect them to the URL. Once the user scans the code or is redirected, WhatsApp opens with an auto-filled message containing a token. After the user sends the message, the `pendingRef` from the initial API call can be used to retrieve the session. ## Use Cases 1. [Sign up](/api/notp/sign-up) a new user 2. [Sign in](/api/notp/sign-in) an existing user 3. [Sign in with auto sign-up](/api/notp/sign-in-auto-sign-up) if the user does not exist 4. [Get Pending Session](/api/notp/pending-session) ## Examples ### Example - user sign-up with QR code 1. Trigger the process with the [Sign-Up](/api/notp/sign-up) endpoint. On success, the response body includes an `image` property (the QR code) and a `pendingRef` property. 2. Display the image to the user. Once they scan it and send the pre-filled message, they are authenticated. 3. Verify the user's state with the [Get nOTP Pending Session](/api/notp/pending-session) endpoint, using the `pendingRef` from step 1. ### Example - user sign-in with redirect URL 1. Trigger the process with the [Sign-In](/api/notp/sign-in) endpoint. On success, the response body includes a `redirectUrl` and a `pendingRef` property. 2. Redirect the user to the provided URL. Once they send the pre-filled message, they are authenticated. 3. Verify the user's state with the [Get nOTP Pending Session](/api/notp/pending-session) endpoint, using the `pendingRef` from step 1. These examples apply to all sign-up, sign-in, and sign-in with auto sign-up use cases when using the relevant API endpoints. # Get NoTP Pending Session (/api/notp/pending-session) Get a session that was generated by NOTP Sign in / Sign up request, and verified with Verify request # Sign-In with Auto Sign-Up (/api/notp/sign-in-auto-sign-up) Login in using NTOP. If the user does not exist, a new user will be created with the given identifier # Sign-In (/api/notp/sign-in) Login a user using NOTP # Sign-Up (/api/notp/sign-up) Create a new user using NOTP # Update User NOTP (/api/notp/update-user-notp) Update user phone using NOTP # Start connecting an outbound OAuth application (/api/o-auth/o-auth-app-connect) Start connecting an outbound OAuth application on behalf of the authenticated user # Exchange SSO Code (/api/oauth/exchange-code) ### Exchange SSO SAML code for Descope user session This endpoint will exchange the unique SAML code (also called a token) for the Descope session information needed for managing the end user session. Call this endpoint from your code flow that responds to the `url` that was returned by the [Sign-In](/api/oauth/sign-up-sign-in) endpoint. The unique code `` is appended as a URL parameter: `code=`, for example, `url = https://sso.mycompany.com/mywork.htm?code=`. ### Next Steps 1. Extract the unique code `` from the URL parameter. 2. Call this endpoint, passing the `` as the request parameter The response object includes the session JWT (sessionJwt) and refresh JWT (refreshJwt) when this endpoint completes successfully. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # OAuth Social Login API Overview (/api/oauth) Use the Descope REST API to build OAuth social logins for your application. # Social Login (OAuth) APIs Use the Social Login (OAuth) APIs to let your end users sign in to your app or website using their existing social network credentials, for example Google or Facebook. ## Overview Implementing OAuth using the APIs is a two-step process: 1. **Authorize** — Call the [Sign-Up / Sign-In](/api/oauth/sign-up-sign-in) endpoint to authorize your end user to sign up or sign in using their OAuth credentials. On success, you receive a unique code to exchange for a user object in the next step. This endpoint handles both sign-up and sign-in. 2. **Exchange Code** — Call the [Exchange Code](/api/oauth/exchange-code) endpoint to exchange the unique code for a user object, which includes the session JWT `sessionJwt` and refresh JWT `refreshJwt`. ## Implementing OAuth ### Authorize sign-up/sign-in Call the [Sign-Up / Sign-In](/api/oauth/sign-up-sign-in) endpoint to authorize your end user to sign up or sign in using their social network credentials. Descope walks the end user through the OAuth provider's authorization and sign-in process. The endpoint takes two arguments: - `provider` — name of the auth provider (for example, `google` or `facebook`). For a list of all supported options, see [OAuth Providers](/customize/auth/oauth#oauth-providers). - `redirectURL` — destination URL the end user session is redirected to after successfully signing in. The API response includes `url`, which the user should be redirected to in order to authenticate with the service provider. Once completed, the user is redirected to the `redirectURL` with a unique code `` appended as a URL parameter. Note that `redirectURL` is optional. If omitted, the project setting applies. If provided, it must be part of the `Approved Domains` configured in the project settings. ### Exchange Code In your source code that responds to the user session being redirected to `redirectUrl`, exchange the unique code `` from the URL parameter for a Descope user object. 1. Extract the unique code `` from the URL parameter. 2. Call the [Exchange Code](/api/oauth/exchange-code) endpoint. The endpoint response returns a valid [User Object](/api/overview#the-user-object), which includes the session JWT `sessionJwt` and refresh JWT `refreshJwt`. # Finishes a full OAuth flow using native APIs (/api/oauth/o-auth-native-finish) Finishes a full OAuth flow using native APIs # Starts a full OAuth flow using native APIs (/api/oauth/o-auth-native-start) Starts a full OAuth flow using native APIs # Create Redirect URI for Sign-In Request (/api/oauth/redirect-sign-in) ### Create an OAuth Redirect URI for user Sign-In Request This endpoint allows you to create an OAuth Redirect URI for user Sign-Up Request. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Create Redirect URI for Sign-Up Request (/api/oauth/redirect-sign-up) ### Create an OAuth Redirect URI for user Sign-In Request This endpoint allows you to create an OAuth Redirect URI for user Sign-In Request. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Creating OAuth redirect URI for update user request (/api/oauth/redirect-update-user) Creating OAuth redirect URI for update user request # Sign-Up / Sign-In (/api/oauth/sign-up-sign-in) ### Authorize end user to sign-up or sign-in using social login credentials Initiate a social login (OAuth) sign-up or sign-in process for an end user. Descope will coordinate the authorization process with the OAUth provider specified in the `provider` field. Specify the URL you want to redirect the end user to after a successful sign-in in the `redirectURL` parameter. When the OAuth authorization completes successfully, the endpoint returns a URL `url` that has a unique code `` appended as a URL parameter to the `redirectURL` you provided. For example, if `redirectURL = https://oauth.mycompany.com/shopping.htm` then `url = https://oauth.mycompany.com/shopping.htm?code=`. The unique code will be exchanged for a valid user object in the next step. After the end user successfully authenticates with the OAuth provider the end user session is redirected to `url`. ### Next Steps Call the [Exchange Code](/api/oauth/exchange-code) endpoint from the flow that responds to the URL specified in the `redirectURL` field, to exchange the unique code for a user session object. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on the stepup, mfa, and customClaims parameters. # Exchanges one tap id token for a JWT (/api/onetap/exchange-one-tap-id-token) Exchanges one tap id token for a JWT # Get Google One Tap Client ID Configuration (/api/onetap/get-one-tap-client-id) Get the client ID configuration for Google One Tap integration # Verifies one tap id token for a code (/api/onetap/verify-one-tap-id-token) Verifies one tap id token for a code # OTP Authentication API Overview (/api/otp) Use the Descope REST API to build one-time password (OTP) authentication for your application. # One-Time-Password APIs ## Overview The One-Time-Password APIs require two phases. The first phase calls the API endpoint to initiate the process (sign-up, sign-in, etc.), and the second phase verifies the OTP code received by the user. Each first-phase endpoint, on success, delivers a code to the user via email, voice call, or text message (SMS). Your app should then wait for the user to supply the code and call the verify endpoint (second phase) to confirm it and complete the action. The OTP code can be delivered over email, voice call, or text message (SMS) — each has its own set of API endpoints. ## Use Cases 1. Sign up a new user ([Email](/api/otp/email), [SMS](/api/otp/sms), [Phone](/api/otp/phone)) 2. Sign in an existing user 3. Sign in with auto sign-up if the user does not exist 4. Update user's email address 5. Update user's phone number ## Examples ### Example - user sign-up over email 1. Trigger the process with the [Sign-Up](/api/otp/email/sign-up) endpoint. On success, the OTP code is delivered to the user's email. 2. Complete the process by verifying the code with the [Verify OTP Code](/api/otp/email/verify-otp) endpoint. Once confirmed, the endpoint returns the user's session and refresh JWTs. This example also applies to the "Sign in an existing user" and "Sign in with auto sign-up" use cases, and to the text message (SMS) channel, when using the relevant API endpoints. ### Example - update user's phone number 1. Trigger the process with the [Update Phone Number](/api/otp/sms/update-phone) endpoint. On success, the OTP is delivered to the new phone number. 2. Complete the process by verifying the code with the Verify Code API endpoint. Once confirmed, the user's phone number is updated, and all future OTP codes over SMS are delivered to the new number. This example also applies to the "Update user's email address" use case, when using the relevant API endpoints. # User Sign-In (IM) (/api/otp/sign-in-otp-instant-message) Login a user based using an instant message (IM) to the given phone number # User Sign-In with Auto Sign-Up (IM) (/api/otp/sign-up-or-in-otp-instant-message) Login a user based using an instant message to the given phone number. If the user does not exist, a new user will be created with the given phone number # User Sign-Up (IM) (/api/otp/sign-up-otp-instant-message) Create a new user using an instant message # Update Phone (IM) (/api/otp/update-user-phone-otpim) Update phone, and verify via OTP based on an instant message # Verify Code (IM) (/api/otp/verify-code-im) Verify a Sign in / Sign up based on an instant message # Creating SAML redirect URI (/api/saml/create-saml-redirect) Creating SAML redirect URI # Finalize SAML authentication (/api/saml/exchange-token) Finalize SAML authentication # IDP Metadata URL for external SAML services (/api/saml/samlidp-metadata) IDP Metadata URL for external SAML services # Password Authentication API Overview (/api/passwords) Use the Descope REST API to build password authentication for your application. # Password APIs ## Overview The Password APIs handle sign-up and sign-in in a single step. On successful authentication, the user's JWT is returned. Password resets require email verification — the reset endpoint only succeeds if the user has a validated email address. ## Use Cases 1. [Sign Up](/api/passwords/sign-up) a new user via password authentication 2. [Sign In](/api/passwords/sign-in) an existing user via password authentication 3. Initiate a [Password Reset](/api/passwords/email) 4. [Replace](/api/passwords/replace-password) an existing user's password 5. [Update](/api/passwords/update-password) an existing user's password ## Examples ### Example - user sign-up via password 1. Call the [Sign-Up](/api/passwords/sign-up) API endpoint. On success, the user's JWT is returned. ### Example - reset user's password 1. Call the [Reset Password](/api/passwords/email/password-reset) API endpoint. 2. The user receives a password reset email. Verify the user after they complete the reset via [Verify Magic Link](/api/magic-link/verification/verify-token). ### Example - update user's password 1. Use the user's refresh token to update their password via [Update Password](/api/passwords/update-password). # Get Password Policy (/api/passwords/password-policy) ### Get the configured password policy for the project. ### See Also - See [Password Policy Customization](/auth-methods/passwords#password-policy) for further details on password policy configuration. # Replace Password (/api/passwords/replace-password) ### Replace the user's password of an existing user utilizing the password API. ### Next Steps Sign the user in with their new password via [Sign-In](/api/passwords/sign-in) ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - You can also utilize [Update Password](/api/passwords/update-password) or [Reset Password](/api/passwords/email/password-reset) as alternatives to change a user's password. # Sign-In User (/api/passwords/sign-in) ### Sign-In an existing user utilizing password authentication. This endpoint will return the user's JWT. ### Next Steps Verify the user's email to allow for password reset by updating the email via [OTP](/api/otp/email/update-email), [Enchanted Link](/api/enchanted-link/update-email), or [Magic Link](/api/magic-link/email/update-email) Add tenants to the user via [Update User Add Tenant](/api/management/users/update-user-add-tenant) Add roles to the user via [Update User Add Role](/api/management/users/update-user-add-roles) ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-Up](/api/passwords/sign-up) endpoint to sign-up a new end user. # Sign-Up User (/api/passwords/sign-up) ### Sign-Up a new user utilizing password authentication. This endpoint will return the user's JWT. ### Next Steps Verify the user's email to allow for password reset by updating the email via [OTP](/api/otp/email/update-email), [Enchanted Link](/api/enchanted-link/update-email), or [Magic Link](/api/magic-link/email/update-email) Add tenants to the user via [Update User Add Tenant](/api/management/users/update-user-add-tenant) Add roles to the user via [Update User Add Role](/api/management/users/update-user-add-roles) ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-In](/api/passwords/sign-in) endpoint to sign-in an existing end user. # Update Password (/api/passwords/update-password) ### Update the user's password of an existing user utilizing the password API. ### Next Steps Sign the user in with their new password via [Sign-In](/api/passwords/sign-in) ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - You can also utilize [Replace Password](/api/passwords/replace-password) or [Reset Password](/api/passwords/email/password-reset) as alternatives to change a user's password. # Select an active tenant (/api/session/active-tenant) ### Set the active tenant for the user's current session This endpoint allows you to get a new session token and refresh token with the `dct` claim on the JWT which shows the active selected tenant for the user. See [Tenant Selection Article](/knowledgebase/descopeflows/tenantselectcomponent/) for more details of the usage. # Token Validation Key (V2) (/api/session/get-keys-v2) ### Get public key for session token validation (V2) This API endpoint will return the public key needed to handle the session token JWT validation. `projectId` is provided as a GET parameter, so this endpoint can be executed with a browser. This endpoint differentiates from [Token Validation Key (V1)](/api/session/get-keys) as the data is returned in JSON format rather than an array. # Token Validation Key (V1) (/api/session/get-keys) ### Get public key for session token validation (V1) This API endpoint will return the public key needed to handle the session token JWT validation. `projectId` is provided as a GET parameter, so this endpoint can be executed with a browser. # Logout (/api/session/idpsso-logout-get) IDP SSO Logout from the session and delete the session and refresh cookies # Logout (/api/session/idpsso-logout-post) IDP SSO Logout from the session and delete the session and refresh cookies # My Details (/api/session/my-details) ### Get current signed-in user details This API Endpoint will return the current user's details. This endpoint requires the user to be signed in and have a valid `refreshJwt`. The `refreshJwt` is then used as part of the Authorization Bearer to perform this task. # Refresh Session (/api/session/refresh-session) ### Refresh the session token, using a valid fresh token This API endpoint will provide a new valid session token for an existing signed-in user, by validating the provided refresh token. The refresh token is provided as part of the HTTP Authorization Bearer. # Get Session History (/api/session/session-history) ### Get user's session history This API Endpoint will return the current user's session history including geo-location and IP address. This endpoint requires the user to be signed in and have a valid `refreshJwt`. The `refreshJwt` is then used as part of the Authorization Bearer to perform this task. # Sign-Out All Active Sessions (/api/session/sign-out-all-devices) ### Log the user out from all signed-in sessions This API endpoint will sign the user out of all the devices they are currently signed-in with. Successfully executing this endpoint will invalidate all user's refresh tokens. Response will include all user tokens and fields empty, so client will remove cookies as well. # Sign-Out (/api/session/sign-out) ### Log the user out from the provided session This API endpoint will sign the user out of the provided session using the `refreshToken`. Successfully executing this endpoint will invalidate the provided refresh tokens. Response will also include all user tokens and fields empty, so the executing client will remove cookies as well. # Try Refresh Session (/api/session/try-refresh-session) Refresh the current session if it is valid, will not fail if the refresh token is missing or invalid # Validate Session (/api/session/validate-session) ### Validate and parse a user's session JWT. This endpoint is used to validate a users session using the Project ID and the user's session JWT. Upon successful validate of the user, you will receive the parsed JWT. When posting to this endpoint from an application, you get the JWT from local or cookie storage, and prepend it with project ID and use that as the bearer. # Exchange SSO Code (/api/sso/exchange-code) ### Exchange SSO SAML code for Descope user session This endpoint will exchange the unique SAML code (also called a token) for the Descope session information needed for managing the end user session. Call this endpoint from your code flow that responds to the `url` that was returned by the [Sign-In](/api/oauth/sign-up-sign-in) endpoint. The unique code `` is appended as a URL parameter: `code=`, for example, `url = https://sso.mycompany.com/mywork.htm?code=`. ### Next Steps 1. Extract the unique code `` from the URL parameter. 2. Call this endpoint, passing the `` as the request parameter The response object includes the session JWT (sessionJwt) and refresh JWT (refreshJwt) when this endpoint completes successfully. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # SAML SSO API Overview (/api/sso) Use the Descope REST API to add SAML single sign-on (SSO) for your application. # SSO (Single Sign-On) APIs Use the SSO (Single Sign-On) APIs to let end users sign in to all authorized applications using the same credentials. ## Overview Implementing SSO using the APIs is a two-step process: 1. **Sign-In** — Call the [Start SSO](/api/sso/start-sso) endpoint to authenticate your end user. On success, you receive a unique code to exchange for a user object in the next step. Access authorization to a specific service provider application can be managed in either of the following ways: * (recommended) Map and manage end user access using [Descope roles and groups](https://app.descope.com/tenants/) for a specific tenant * Manage access manually using the roles and access mechanism provided by each individual service provider 2. **Exchange Code** — Call the [Exchange SSO Code](/api/sso/exchange-code) endpoint to exchange the unique code for a user object, which includes the session JWT `sessionJwt` and refresh JWT `refreshJwt`. ## Implementing SSO ### Sign-in Call the [Start SSO](/api/sso/start-sso) endpoint to authenticate your end user. Descope walks the end user through the SSO sign-in process. The endpoint takes two arguments: - `tenant` — the specific tenant for which to sign in the end user - `redirectURL` — destination URL the end user session is redirected to after successfully signing in The API response returns `url`, which is the `redirectURL` with a unique code (``) appended as a URL parameter. For example, if `redirectURL = https://sso.mycompany.com/mywork.htm` then `url = https://sso.mycompany.com/mywork.htm?code=`. After the end user has been successfully authenticated with the identity provider (IdP), the end user session is redirected to `url`. ### Exchange Code In your source code that responds to the user session being redirected to `url`, exchange the unique code (``) from the URL parameter for a Descope user object. 1. Extract the unique code (``) from the URL parameter. 2. Call the [Exchange SSO Code](/api/sso/exchange-code) endpoint. The endpoint response returns a valid [User Object](/api/overview#the-user-object), which includes the session JWT `sessionJwt` and refresh JWT `refreshJwt`. # Start SSO (/api/sso/start-sso) ### Authorize end user to sign-in using SAML SSO Initiate a SAML SSO (Single Sign-On, "sign-in" in Descope terminology) process for an end user. Descope will coordinate the sign-in process with the service provider. Specify the URL you want to redirect the end user to after a successful sign-in in the `redirectURL` parameter. When the SSO sign-in completes successfully, the endpoint returns a URL `url` that has a unique code ``, also called a token) appended as a URL parameter to the `redirectURL` you provided. For example, if `redirectURL = https://sso.mycompany.com/mywork.htm` then `url = https://sso.mycompany.com/mywork.htm?code=`. The unique code will be exchanged for a valid user object in the next step. After the end user has been successfully authenticated with the identity provider (IdP) the end user session is redirected to `url`. ### Next Steps Call the [Exchange Code](/api/sso/exchange-code) endpoint from the flow that responds to the URL specified in the `redirectURL` field, to exchange the unique code for a user session object. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on the stepup, mfa, and customClaims parameters. # Finish Authorization Endpoint (/api/third-party-apps/authorization-finish) Third Party application authorization finish endpoint # OAuth 2.0 authorize endpoint (GET) (/api/third-party-apps/authorization-get) Start the authorization code flow for an [Inbound App](/identity-federation/inbound-apps). Redirect the user-agent to this endpoint with `client_id`, `redirect_uri`, `response_type=code`, `scope`, `state`, and PKCE parameters. See [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server) for the full flow. # OAuth 2.0 authorize endpoint (POST) (/api/third-party-apps/authorization-post) Start authorization with a JSON request body (non-browser clients). Same semantics as the GET endpoint. See [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server). # Inbound Apps (OAuth) API Overview (/api/third-party-apps) REST API reference for Descope's OAuth 2.0 authorization server used by Inbound Apps — authorize, token, revoke, and userinfo endpoints. # Inbound Apps (OAuth) APIs These endpoints implement Descope's **OAuth 2.0 / OpenID Connect authorization server** for [Inbound Apps](/identity-federation/inbound-apps). Third-party applications, backend services, and agentic clients use them to obtain access tokens scoped to your [Resources](/resources). For conceptual guidance, grant types, and example requests, see [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server). ## Endpoints | Route | Methods | Description | | ----- | ------- | ----------- | | `/oauth2/v1/apps/authorize` | `GET`, `POST` | Start user authentication and consent ([GET](/api/third-party-apps/authorization-get), [POST](/api/third-party-apps/authorization-post)) | | `/oauth2/v1/apps/token` | `POST` | Issue, refresh, and exchange tokens ([Token endpoint](/api/third-party-apps/token-endpoint)) | | `/oauth2/v1/apps/revoke` | `POST` | Revoke tokens ([Revoke](/api/third-party-apps/revoke-token)) | | `/oauth2/v1/apps/userinfo` | `GET`, `POST` | Read token claims ([GET](/api/third-party-apps/user-info-get), [POST](/api/third-party-apps/user-info-post)) | Base URL: `https://api.descope.com` (or your [custom domain](/how-to-deploy-to-production/custom-domain#configure-custom-domain)). ## Configure Inbound Apps Use the [Management API](/api/management/third-party-apps) to create Inbound Apps, rotate secrets, and manage consents programmatically. For [agentic identities](/agentic-identity-hub/core-components/agents) (MCP server authorizations), use the dedicated [Agentic Identity Management API](/api/management/agentic-identity-hub/search-agentic-identities) to search and revoke access. # OIDC revoke endpoint (/api/third-party-apps/revoke-token) OIDC revoke endpoint # OAuth 2.0 token endpoint (Inbound Apps) (/api/third-party-apps/token-endpoint) Exchange authorization codes, refresh tokens, client credentials, JWT bearer assertions, and RFC 8693 token-exchange requests for [Inbound App](/identity-federation/inbound-apps) access tokens. Supported `grant_type` values and examples are documented in [Authorization server endpoints](/identity-federation/inbound-apps/authorization-server) and [Using Inbound Apps](/identity-federation/inbound-apps/using-inbound-apps). # Third Party application Get UserInfo endpoint (/api/third-party-apps/user-info-get) Third Party application Get UserInfo endpoint # Third Party application Post UserInfo endpoint (/api/third-party-apps/user-info-post) Third Party application Post UserInfo endpoint # Add / Update Key (/api/totp/add-update-key) ### Add or update TOTP key for existing end user Initiate a flow to add TOTP functionality for an existing end user, or to update the TOTP key for an existing end user. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full Add / Update TOTP flow has successfully completed. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. If the end user is not yet registered use the [Sign-Up](/api/totp/sign-up) endpoint to register the user. ### Next Steps 1. Display the TOTP key to the end user so the key can be entered into the authenticator app. Use any of the following methods to display the key to your end user: * (recommended) Redirect the end user session to the `provisioningURL` returned in the response body. The URL displays the key as a QR code that can be scanned directly from the authenticator app. * Render the QR code using your own web page using the `image` (the QR code as Base64) returned in the response body. * If your end user cannot scan a QR code, present the `key` returned in the response body so the key can be pasted into their authenticator app. If the authenticator app prompts, the end user must select key type: "time based". 2. Prompt the end user user for a TOTP code generated by their authenticator app. 3. Verify the TOTP code using the [Sign-In / Verify](/api/totp/sign-in-verify) endpoint to complete the Add / Update process. After successfully verifying the TOTP code the new TOTP key will be used to validate future TOTP code. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # TOTP API Overview (/api/totp) Use the Descope REST API to add TOTP authenticator apps to your application. # Authenticator App (TOTP) ## Overview The Authenticator App (TOTP) APIs enable users to sign in with an authenticator app. To get started, provision the user with a secret seed — either by signing up a new user or updating an existing user. To sign up a new user or add an authenticator app to an existing user, call the [Sign-Up](/api/totp/sign-up) or Add/Update API endpoints. These endpoints generate a seed for the user to configure in their authenticator app. Once the user has configured the app, call the [Sign In / Verify](/api/totp/sign-in-verify) endpoint to confirm the TOTP code and complete setup. Once a seed is configured, users can sign in with the code (TOTP) generated by their authenticator app. Use the [Sign In / Verify](/api/totp/sign-in-verify) endpoint with the user's `loginId` and the generated code. ## Use Cases 1. [Sign up](/api/totp/sign-up) a new user 2. [Add or update](/api/totp/add-update-key) an authenticator app for an existing user 3. [Sign in / Verify](/api/totp/sign-in-verify) a user ## Examples ### Example - user sign-up 1. Trigger the process with the [Sign-Up](/api/totp/sign-up) endpoint. This returns the authenticator app secret seed (as a code or a BASE64-encoded image). 2. Complete the process by verifying the code with the [Sign In / Verify](/api/totp/sign-in-verify) endpoint. Once confirmed, the user is signed up with the authenticator app, and the endpoint returns the user's session and refresh JWTs. This example also applies to the "Add or update an authenticator app for an existing user" use case. ### Example - sign in a user Call the [Sign In / Verify](/api/totp/sign-in-verify) endpoint with the user's `loginId` and the TOTP code from their authenticator app. The endpoint returns the session and refresh JWTs. # Sign-In / Verify (/api/totp/sign-in-verify) ### Verify the TOTP of an end user Verify the TOTP code of an end user. This endpoint is the final API call for the following TOTP flows: * Sign-In - If the end user is already registered, this end-point is the only call you need to sign-in that user. * Sign-Up - If you are implementing a sign-up flow, this endpoint will verify the TOTP code and complete the sign-up process * Add/ Update - If you are implementing an Add / Update flow, this endpoint completes the process of adding/updating the TOTP key for that user. The response object includes the session JWT `sessionJwt` and refresh JWT `refreshJwt` when the endpoint completes successfully, and the end user will be signed in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. # Sign-Up (/api/totp/sign-up) ### Sign-up new end user via TOTP Initiate a TOTP sign-up process for a new end user. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full TOTP sign-up flow has successfully completed. If the end user is already registered use the [add/update](/api/totp/add-update-key) endpoint to add TOTP funtionality to an existing end user, to prevent the same person being registered twice. ### Next Steps 1. Display the TOTP key so it can be entered into their authenticator app. The TOTP key is returned in the response object in three ways, to ensure it can easily be entered into the end user's authenticator app. 2. Prompt the end user user for a TOTP code generated by their authenticator app. 3. Verify the TOTP code using the [Sign-In / Verify](/api/totp/sign-in-verify) endpoint to complete the sign-in process. After successfully verifying the TOTP code the new end user will be registered using the details you provided in the body of this endpoint. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [add/update](/api/totp/add-update-key) endpoint to add TOTP funtionality to an existing end user. # Finalize Add WebAuthn (/api/web-authn/web-authn-device-add-finish) Finalize adding a new WebAuthn device # Add WebAuthn Device (/api/web-authn/web-authn-device-add-start) Add a new WebAuthn device to an existing user # User Sign-In with Auto Sign-Up (/api/web-authn/web-authn-sign-up-in-start) Use to login with WebAuthn, if user doesn't exist a new user will be created # Finalize Sign-In (/api/web-authn/web-authn-signin-finish) Finalize a WebAuthn signin operation # User Sign-In (/api/web-authn/web-authn-signin-start) Login an existing user with WebAuthn # Finalize Sign-Up (/api/web-authn/web-authn-signup-finish) Finalize a WebAuthn signup operation # User Sign-Up (/api/web-authn/web-authn-signup-start) Create a new user using WebAuthn # WS-Fed IDP Metadata (/api/ws-fed/ws-fed-idp-metadata) WS-Federation metadata endpoint for external RP services # Custom Policies (/migrate/azure-ad-b2c/b2c-flows-migration) This guide covers how to migrate your Azure AD B2C custom policies to Descope flows. # Migrating Custom Policies Azure AD B2C custom policies are XML-based configuration files that define authentication journeys (**User Journeys**) as a sequence of **OrchestrationSteps**, each invoking a **TechnicalProfile** (e.g. collect input, call REST API, issue token). Data is held in **claims**; **ClaimsTransformations** manipulate them; **Preconditions** on steps control branching. ## Why Migrate to Descope Flows In Descope, you build the same journeys with our [Flows](/flows), with: no `XML`, no `ClaimsSchema` or `ContentDefinitions`. You can branch logic using `Condition` blocks instead of `Preconditions`, and token issuance is automatic at flow end. Scriptlets and the [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) can cover custom logic and REST calls. Below we break down a B2C flow step by step and show how each part maps to Descope — so you can translate your own policies the same way. ## Example: Sign-in with optional MFA In this B2C flow, a user signs in with email and password. If the user has an MFA phone number registered, the flow will require SMS OTP; otherwise it will skip MFA. Then it will issue a JWT with `email` and `name`. ### B2C Custom Policy Example The journey is defined in XML. Claims (e.g. `email`, `password`, `strongAuthenticationPhoneNumber`) are declared elsewhere in the policy; steps run in order, with `Preconditions` controlling whether MFA runs. ```xml strongAuthenticationPhoneNumber SkipThisOrchestrationStep ``` Supporting pieces live in other parts of the same (or extended) policy: - **ClaimsSchema** for `email`, `password`, `strongAuthenticationPhoneNumber`, `name` - **TechnicalProfiles** for `SelfAsserted-LocalAccountSignin`, `PhoneFactor-Verify`, and `JwtIssuer-IssueToken` - **ContentDefinitions** for the HTML of each page - **RelyingParty** to select this journey and list **OutputClaims** for the token. ### How it Maps to Descope Flows The same flow can be built with Descope Flows according to the following steps: 1. `Sign Up or In / Password` action — replaces the Self-Asserted local account sign-in TechnicalProfile. User enters email and password; Descope validates and loads the user (and any stored attributes, e.g. phone for MFA). 2. `Condition` block — check whether the user has a phone number (e.g. `user.phone` or a custom attribute). One branch goes to MFA, the other skips it (same idea as the B2C Precondition). 3. `Sign In OTP / SMS` action (on the "has phone" branch) — replaces the `PhoneFactor-Verify` TechnicalProfile. Send and verify the code. 4. `Custom Claims` action — add `email` and `name` (or any flow context values) to the session JWT, analogous to RelyingParty OutputClaims. 5. `End` action — Descope issues the session token automatically; no separate JWT `TechnicalProfile` is needed. No XML, no ClaimsSchema, and no ContentDefinitions. Sequencing is the visual order of blocks; the condition replaces Preconditions. ![An example of how the B2C custom policy maps to Descope flows](/assets/b2c-flows-migration-example.webp) # From Azure AD B2C (/migrate/azure-ad-b2c) This guide covers how to migrate your Azure AD B2C users to Descope. # Azure AD B2C Migration Guide If you want to keep Azure AD B2C as the main identity layer and use Descope for authentication (e.g. passkeys or modern methods), you can configure Descope as an OpenID Connect (OIDC) [identity provider](/identity-federation/applications/setup-guides/azure-ad-b2c-oidc) in B2C. You can migrate from [Azure AD B2C](https://learn.microsoft.com/en-us/azure/active-directory-b2c/overview) to Descope in two main ways: - **[Full Migration](#full-migration)** (export users and import into Descope) - **[JIT Migration](#jit-migration)** (provision users on sign-in). You can also use [SSO migration](#sso-migration) for seamless migration of your SSO connections. ## Azure AD B2C and Descope Terminology The table below shows how authentication and authorization concepts in Azure AD B2C map to Descope. | Azure AD B2C | Descope | |--------------|---------| | **B2C Tenant** | [Project](https://app.descope.com/settings/project) - your Descope project and configuration boundary. | | **User** (consumer/customer identity) | [User](https://app.descope.com/users) - identity record with login IDs, profile, and custom attributes. | | **User flow** or **Custom policy** | [Flow](/flows) - the sign-up/sign-in or authentication journey you design in Descope. | | **Identity provider** (local account, social, or external IdP in B2C) | [Custom OAuth Providers](/auth-methods/oauth/providers) or [Tenant-based SSO](/auth-methods/sso). | | **Application** (enterprise app registration in B2C) | Your Descope [Inbound Application](/identity-federation/inbound-apps). | | **User attributes** | [User custom attributes](https://app.descope.com/users/attributes) and built-in user fields (email, name, phone, etc.). | | **Application attributes** | [Inbound application custom attributes](https://app.descope.com/apps/inbound/attributes). | | **Conditional Access** / **MFA** in B2C | [Flow steps](/flows) - conditions, MFA (e.g. TOTP, passkeys), and branching in a Descope flow. | Azure AD B2C has no built-in concept of [tenants](/b2b#how-multi-tenancy-works) or [roles](/authorization/role-based-access-control). You have one B2C directory; enterprise IdPs (SAML/OIDC) are just external identity providers in that directory, and any role-like behavior must be implemented manually using custom attributes and claims. Advanced flows (home realm discovery, custom branding, REST API calls, multi-IdP orchestration) use [custom policies](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview). If you use [Descope Tenants](/b2b#how-multi-tenancy-works) or [Descope Roles](/authorization), you are introducing those models yourself — there is nothing in B2C to map 1:1. You will have to decide how to translate your setup (e.g. one Descope tenant per enterprise IdP, B2C custom attributes mapped to Descope roles, etc). ## Full Migration Azure AD B2C does not allow the export of user password hashes. This means a full migration is always a **without-passwords** migration. In a full migration you move your users and identity data to Descope and eventually run only on Descope. After importing users into Descope, you have two options. You can either transition users to **passwordless** authentication methods (magic link, OTP, passkeys), or require users to **reset their password** on first sign-in (e.g. using a `freshlyMigrated` flag to trigger a password-reset step in your flow). If preserving the existing password experience is critical, consider [JIT migration with a Generic HTTP Connector](/migrate/azure-ad-b2c#using-the-generic-http-connector) instead. After a full migration, you can use [session migration](#session-migration) so users with an active B2C session don't have to sign in again. ### Getting the Existing User Data To get the existing user data, you can either: - [Export Users from the Microsoft Graph API](#exporting-users-from-the-microsoft-graph-api) - List users via the Microsoft Graph API, map attributes, then import into Descope. - [Connect your existing data store](#connecting-an-existing-data-store-to-descope) - If B2C uses custom policies and an external user store (e.g. REST API or database), connect that same store to Descope and then remove B2C from the path. #### Exporting Users from the Microsoft Graph API Use the [Microsoft Graph API](https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-operations) to list and export users. **Prerequisites** - Access to your [Azure AD B2C tenant](https://portal.azure.com/) with permissions to manage applications and read users. - An [app registration](https://learn.microsoft.com/en-us/azure/active-directory-b2c/microsoft-graph-get-app) in the B2C tenant (or the tenant that manages B2C) with Microsoft Graph **application** permissions: `User.Read.All` or `Directory.Read.All`, and admin consent granted. Create a client secret or certificate for server-side authentication. - Your [Descope project ID](https://app.descope.com/settings/project) and a [Descope Management Key](https://app.descope.com/settings/company/managementkeys). **Step 1: Authenticate with Microsoft Graph** Obtain an access token using the [OAuth 2.0 client credentials flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-client-creds-grant-flow). Make a `POST` request to: ``` POST https://login.microsoftonline.com/{your-b2c-tenant-id}/oauth2/v2.0/token Content-Type: application/x-www-form-urlencoded client_id={app-client-id} &scope=https://graph.microsoft.com/.default &client_secret={app-client-secret} &grant_type=client_credentials ``` The response includes an `access_token` you will use in the `Authorization: Bearer` header for all Graph API calls. **Step 2: Export users from Azure AD B2C** Call the [List users](https://learn.microsoft.com/en-us/graph/api/user-list) endpoint with `$select` to retrieve only the fields you need. For B2C users, the `identities` collection is critical because it contains the sign-in identifiers (email, username, or federated identity). ``` GET https://graph.microsoft.com/v1.0/users ?$select=id,identities,displayName,givenName,surname,mail,otherMails,mobilePhone,createdDateTime &$top=999 Authorization: Bearer {access_token} ``` Key details about this API: - **Pagination**: The response includes an `@odata.nextLink` URL when more results are available. Follow this URL to retrieve the next page. Continue until no `@odata.nextLink` is returned. - **Rate limiting**: Microsoft Graph enforces [throttling limits](https://learn.microsoft.com/en-us/graph/throttling). If you receive a `429 Too Many Requests` response, respect the `Retry-After` header and implement exponential backoff. - **The `identities` collection**: Each B2C user has an `identities` array containing objects with `signInType` (e.g. `emailAddress`, `userName`, `federated`), `issuer`, and `issuerAssignedId`. Use this to determine the user's sign-in identifier. - **Extension attributes**: If you defined custom user attributes via B2C extension attributes, they appear as properties like `extension_{app-id-no-hyphens}_{attribute-name}`. You must include these in `$select` explicitly. Example response (abbreviated): ```json { "@odata.nextLink": "https://graph.microsoft.com/v1.0/users?$skiptoken=...", "value": [ { "id": "a1b2c3d4-...", "displayName": "Jane Doe", "givenName": "Jane", "surname": "Doe", "mail": "jane@example.com", "identities": [ { "signInType": "emailAddress", "issuer": "yourb2ctenant.onmicrosoft.com", "issuerAssignedId": "jane@example.com" } ] } ] } ``` If you need to export users in bulk for very large directories (hundreds of thousands of users), you can also use the [Export Personal Data](https://learn.microsoft.com/en-us/graph/api/user-exportpersonaldata) API or the [Microsoft Graph Data Connect](https://learn.microsoft.com/en-us/graph/data-connect-concept-overview) service for higher throughput, though the paginated `List users` approach works well for most B2C migrations. **Step 3: Map B2C Attributes to Descope** For each user, map the exported fields: - B2C `identities[].issuerAssignedId` (where `signInType` is `emailAddress` or `userName`) → Descope `loginIds` (required; unique per user). - B2C `displayName` → Descope `name`. - B2C `givenName` → Descope `givenName`. - B2C `surname` → Descope `familyName`. - B2C `mail` or `otherMails[0]` → Descope `email`. - B2C `mobilePhone` → Descope `phone`. - Extension attributes or custom claims → Descope [custom attributes](https://app.descope.com/users/attributes) (create the corresponding custom attribute definitions in Descope first). Build a JSON array in the format expected by Descope (see [user format guide](/migrate/custom/user-format-json)). **Step 4: Import Users into Descope** Use the Descope Management API to import users: - **Single user**: [Create User](/api/management/users/create-user) — `POST /v1/mgmt/user/create` - **Batch**: [Batch Create Users](/api/management/users/batch-create-users) — `POST /v1/mgmt/user/create/batch` (see [rate limits](/rate-limiting#specific-user-management-paths) here) When importing, you'll need to make sure all `loginId` values you import are unique per user. Optionally set a custom attribute `freshlyMigrated` to `true` for post-migration flows (see [Post-Migration Verification](#post-migration-verification)). You can also set `verifiedEmail` and `verifiedPhone` to `true` if these were already verified in B2C, so users are not asked to re-verify. Once the users are imported, you can verify them in the [Descope Users](https://app.descope.com/users) list and test sign-in with a few migrated users (remember: passwords were not migrated, so test with passwordless methods or password reset). #### Connecting an Existing Data Store to Descope If you use Azure AD B2C [custom policies](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview) with an **external user store** (REST API, SQL, or other database), your users and data may already live in that store. You do not need to export from Microsoft Graph. 1. **Connect the same store to Descope** - Use an [HTTP Generic Connector](/connectors/connector-configuration-guides/network/generic-http) in your Descope flow to call your store's API or endpoints for authentication and user lookup. Map the response to Descope users and sessions. 2. **Sever the connection to B2C** - Once Descope is successfully authenticating against your store, remove or disable B2C from the path. Your identity data stays in your store; only the connection moves from B2C to Descope. ### Session Migration [Session Migration](/migrate/session-migration) is used after you have completed a **full migration**: all users are already in Descope (exported from B2C, with attributes mapped and imported). Session migration then lets you move their **active session** to Descope so they don't have to sign in again. - **Requires full migration first** - Users must already exist in Descope. Attributes are mapped during that import. - **Then migrate the session** - Deploy a new version of your web or mobile app that sends the user's existing Azure AD B2C session token to Descope. Descope validates the token, finds the matching user (already in Descope), and issues a Descope token. See the [Session Migration](/migrate/session-migration) guide for setup, prerequisites, and SDK usage. Your backend should [support both token types](#session-validation-strategy) during the transition so users with B2C sessions can be migrated gradually. ## JIT Migration In order to use JIT migration, you must keep B2C running until all active users have signed in at least once. With **Just-In-Time (JIT) migration**, you do not bulk-export users. Users are provisioned in Descope **when they sign in**. This approach lets you migrate users gradually without downtime and without needing to coordinate a bulk import. Azure AD B2C does not expose user password hashes, so your JIT architecture depends on whether you want to **preserve the password sign-in experience** or **transition to passwordless**. The two main approaches are described below. ### Using the Generic HTTP Connector Using the Generic HTTP Connector (ROPC flow method) does not support MFA or federated IdP sign-in in B2C. Use this approach if you want to **keep passwords working** during migration. On first sign-in, Descope collects the user's email and password, sends them to Azure AD B2C for verification, and—if successful—creates the user in Descope and sets their password in Descope so future sign-ins go directly through Descope. **How it works:** 1. The user enters their email and password in your Descope flow. 2. A [Generic HTTP Connector](/connectors/connector-configuration-guides/network/generic-http) in the flow sends a request to Azure AD B2C to verify the credentials. This can be done through one of two B2C mechanisms: - **Resource Owner Password Credentials (ROPC) flow**: If you have an [ROPC user flow or policy](https://learn.microsoft.com/en-us/azure/active-directory-b2c/add-ropc-policy) configured in B2C, the connector calls the B2C token endpoint (`POST https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{policy}/oauth2/v2.0/token`) with `grant_type=password`, `username`, `password`, `client_id`, and `scope`. - **Custom policy with a REST API validation step**: If you have a B2C custom policy that accepts credentials via a REST API technical profile, the connector calls that endpoint. 3. If B2C returns a success (valid credentials), the Descope flow proceeds to create or update the user in Descope (via the [Create User](/api/management/users/create-user) Management API action or an inline flow action), **sets the user's password in Descope**, and issues a Descope session token. 4. On subsequent sign-ins, the user authenticates directly against Descope — no further calls to B2C are needed. To set the password in step 3, use **Sign Up / Password** for users new to Descope, or **Update Password** for users who already exist there. The second case applies if your flow signs them in with a magic link or OTP before storing the password. Both actions accept the password from either the **Login Password** or the **New Password** [screen component](/flows/screens/inputs/passwords#using-it-to-set-a-password). Prefer **Login Password**, since the user is entering a password they already have and does not need the confirmation field or policy previewer. Whichever component collects it, the password must satisfy your project's [password policy](/auth-methods/passwords/settings). ### Using Azure AD B2C as a Custom OIDC Provider Use this approach if you are transitioning to **passwordless** authentication or are okay with requiring users to reset their password on first sign-in. You configure your Azure AD B2C tenant as a [custom OAuth provider](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider) in Descope, so users authenticate through the standard B2C sign-in experience and are provisioned in Descope automatically on first sign-in. You'll need to make sure there is an [application](https://learn.microsoft.com/en-us/azure/active-directory-b2c/app-registrations) in B2C that Descope can use to sign in users with. If using an existing B2C application, make sure you add the Descope authorized callback URL in the application's settings. **How it works:** 1. **Configure B2C in Descope** - In Descope, add Azure AD B2C as a [custom OIDC provider](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider). - Use your Azure AD B2C app client ID and secret, and set the endpoints (replace `{tenant}` with your B2C tenant name and `{policy}` with your user flow or custom policy name). 2. **Add OIDC sign-in to your flow** - In your Descope flow, add a [Sign Up or In / OAuth](/auth-methods/oauth#flow-actions) or `SSO` step in your flow that redirects users to Azure AD B2C. They will then sign in through B2C as usual (including MFA, social, or custom policy logic). 3. **User is provisioned in Descope** - After B2C authenticates the user, it returns an ID token with claims (email, name, custom attributes). Descope provisions the user and maps those claims to Descope user attributes, then issues a Descope session token. Once a user has been provisioned in Descope, subsequent sign-ins can go directly through Descope, controlled via a [condition](/flows/conditions) in your flow. ![An example of using the freshlyMigrated attribute within a Descope flow conditional](/assets/descope-auth0-migration-guide-example.webp) ## B2C Flows Migration You cannot migrate [passkeys](/auth-methods/passkeys) or [TOTP](/auth-methods/auth-apps) seeds from Azure AD B2C to Descope. Users will need to **reprovision** them in Descope (e.g. enroll a new passkey or set up TOTP again in your Descope flow after migration). For detailed examples of how Azure AD B2C custom policies (user journeys, orchestration steps, technical profiles) map to Descope Flows, see [B2C flows migration](/migrate/azure-ad-b2c/b2c-flows-migration). ## SSO migration If your Azure AD B2C setup includes SSO (SAML/OIDC) for organizations or partners, you can migrate those configurations to Descope without forcing admins to re-configure their IdPs. Descope can consume the existing IdP response and complete authentication so end users keep a seamless experience. For the full process—implementing SSO with Descope, setting up tenants, DNS redirect, and testing—see [SSO Migration](/migrate/sso). ## Session Validation Strategy Whether you do a [full migration](#full-migration) or a [JIT migration](#jit-migration), plan for a period when some users have Descope sessions and others still have B2C sessions. Your backend should support **both** token types during the transition: - **Validate Descope session tokens (JWTs)** for users who have already migrated or signed in via Descope. - **Validate Azure AD B2C tokens** for users who still have active B2C sessions. Inspect the token (e.g. issuer or `kid`) to determine the provider, then validate accordingly. This lets you roll out the migration gradually. For the implementation pattern, see the docs on backend session validation [here](/migrate/session-migration#step-1-dual-token-validation-in-your-backend). # From Custom Data Store (/migrate/custom) Discover how to efficiently migrate your users from any custom data store to Descope with our detailed guide and user management API. # Custom Data Store Migrating your users from a custom data store to Descope can be straightforward with the right preparation and tools. This guide will cover everything you need to know about exporting your user data and importing it into Descope using our User Management API. ## Preparing for Migration ### Exporting User Data Start by ensuring your user data is accessible for import. This often means exporting your data to a format like CSV or JSON. The key information you'll need includes: - **Login ID** (Required): The unique identifier for authentication. - **Email**: Optional, but recommended for communication. - **First and Last Name**: Optional, for personalization. - **Password**: Optional, for importing existing password hashes. ### Verifying Data Accessibility Ensure you can programmatically access the exported data, either directly from your data source or from the exported file. ## Creating Users To learn more about how to properly format these users when importing them into Descope, check out our [guide](/migrate/custom/user-format-json) on JSON formatting for user details. Utilize the Descope [Create User API](/api/management/users/create-user) or [Batch Create User API](/api/management/users/batch-create-users) to import each user. This will create a matching record for each user within Descope. A successful response will include the creation fields, along with a new `userId`, which you can persist in your data store if desired. ```json { "user": { "loginIds": [ "asdfafw3e" ], "userId": "U2beSnHc1GhgIw7TscMfj5ASG", "name": "", "email": "test10@gmail.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false, "roleNames": [], "userTenants": [], "status": "invited", "externalIds": [ "asdfafw3e" ], "picture": "", "test": false, "customAttributes": {}, "createdTime": 1706574216, "TOTP": false, "SAML": false, "OAuth": {}, "webauthn": false, "password": true, "ssoAppIds": [], "givenName": "", "middleName": "", "familyName": "" } } ``` ### Importing Passwords If your application users currently have password-based authentication, you can use our Management APIs to migrate over their pre-existing passwords, in either plaintext or hashed form. When using the [Create User API](/api/management/users/create-user), Descope will create a hash comparison based on your hash, salt, and iterations. Hashed passwords can be migrated to Descope if they are using `bcrypt`, `argon2`, `django`, `firebase`, `pbkdf2`, or `sha`. This is what the password section of the JSON will look like in the API request body: ```json "password": "string", // or "hashedPassword": { // Whatever algorithm that you're currently using, code snippets for specific algorithms are documented below }, ``` By importing passwords this way, users will be able to sign in with their existing password without having to go through a password reset flow. A cleartext `password` on any user caps a [Batch Create Users](/api/management/users/batch-create-users) request at 100 users. Batches using only `hashedPassword` carry no such cap, so you can migrate hashed-password users at any batch size. #### Password Algorithms Descope supports migrating hashed passwords using several algorithms. Here’s a quick list of each supported algorithm with links to the full details below: 1. **[Bcrypt](#bcrypt)** - Secure, adaptive hashing algorithm with increasing iterations. 2. **[Argon2](#argon2)** - Modern hashing algorithm with a memory cost that's resistant to GPU attacks. 3. **[Django](#django)** - Designed for Django applications with built-in security. 4. **[Firebase](#firebase)** - Specific hashing configuration for Firebase Authentication. 5. **[PBKDF2](#pbkdf2)** - Key derivation function suitable for security-focused applications. 6. **[PHPass](#phpass)** - A framework for PHP applications using multiple hashing methods. 7. **[MD5](#md5)** - Legacy algorithm, supported for backward compatibility. 8. **[SHA-1, SHA2-256, SHA2-512, SHA3-256, SHA3-512](#sha)** - Standard widely used cryptographic hash algorithms. 8. **[Buddy-Auth (Bcrypt+SHA-512)](#buddy-auth)** - Secure, adaptive bcrypt algorithm but standardizes the input. Let's explore the details and use-cases for each supported algorithm: #### 1. Bcrypt - **Use Case**: Commonly used for secure password hashing. It's resilient against brute-force attacks due to its adaptive nature, which allows the number of iterations (work factor) to be increased as hardware becomes more powerful. - **Parameters**: - `hash`: The bcrypt hash string. ```json "hashedPassword": { "bcrypt": { "hash": "string" }, } ``` #### 2. Argon2 - **Use Case**: Commonly used for secure password hashing in new systems and frameworks. It's resilient against brute-force attacks, similar to bcrypt, but also provides resistance against GPU attacks by limiting parallelism. - **Parameters**: - `hash`: The hash value of the password, base64-encoded. - `salt`: Salt value used along with the hash, base64-encoded. - `iterations`: The number of passes that's used to tune the running time independently of the memory size. - `memory`: Specifies the number of KBs of memory used to compute the hash. - `threads`: The degree of parallelism that determines how many independent computational lanes can be run. - `type` (optional): The Argon2 variant used to produce the hash, either `argon2id` or `argon2i`. Defaults to `argon2id` if omitted. ```json "hashedPassword": { "argon2": { "hash": "base64 string", "salt": "base64 string", "iterations": 8, "memory": 8192, "threads": 1, "type": "argon2id" } }, ``` If your existing hashes were produced with Argon2i instead of Argon2id, set `type` to `"argon2i"`. If this field is left out for an Argon2i hash, Descope will attempt Argon2id verification instead, and the user's login will fail with a generic "Password signin failed" error (E062903). This looks identical to an incorrect password, so double-check `type` first if imported users can't sign in. The `type` field is currently only available by calling the Management API directly. ##### Converting a PHC format Argon2 hash If your existing system stores hashes in the standard Argon2 PHC string format, for example: ``` $argon2i$v=19$m=64000,t=20,p=2$D5kRHi3TkX5+ZxTX5B8EAg$Ed4sqVR4g9klZR0Mg6wNY4++0iOjOCt3s32ZS1UPHJ4 ``` Before sending the hash to Descope, split the PHC-formatted string into its component fields (`type`, `memory`, `iterations`, `threads`, `salt`, `hash`) so that each maps to the corresponding Descope parameter. | PHC segment | Maps to | | --- | --- | | `argon2i` | `type` | | `m=64000` | `memory` | | `t=20` | `iterations` | | `p=2` | `threads` | | `D5kRHi3TkX5+ZxTX5B8EAg` | `salt` | | `Ed4sqVR4g9klZR0Mg6wNY4++0iOjOCt3s32ZS1UPHJ4` | `hash` | The salt and hash segments in a PHC string use `base64` without padding. Descope's API expects standard `base64`, so you may need to re-pad these values before sending them. #### 3. Django - **Use Case**: Designed for Django applications, this option allows for seamless migration from Django's authentication system to Descope. - **Parameters**: - `hash`: The entire hash string as stored in the Django application. Django typically combines the algorithm, iterations, salt, and hash in one string. ```json JSON "hashedPassword": { "django": { "hash": "string" } }, ``` #### 4. Firebase - **Use Case**: Suitable for migrating users from Firebase authentication, accommodating its specific hashing configurations. - **Parameters**: - `hash`: The hash value of the password. - `salt`: Salt value used along with the hash. - `saltSeparator`: A separator used in Firebase's hashing strategy to distinguish between salt and password. - `signerKey`: Key used for additional security during the hashing process. - `memory`: Specifies the amount of memory used for hashing. - `rounds`: The number of hashing iterations. ```json "hashedPassword": { "firebase": { "hash": "base64 string", "salt": "base64 string", "saltSeparator": "base64 string", "signerKey": "base64 string", "memory": 14, "rounds": 8 } }, ``` #### 5. PBKDF2 - **Use Case**: A widely used algorithm that applies a pseudorandom function to derive keys. The number of iterations can be configured to increase the difficulty of deriving keys, enhancing security. - **Parameters**: - `hash`: The hash value of the password. - `salt`: Salt value used along with the hash. - `iterations`: The number of iterations the algorithm runs, increasing the time it takes to compute the hash. - `type`: The type of pseudorandom function used: `sha1`, `sha256`, or `sha512`. ```json "hashedPassword": { "pbkdf2": { "hash": "base64 string", "salt": "base64 string", "iterations": 4000, "type": "sha256" } }, ``` #### 6. PHPass - **Use Case**: PHPass (PHP Password Hashing Framework) is a portable password hashing framework for PHP applications. It is designed to be secure, using multiple hashing algorithms and iterating over the password to increase security against brute-force attacks. - **Parameters**: - `hash`: The hashed value of the password. - `salt`: Salt value used along with the hash. - `iterations`: The number of iterations the algorithm runs, increasing the time it takes to compute the hash. - `type`: The hash format (md5, sha512) ```json "hashedPassword": { "phpass": { "hash": "string", "salt": "string", "iterations": 10000, "type": "sha512" } } ``` #### 7. MD5 We currently do not support salted MD5 passwords. - **Use Case**: MD5 (Message-Digest Algorithm) is a widely known cryptographic hash function that was once commonly used to hash passwords. However, due to vulnerabilities, it is no longer considered secure for password hashing. While it is generally discouraged for use in new applications, MD5 might still be found in legacy systems. Descope supports migration of MD5-hashed passwords to allow seamless user transitions during system upgrades. - **Parameters**: - `hash`: The hashed value of the password. ```json "hashedPassword": { "md5": { "hash": "string" } } ``` #### 8. SHA-1, SHA2-256, SHA2-512, SHA3-256, SHA3-512 - **Use Case**: The SHA family is among the most widely used cryptographic hash algorithms. They are used across many online systems and security protocols. While SHA-1 is considered deprecated and discouraged, it is still supported by Descope for legacy systems. It is recommended to upgrade to SHA2 or SHA3. - **Parameters**: - `hash`: The hash value of the password. - `type`: The type of pseudorandom function used: (e.g. `sha1`, `sha2-256`, `sha2-512`, `sha3-256`, `sha3-512`). - `salt`: Salt value used along with the hash. - `saltPosition` (optional): Position of the salt relative to the password when computing the digest: 'prefix' for SHA(salt + password) or 'suffix' for SHA(password + salt). Defaults to 'suffix' when omitted. ```json "hashedPassword": { "sha": { "hash": "base64 string", "type": "string", "salt": "string", "saltPosition": "string" } } ``` The salt position matters when computing the hash. By default, Descope assumes the salt is appended to the password ('suffix'). If your original system prepended the salt instead, set `saltPosition` to 'prefix' so the digests match. #### 9. Buddy-Auth (Bcrypt+SHA512) - **Use Case**: Buddy-auth has the benefits of bcrypt but avoids the input length limitations of it by preprocessing the password with a SHA-512 hash. - **Parameters**: - `hash`: The hash value of the password. Has the form of `bcrypt+sha512$(salt)$(iterations)$(hash)`. ```json "hashedPassword": { "buddyauth": { "hash": "bcrypt+sha512$base64 string$12$base64 string" } } ``` ### Importing TOTP Secrets If your users authenticate with an authenticator app (TOTP/2FA), you can import their existing TOTP secrets at the same time as their other credentials. Include the `seed` field on each user object in the batch create request — this is the base32-encoded secret your current system uses to generate one-time codes. ```json { "users": [ { "loginId": "user@example.com", "email": "user@example.com", "verifiedEmail": true, "seed": "JBSWY3DPEHPK3PXP" } ] } ``` You can set `seed` and `hashedPassword` on the same user object to migrate both at once: ```json { "users": [ { "loginId": "user@example.com", "email": "user@example.com", "verifiedEmail": true, "seed": "JBSWY3DPEHPK3PXP", "hashedPassword": { "bcrypt": { "hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy" } } } ] } ``` After import, users can sign in with their existing authenticator app immediately — they do not need to re-enroll or scan a new QR code. For users who already exist in Descope and need TOTP added or replaced, use the [Add / Update Key](/api/totp/add-update-key) endpoint. This generates a new TOTP seed for the user, which they set up in their authenticator app by scanning the returned QR code. ### Preserving Creation Dates By default, Descope stamps a batch-imported user's `createdTime` with the moment of import. Long-time customers end up looking like brand-new signups. To preserve each user's original signup date, set `createdTime` on their user object in a Batch Create Users request ([API Reference](/api/management/users/batch-create-users) and [SDK Reference](/management/user-management/sdks#batch-create-users)). Use a Unix timestamp in seconds. It must be non-negative and can't fall in the future. ```json { "users": [ { "loginId": "user@example.com", "email": "user@example.com", "verifiedEmail": true, "createdTime": 1609459200 } ] } ``` `createdTime` is only available on the [Batch Create Users API](/api/management/users/batch-create-users), not the single Create User API endpoint/SDK function. ### Migration Strategy - **Assessment**: Review the current hashing algorithms and parameters used in your existing system. - **Compatibility Check**: Ensure the new system supports all features of the old system's hashing mechanisms. - **User Transparency**: Migrate hashes without requiring users to reset their passwords, ensuring a seamless transition. By understanding these parameters and their implications, you can plan an effective password migration strategy that maintains security standards and minimizes user disruption. This approach ensures that the new system respects existing security measures while potentially enhancing them. #### Triggering password resets If user passwords can't be exported, we can trigger password resets. This can happen at any time, not necessarily when the import is happening. It could simply be part of a login flow-after a user inputs their email, they get a password reset message to their inbox. #### Social Login After migrating user Login IDs, social login will work just as previously, no other importing required. ## Handling user signups during migrations You'll want to consider scheduling the migration to occur at a time where user activity is minimal. One option is to disable sign up while the migration is occurring. This way all users will be captured. To prevent downtime, you can implement user syncing between Descope and your future sign ups. This way, you can simply migrate the previous users and new users who sign up during the migration will already exist. This adds complexity into the process as you'll have to account for edge cases: What if an existing user updates their email, password, etc. during the migration? ## Example Check out an example migration script [here](https://github.com/descope-sample-apps/user-migration-django-passwords). # User JSON Formatting (/migrate/custom/user-format-json) Learn how to import users from a JSON file into Descope's User Management portal. # Import Users from JSON File If you're just beginning to implement Descope into your projects, you may want to migrate all of your existing users to the User Management portal in the [Descope Console](https://app.descope.com/users). This is a guide that shows you how to adapt your current user base to a JSON file in the correct format, and then upload them through the User Management Portal. ## Formatting User Data JSON 1. If your user data is in a CSV, you will need to convert the data into a JSON. There are many free online tools that will allow you to do this, such as [csvjson.com](https://csvjson.com/csv2json). 2. You will need to make sure your user data contains the correct headers and is organized in the correct manner. Every user in your JSON will must have an **identifier** and a **displayName**, along with either an email or phone number. The identifier can either be the email or the phone number depending on which one you decide to add. If your users have phone and email data, then your JSON should look something like this: If you are adding users with just email data, the JSON should look like this: ```json [ { "identifier": "example@descope.com", "displayName": "Test User", "email": "example@descope.com" } ] ``` If you only have phone data, the JSON should look like this instead: ```json [ { "identifier": "+14151234567", "displayName": "Test User", "phoneNumber": "+14151234567" } ] ``` If you want to add both an email or phone number, along with custom attributes and other parameters, this is what a fully loaded JSON will look like (you can just remove the key/value pairs you do not have data for): ```json [ { "identifier": "example@descope.com", "displayName": "Test User", "email": "example@descope.com", "phoneNumber": "+14151234567", "verifiedEmail": true, "verifiedPhone": false, "authorization": { "roles": [ {"id": "xxxx"} ] }, "tenants": [], "status": "enabled", "test": false, "customAttributes": { "custom_att": "sample_value" } }, { "identifier": "example1@descope.com", "displayName": "Test User 1", "email": "example1@descope.com" }, ... ] ``` If you would like to include custom attributes to your user, as I have done in the JSON above, you will need to make sure they are first created with the same key [here](https://app.descope.com/users/attributes). If you have not added they attribute key, the user will still be added to the main list, but will not be assigned the custom attribute value. 4. Once you have created the file, all you will have to do is head to the [Users Management](https://app.descope.com/users) page and select `Import Users` in the top right corner. ![Descope import user guide - import users button](/assets/import-users-button.webp) Once the import is complete, refresh the page and you should see all of your users there. Now you can go ahead and manage them either from within the Descope Console or with any of our SDKs and APIs. If you have any other questions about Descope or adding users, feel free to reach out to [us](/support)! # Overview (/sessions/management) Manage Descope session tokens on web and mobile — storage, refresh, logout, and sending tokens to your backend. # Session Management After a user signs in, your **web or mobile app** holds the session and refresh tokens. The Client or Mobile SDK stores them securely, refreshes the session token when it expires, and attaches the session token to requests your app sends to your backend. Session management on the client is **not** authorization. Your backend still [validates](/sessions/validation) every session token before serving protected resources. | Platform | What the SDK handles | Guide | | --- | --- | --- | | **Web** | Read tokens from the SDK, send them to your API, logout, optional UI checks (expiry, roles for display) | | | **Mobile** | Secure storage (Keychain / EncryptedSharedPreferences), session manager, refresh, logout, attach token to API calls | | Web and mobile SDKs may check expiry or read roles for UI purposes, but that is not a substitute for [server-side validation](/sessions/validation). Never rely on client-side role or permission checks to protect data. For more information on the session and refresh token model, see the [Sessions](/sessions) docs. # Mobile Sessions (/sessions/management/mobile) How Descope Mobile SDKs store, refresh, and manage session tokens on iOS, Android, Flutter, and React Native. # Mobile Sessions Descope Mobile SDKs (Swift, Kotlin, Flutter, React Native) manage the full session lifecycle on the device: secure token storage, automatic refresh, logout, and attaching the session token to backend requests. Like web apps, **mobile clients do not authorize API access** — they hold tokens and send them to your server, which [validates](/sessions/validation/backend) them. The SDK handles: - **Secure storage** — Keychain (iOS) and EncryptedSharedPreferences (Android) - **Session manager** — persist sessions after sign-in and refresh before expiry - **Outgoing requests** — attach the session token as a bearer header - **Logout** — revoke the refresh token and clear local storage See [Sessions](/sessions) for the session and refresh token model. ## How Token Storage Works with Mobile SDKs With Descope's Mobile SDKs at your disposal, you can bypass the need to manually save tokens on the device. The SDKs, designed to cater to the unique needs of each platform, handle token storage and retrieval in an unobtrusive and secure manner in the background. ### Swift Descope's Swift SDK uses iOS's `Keychain Services` to store, retrieve and manage the session and refresh tokens. `Keychain Services` provide a secure, encrypted storage mechanism for sensitive user information, including Descope tokens. ### Kotlin Descope's Kotlin SDK employs Android's native `EncryptedSharedPreferences` to securely store session and refresh tokens locally. The `EncryptedSharedPreferences` provide a robust, encrypted storage environment to protect sensitive user information, such as Descope tokens. ### Flutter Descope's Flutter SDK stores session and refresh tokens using `Keychain Services` on iOS and `EncryptedSharedPreferences` on Android. ### React Native Descope's React Native SDK stores session and refresh tokens using `Keychain Services` on iOS and `EncryptedSharedPreferences` on Android. When using React Native, you can access session information outside of the component render-lifecycle using the `getCurrentSessionToken()`, `getCurrentRefreshToken()`, and `getCurrentUser()` helper functions. ## Using our SDKs ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ### Sending session token to application server If you are using a Mobile SDK or Descope Flows, send the session token to your application server on each API call. Your server [validates](/sessions/validation/backend) the token — the mobile app does not. ```swift let sessionToken = Descope.sessionManager.session // example fetch call with authentication header fetch('your_application_server_url', { headers: { Accept: 'application/json', Authorization: 'Bearer '+ sessionToken, } }) ``` ```kotlin val session = Descope.sessionManager.session // example fetch call with authentication header fetch('your_application_server_url', { headers: { Accept: 'application/json', Authorization: 'Bearer '+ sessionToken, } }) ``` ```dart var url = Uri.parse('https://example.com/api/resource'); // Create an HTTP request var request = http.Request('GET', url); // Use the extension method to set the Authorization header from the session manager await Descope.sessionManager.refreshSessionIfNeeded(); final sessionJwt = Descope.sessionManager.session?.sessionJwt; if (sessionJwt != null) { request.headers['X-Auth-Token'] = sessionJwt; } else { // unauthorized return; } // Send the request var response = await http.Response.fromStream(await request.send()); // Handle the response if (response.statusCode == 200) { print('Successful response: ${response.body}'); } else { print('Failed response: ${response.statusCode}'); } ``` ``` js const sessionToken = getCurrentSessionToken(); const res = await fetch('/path/to/server/api', { headers: { Authorization: `Bearer ${sessionToken}`, }, }) ``` ### Logout using Mobile SDK If you are integrating using Descope Mobile SDKs, then you must use the Mobile SDK to logout. If you are using Descope Flows with React SDK, refer to the [Quick Start](/getting-started) for details. If you are using Descope Mobile SDKs without flows, then refer to the sample code below for logout. ```swift guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } try await Descope.auth.logout(refreshJwt: refreshJwt) Descope.sessionManager.clearSession() ``` ```kotlin Descope.sessionManager.session?.refreshJwt?.run { Descope.auth.logout(this) Descope.sessionManager.clearSession() } ``` ```dart final refreshJwt = Descope.sessionManager.session?.refreshJwt; if (refreshJwt != null) { Descope.auth.logout(refreshJwt); Descope.sessionManager.clearSession(); } ``` ```js import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { session, clearSession } = useSession() const refreshToken = getCurrentSessionToken(); await descope.logout(refreshToken) await clearSession(resp.data) ``` ### Checking token expiration (UI only) Check whether the refresh token has expired to decide when to show login vs main UI. This does not replace [backend validation](/sessions/validation/backend). ```swift if let session = Descope.sessionManager.session, session.refreshToken.isExpired { print("Session token has expired.") } else { print("Session token is valid.") } ``` ```kotlin if (Descope.sessionManager.session?.refreshToken?.isExpired == true) { println("Session token has expired.") } else { println("Session token is valid.") } ``` ```dart if (Descope.sessionManager.session?.refreshToken?.isExpired == true) { // Show main UI } else { // Show login UI } ``` ## Handling authorization (UI only) Read roles and permissions from the session token to drive in-app UI. Your backend must validate the token and enforce access on every API request. ### Roles ```swift let sessionToken = Descope.sessionManager.session let roles = sessionToken.roles() print("User Roles: \(roles)") ``` ```kotlin val roles = Descope.sessionManager.session?.roles() println("User Roles: ") println(roles) ``` ```dart final roles = Descope.sessionManager.session?.roles(); print("User Roles:"); print(roles); ``` ### Permissions ```swift let sessionToken = Descope.sessionManager.session let permissions = sessionToken.permissions() print("User Permissions: \(permissions)") ``` ```kotlin val permissions = Descope.sessionManager.session?.permissions() println("User Permissions: ") println(permissions) ``` ```dart final permissions = Descope.sessionManager.session?.permissions(); print("User Permissions:"); print(permissions); ``` ### Starting a managed session After a user finishes a sign in flow successfully, you should use the Descope session manager to manage the user's session. You can also create a `DescopeSession` object from the `AuthenticationResponse` value returned by all the authentication APIs. ```swift let authResponse = try await Descope.otp.verify(with: .email, loginId: "andy@example.com", code: "123456") let session = DescopeSession(from: authResponse) Descope.sessionManager.manageSession(session) ``` ```kotlin val authResponse = Descope.otp.verify(DeliverMethod.Email, "andy@example.com", "123456") val session = DescopeSession(authResponse) Descope.sessionManager.manageSession(session) ``` ```dart // if the user entered the right code the authentication is successful final authResponse = await Descope.otp.verify(method: DeliveryMethod.email, loginId: 'andy@example.com', code: "123456"); // we create a DescopeSession object that represents an authenticated user session final session = DescopeSession.fromAuthenticationResponse(authResponse); // the session manager automatically takes care of persisting the session // and refreshing it as needed Descope.sessionManager.manageSession(session); ``` ```javascript import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { manageSession } = useSession() const resp = await descope.otp.email.verify('andy@example.com', '123456') const session = DescopeSession(resp.data) manageSession(session) ``` ### Authenticate outgoing requests The session can then be used to authenticate outgoing requests to your backend with a bearer token authorization header. ```swift var request = URLRequest(url: url) request.setAuthorizationHTTPHeaderField(from: Descope.sessionManager) let (data, response) = try await URLSession.shared.data(for: request) ``` ```kotlin val connection = url.openConnection() as HttpsURLConnection connection.setAuthorization(Descope.sessionManager) ``` ### Using the session JWT directly If your backend uses a different authorization mechanism you can of course use the session JWT directly instead of the extension function: ```swift try await Descope.sessionManager.refreshSessionIfNeeded() guard let sessionJwt = Descope.sessionManager.session?.sessionJwt else { throw ServerError.unauthorized } request.setValue(sessionJwt, forHTTPHeaderField: "X-Auth-Token") ``` ```kotlin Descope.sessionManager.refreshSessionIfNeeded() Descope.sessionManager.session?.sessionJwt?.apply { connection.setRequestProperty("X-Auth-Token", this) } ?: throw ServerError.unauthorized ``` ```dart // Create a URL for the request var url = Uri.parse('https://example.com/api/resource'); // Create an HTTP request var request = http.Request('GET', url); // Use the extension method to set the Authorization header from the session manager request.setAuthorization(sessionManager); ``` ```javascript try { // refresh if needed await refreshSessionIfAboutToExpire() } catch (e) { // fail silently - as this shouldn't affect the request being performed } // add authorization header request.headers.Authorization = `Bearer ${session.sessionJwt()}` ``` ## Listening for Session Changes Add an event listener or delegate to the session manager to be notified whenever the session tokens are refreshed or the user's details change. ```swift class MySessionObserver: DescopeSessionManagerDelegate { func sessionManagerDidUpdateTokens(_ sessionManager: DescopeSessionManager, session: DescopeSession) { // Called after the session tokens are updated by a refresh or updateTokens() print("Session tokens updated: \(session.sessionJwt)") } func sessionManagerDidUpdateUser(_ sessionManager: DescopeSessionManager, session: DescopeSession) { // Called after Descope.sessionManager.updateUser(with:) updates the user // (not session.update(with:) below, which only updates the local session object) print("User details updated: \(session.user)") } } let observer = MySessionObserver() Descope.sessionManager.addDelegate(observer) // Remove the delegate when it's no longer needed Descope.sessionManager.removeDelegate(observer) ``` ```kotlin val listener = object : DescopeSessionManager.Listener { override fun onUpdateTokens(session: DescopeSession) { // Called after the session tokens are updated by a refresh or updateTokens() println("Session tokens updated: ${session.sessionJwt}") } override fun onUpdateUser(session: DescopeSession) { // Called after Descope.sessionManager.updateUser(...) updates the user // (not session.updateUser(...) below, which only updates the local session object) println("User details updated: ${session.user}") } } Descope.sessionManager.addListener(listener) // Remove the listener when it's no longer needed Descope.sessionManager.removeListener(listener) ``` These callbacks only trigger when an existing session is updated, such as during an automatic or manual token refresh, or when user details change. ## Descope User The ``DescopeUser`` struct represents an existing user in Descope. After a user is signed in with any authentication method the ``DescopeSession`` object keeps a ``DescopeUser`` value in its `user` property so the user's details are always available. In the example below we finalize an OTP authentication for the user by verifying the code. The authentication response has a `user` property which can be used directly or later on when it's kept in the ``DescopeSession``. ```swift let authResponse = try await Descope.otp.verify(with: .email, loginId: "andy@example.com", code: "123456") print("Finished OTP login for user: \(authResponse.user)") Descope.sessionManager.session = DescopeSession(from: authResponse) print("Created session for user \(descopeSession.user.userId)") ``` ```kotlin val authResponse = Descope.otp.verify(method = DeliveryMethod.Email, loginId = "andy@example.com", code = "123456") print("Finished OTP login for user: ${authResponse.user}") Descope.sessionManager.session = DescopeSession(authResponse) print("Created session for user ${descopeSession.user.userId}") ``` ```dart // if the user entered the right code the authentication is successful final authResponse = await Descope.otp.verify(method: DeliveryMethod.email, loginId: 'andy@example.com', code: "123456"); // we create a DescopeSession object that represents an authenticated user session final session = DescopeSession.fromAuthenticationResponse(authResponse); // the session manager automatically takes care of persisting the session // and refreshing it as needed Descope.sessionManager.manageSession(session); // the user is available on the session object print("Finished OTP login for user: ${session.user.userId}"); ``` ```javascript import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { manageSession } = useSession() const resp = await descope.otp.email.verify('andy@example.com', '123456') const session = DescopeSession(resp.data) manageSession(session) ``` The details for a signed in user can be updated manually by calling `auth.me` with the `refreshJwt` from the active ``DescopeSession``. If the operation is successful the call returns a new ``DescopeUser`` value. ```swift guard let session = Descope.sessionManager.session else { return } let descopeUser = try await Descope.auth.me(refreshJwt: session.refreshJwt) session.update(with: descopeUser) ``` ```kotlin val session = Descope.sessionManager.session ?: return val descopeUser = Descope.auth.me(refreshJwt = session.refreshJwt) session.updateUser(descopeUser) ``` ```dart final session = Descope.sessionManager.session; if (session != null) { final descopeUser = await Descope.auth.me(session.refreshJwt); session.updateUser(descopeUser); } ``` ```javascript import { useDescope, useSession } from '@descope/react-native-sdk' const descope = useDescope() const { updateUser } = useSession() const session = Descope.sessionManager.session const userResponse = await descope.me(session.refreshJwt) session.updateUser(userResponse) ``` The DescopeUser struct contains the following attributes: - `userId`: The user's unique Descope generated userId. - `loginIds`: An array of loginIds associated to the user. - `createdAt`: The time at which the user was created in Descope. - `name`: Name associated to the user. - `picture`: The user's profile picture. - `email`: Email address associated to the user. - `isVerifiedEmail`: Boolean whether the email address for the user has been verified. - `phone`: Phone number associated to the user. - `isVerifiedPhone`: Boolean whether the phone number for the user has been verified. ## External Token If your Descope project has an [External Token](https://github.com/management/project-settings/external-token) connector configured ([Firebase](https://docs.descope.com/connectors/connector-configuration-guides/token/firebase), [Supabase](https://docs.descope.com/connectors/connector-configuration-guides/token/supabase), or a [Generic HTTP Token](https://docs.descope.com/connectors/connector-configuration-guides/token/generic-token)), Descope returns an `externalToken` alongside the session and refresh tokens. Your backend can keep validating tokens in your existing format while Descope handles authentication. The connector runs at the [End action](https://docs.descope.com/flows/actions/end-action) of a flow. Direct authentication calls like `otp.verify`, `oauth.exchange`, and `passkey.signIn` do not trigger it. On mobile, this means an external token is only returned when the user authenticates by [running a flow](https://github.com/management/project-settings/external-token) through the SDK's FlowView. If there is no External Token connector configured, then `externalToken` will be returned will a `null` value. Our [Swift](/getting-started/swift), [Kotlin](/getting-started/kotlin), and [Flutter](/getting-started/flutter) SDKs expose `externalToken` on the `AuthenticationResponse` returned to the flow's success callback. [React Native](/getting-started/react-native) includes `externalToken` on the response passed to the FlowView `onSuccess` callback. ```swift let flow = DescopeFlow(url: "https://example.com/myflow") let flowViewController = DescopeFlowViewController() flowViewController.delegate = self flowViewController.start(flow: flow) // The delegate receives the AuthenticationResponse when the flow completes func flowViewControllerDidFinish(_ controller: DescopeFlowViewController, response: AuthenticationResponse) { // externalToken is set only when the flow's End step has an External Token connector configured if let externalToken = response.externalToken { // use externalToken with your existing backend or service (e.g. Firebase, Supabase) } let session = DescopeSession(from: response) Descope.sessionManager.manageSession(session) } ``` ```kotlin descopeFlowView.listener = object : DescopeFlowView.Listener { override fun onSuccess(response: AuthenticationResponse) { // externalToken is set only when the flow's End step has an External Token connector configured val externalToken = response.externalToken if (externalToken != null) { // use externalToken with your existing backend or service (e.g. Firebase, Supabase) } Descope.sessionManager.manageSession(DescopeSession(response)) } override fun onError(exception: DescopeException) { // handle flow errors } } val descopeFlow = DescopeFlow("") descopeFlowView.run(descopeFlow) ``` ```dart DescopeFlowView( config: DescopeFlowConfig( url: 'https://api.descope.com/login/?flow=', ), callbacks: DescopeFlowCallbacks( onSuccess: (AuthenticationResponse response) { // externalToken is set only when the flow's End step has an External Token connector configured final externalToken = response.externalToken; if (externalToken != null) { // use externalToken with your existing backend or service (e.g. Firebase, Supabase) } final session = DescopeSession.fromAuthenticationResponse(response); Descope.sessionManager.manageSession(session); }, onError: (DescopeException error) { // handle flow errors }, ), controller: descopeFlowController, ); ``` ```javascript import { FlowView, useSession } from '@descope/react-native-sdk' const { manageSession } = useSession() { // externalToken is set only when the flow's End step has an External Token connector configured const externalToken = jwtResponse.externalToken if (externalToken) { // use externalToken with your existing backend or service (e.g. Firebase, Supabase) } await manageSession(jwtResponse) }} onError={(error) => { // handle flow errors }} /> ``` # Web Client Sessions (/sessions/management/web) How web apps retrieve Descope session tokens with the Client SDK, send them to your backend, and manage logout and session state in the browser. # Web Client Sessions After a user signs in with Descope, your web app holds a **session token** and **refresh token**. The Client SDK stores them (typically in memory or a secure cookie) and refreshes the session token when it expires. Your web app does **not** validate the session for API authorization — your **backend** does. The client's job is to: 1. **Retrieve** the session token from the SDK after authentication 2. **Send** it to your application server on each API request (usually as a `Bearer` token) 3. **Manage** logout and optional UI checks (expiry, roles for display) See [Session Validation](/sessions/validation) for backend and gateway validation. See [Sessions](/sessions) for the session and refresh token model. If you're building APIs without a Descope backend SDK, you still need to validate every incoming token — see [Backend validation](/sessions/validation/backend). ## Sending session token to application server If you are using Client SDK or using Descope Flows, then your application client must send the session token to you application server. The `getSessionToken()` function gets the `sessionToken` from local storage via JS which you can then include in your request. NextAuth does not use Descope's Client SDK but does use JWTs for session management. ```javascript import { getSessionToken } from '@descope/react-sdk'; const sessionToken = getSessionToken(); // example fetch call with authentication header fetch('your_application_server_url', { headers: { Accept: 'application/json', Authorization: 'Bearer '+ sessionToken, } }) ``` ```javascript // server components import { getServerSession } from 'next-auth' import { authOptions } from '@/app/_utils/options' const session = await getServerSession(authOptions) // client components 'use client' import { useSession } from "next-auth/react" const { data: session } = useSession() ``` ```javascript import createSdk from '@descope/web-js-sdk'; const descopeSdk = createSdk({projectId: "__ProjectID__"}); const sessionToken = descopeSdk.getSessionToken(); // example fetch call with authentication header fetch('your_application_server_url', { headers: { Accept: 'application/json', Authorization: 'Bearer '+ sessionToken, } }) ``` ```html ``` ```javascript import { DescopeAuthService } from '@descope/angular-sdk'; private authService = inject(DescopeAuthService); token = getSessionToken() ``` At any time, your web client should send only the **session token** to your application server. Your server validates it with the [Descope Backend SDK](/sessions/validation/backend) (or an [API gateway JWT authorizer](/sessions/validation/jwt-authorizers)) before serving protected data. ## Logout using Client SDK If you are integrating using the Descope Client SDK, then you must use the Client SDK to logout. If you are using Descope Flows with React SDK, refer to the [Quick Start](/getting-started) for details. If you are Descope Client SDK without flows, then refer to the sample code below for logout. If you're using NextAuth and Next.js, you'll need to also make sure that you're handling the logout using the federated IdP revocation endpoint. You can see this working in a sample app [here](https://github.com/descope/nextjs-hackathon-template/blob/main/app/api/auth/federated-sign-out/route.ts). ```javascript import DescopeSdk from '@descope/web-js-sdk'; const descopeSdk = Descope({projectId: "__ProjectID__"}); // Logout from the current session const resp = await descopeSdk.logout(); // Logout from all the sessions const resp = await descopeSdk.logoutAll(); ``` ```javascript // Sign out with NextAuth works a bit differently then you may be expecting. The reason is that, by using NextAuth, you're creating a NextAuth session between NextAuth and your client but also an OIDC session between Descope and NextAuth. // Therefore, you'll need to make sure both sessions are cleared when logging out, which is detailed below. --------------- // /app/signout/page.tsx "use client"; import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { signOut } from "next-auth/react"; function SignOutCallback() { const router = useRouter(); useEffect(() => { const performSignOut = async () => { await signOut({ redirect: false }); router.push("/"); }; performSignOut(); }, [router]); return

; } --------------- // /app/api/auth/federated-sign-out import { authOptions } from "@/app/_utils/options"; import { getServerSession } from "next-auth"; import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; const handler = async (req: NextRequest, res: NextResponse) => { try { const session = await getServerSession(authOptions); if (!session) { return NextResponse.redirect(process.env.NEXTAUTH_URL!); } const endSessionURL = `__BaseURL__/oauth2/v1/logout`; const redirectURL = `${process.env.NEXTAUTH_URL}/signout`; const endSessionParams = new URLSearchParams({ // @ts-ignore id_token_hint: session.idToken, post_logout_redirect_uri: redirectURL, }); const fullUrl = `${endSessionURL}?${endSessionParams.toString()}`; return NextResponse.redirect(fullUrl); } catch (error) { console.error(error); } }; export const GET = handler; --------------- // app/components/signout-button "use client"; import { signOut } from "next-auth/react"; function SignOutButton() { return (
); } ``` ```javascript const descopeSdk = Descope({projectId: "__ProjectID__"}); // Logout from the current session const resp = await descopeSdk.logout(); // Logout from all the sessions const resp = await descopeSdk.logoutAll(); ``` ```html ``` ```javascript import { Component, OnInit } from '@angular/core'; import { DescopeAuthService } from '@descope/angular-sdk'; @Component({ selector: 'app-home', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { isAuthenticated: boolean = false; userName: string = ''; constructor(private authService: DescopeAuthService) {} ngOnInit() { this.authService.session$.subscribe((session) => { this.isAuthenticated = session.isAuthenticated; }); this.authService.user$.subscribe((descopeUser) => { if (descopeUser.user) { this.userName = descopeUser.user.name ?? ''; } }); } logout() { this.authService.descopeSdk.logout(); } } ``` ## Checking token expiration (UI only) Your web app can read session expiration in the browser to drive UI — a countdown, a "your session is about to expire" modal, or an early redirect to the login screen. This is a **client-side convenience** — it does not replace [backend validation](/sessions/validation/backend). There are two different questions you can answer in the browser: | What you need | Use | | --- | --- | | Whether a token has **already** expired | `isSessionTokenExpired()` / `isRefreshTokenExpired()` — see [Is the token expired](#is-the-token-expired) | | **When** the session expires | The `exp` and `rexp` claims returned by `useSession()` — see [Reading the expiration time](#reading-the-expiration-time) | ### Is the token expired ```javascript import { isSessionTokenExpired } from '@descope/react-sdk'; // With no argument, the helper reads the current session token from storage if (isSessionTokenExpired()) { console.log('Session token has expired.'); } else { console.log('Session token is valid.'); } ``` ```javascript 'use client' import { useSession } from "next-auth/react" // NextAuth manages its own session object and does not expose Descope claims const { data: session, status } = useSession() if (status === 'unauthenticated') { console.log('Session has expired, or the user is not signed in.'); } else if (session?.expires) { console.log('NextAuth session expires at:', new Date(session.expires)); } ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__' }); const sessionToken = sdk.getSessionToken(); if (sdk.isJwtExpired(sessionToken)) { console.log('Session token has expired.'); } else { console.log('Session token is valid.'); } ``` ```html ``` ### Reading the expiration time The `useSession()` hook returns a `claims` object containing the `exp` (session token) and `rexp` (refresh token) expiration timestamps. For the exact data types and payload structure, see [Claims returned by the Client SDK](/management/token#claims-returned-by-the-client-sdk). ```jsx import { useSession } from '@descope/react-sdk'; const SessionInfo = () => { const { claims } = useSession(); // `exp` is UNIX epoch seconds — multiply by 1000 for a JavaScript Date const sessionTokenExpiresAt = claims?.exp ? new Date(claims.exp * 1000) : null; // `rexp` is an ISO 8601 string — pass it to Date directly const signOutAt = claims?.rexp ? new Date(claims.rexp) : null; return (
  • Session token expires at: {sessionTokenExpiresAt?.toLocaleString()}
  • You will be signed out at: {signOutAt?.toLocaleString()}
); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__', autoRefresh: true }); // onClaimsChange re-emits the claims after every session refresh const unsubscribe = sdk.onClaimsChange((claims) => { if (!claims) return; const sessionTokenExpiresAt = claims.exp ? new Date(claims.exp * 1000) : null; const signOutAt = claims.rexp ? new Date(claims.rexp) : null; console.log('Session token expires at:', sessionTokenExpiresAt); console.log('User must authenticate again at:', signOutAt); }); ``` ```html ``` #### Warning the user before the session ends Drive the countdown off **`rexp`**, not `exp`. The SDK refreshes the session token silently in the background, so a countdown built on `exp` appears to reset every few minutes; `rexp` is when the user is actually signed out. ```jsx import { useEffect, useState } from 'react'; import { useSession } from '@descope/react-sdk'; const WARN_BEFORE_MS = 30 * 60 * 1000; // warn 30 minutes ahead const SessionTimeoutWarning = () => { const { claims, isAuthenticated } = useSession(); const [showWarning, setShowWarning] = useState(false); useEffect(() => { if (!isAuthenticated || !claims?.rexp) return; const signOutAt = new Date(claims.rexp).getTime(); // Poll rather than schedule a single timeout: setTimeout cannot span more // than ~24.8 days, and refresh token timeouts are often longer than that. const check = () => setShowWarning(signOutAt - Date.now() <= WARN_BEFORE_MS); check(); const interval = setInterval(check, 30_000); return () => clearInterval(interval); }, [claims?.rexp, isAuthenticated]); if (!showWarning) return null; return
Your session is about to expire. Save your work.
; }; ``` The `rexp` value is derived from your project's [Refresh Token Timeout](/management/project-settings#refresh-token-timeout), and `exp` from the [Session Token Timeout](/management/project-settings#session-token-timeout). ## Roles and permissions (UI only) You can read roles and permissions from the session token to drive UI (show/hide menus, etc.). **Do not rely on these values for authorization** — your backend must validate the token and enforce access on every API request. ```javascript import { getSessionToken, getJwtRoles } from '@descope/react-sdk' const sessionToken = getSessionToken(); const roles = getJwtRoles(sessionToken); console.log('User roles:', roles); ``` ```javascript import createSdk from '@descope/web-js-sdk'; const descopeSdk = createSdk({projectId: '__ProjectID__'}); const sessionToken = descopeSdk.getSessionToken(); const roles = descopeSdk.getJwtRoles(sessionToken); console.log('User roles:', roles); ``` ```html ``` ```javascript import { DescopeAuthService } from '@descope/angular-sdk'; private authService = inject(DescopeAuthService); this.authService.descopeSdk.getJwtRoles(token = getSessionToken(), tenant = '') ``` ## Permissions (UI only) Permissions can also be read from the session token for display purposes using `getJwtPermissions`. Enforce permissions on your server, not in the browser. ```javascript import { getSessionToken, getJwtPermissions } from '@descope/react-sdk' const sessionToken = getSessionToken(); const permissions = getJwtPermissions(sessionToken); console.log('User permissions:', permissions); ``` ```javascript import createSdk from '@descope/web-js-sdk'; const descopeSdk = createSdk({projectId: '__ProjectID__'}); const sessionToken = descopeSdk.getSessionToken(); const permissions = descopeSdk.getJwtPermissions(sessionToken); console.log('User permissions:', permissions); ``` ```html ``` ```javascript import { DescopeAuthService } from '@descope/angular-sdk'; private authService = inject(DescopeAuthService); getJwtPermissions(token = getSessionToken(), tenant = '') ``` # Overview (/sessions/validation) Validate Descope session tokens on your backend or at an API gateway before serving protected resources. # Session Validation **Validate session tokens on your server or at the gateway** — not in the browser or on the device. After a user signs in, your web or mobile client sends the session token to your API as described in [Session management](/sessions/management). Your backend (or an API gateway in front of it) must verify the token before serving protected resources. | Where to validate | Description | | | --- | --- | --- | | **Backend** | Validate server-side on every API request. Recommended for all APIs and when enforcing [roles and permissions](/authorization/role-based-access-control) from JWT claims. | | | **API gateway** | Validate at the edge with AWS, Azure, GCP, or other JWT authorizers — no application code required. | | Web and mobile SDKs store tokens and attach them to API requests — see [Session management](/sessions/management). They do not authorize access; your server does. For **live claims** on every request (roles, tenant, or custom attributes that may have changed since issuance), use [Token introspection](/sessions/introspection) via the UserInfo endpoint in addition to local JWT validation. For the session and refresh token model, see [Sessions](/sessions). # Cypress (/unit-testing/e2e-testing-guides/e2e-cypress) Discover the step-by-step process to automate your web application testing using Cypress. # E2E Testing with Cypress Welcome to the guide on testing [Descope](https://descope.com) flows with Cypress. Whether you are new to end-to-end testing or an experienced tester looking to better understand Descope flows, this guide will provide you with practical knowledge and examples that you can implement straight away. Let's dive in! This guide covers creating test users manually for testing; however, you can also see our [Dynamic Test Users Configuration](/test-users#dynamic-test-user-creation) guide if you would like the ability to create test users dynamically as part of the sign-up-or-in process. ## What you'll learn - Use custom Cypress commands to: - Programmatically authenticate with Descope - Authenticate with Descope through the UI - Including your tests in continuous integration via Github Actions ## Cypress Installation Make sure you have Cypress already installed within your application. If not, you can do so [here](https://docs.cypress.io/guides/getting-started/installing-cypress). ## Descope Application Setup To get started with Descope, an application needs to be setup within the Descope Console via the following steps: 1. Visit the Descope Console and create a new project. 2. Enter the desired name for your application. 3. Get your `projectId` found in `Settings/Project` , and create a `managementKey`, found in `Settings/Company/Management Keys/+ Mangement Key` ## Setting Descope app credentials in Cypress To have access to test user credentials within our tests we need to configure Cypress to use the Descope environment variables set in the `.env` file. - cypress.config.js - cypress.config.ts ```jsx const { defineConfig } = require("cypress"); // Populate process.env with values from .env file require('dotenv').config() module.exports = defineConfig({ e2e: { includeShadowDom: true, // Important for interacting with Descope components baseUrl: 'http://localhost:3000', setupNodeEvents(on, config) { // implement node event listeners here }, }, env: { descope_project_id: process.env.REACT_APP_DESCOPE_PROJECT_ID, descope_management_key: process.env.REACT_APP_DESCOPE_MANAGEMENT_KEY }, }); ``` Make sure the corresponding environment variables exist in your `.env` file ## Custom Command for Descope Authentication There are two ways you can authenticate to Descope: - Login with UI - Programmatic Login For both methods of log in, you will need to include your environment variables and test user first. Then, you can start the login flow. ```jsx title="cypress/support/commands.js" const projectId = Cypress.env('descope_project_id') const managementKey = Cypress.env('descope_management_key') const descopeAPIDomain = "api.descope.com" // Define the authorization header const authHeader = { 'Authorization': `Bearer ${projectId}:${managementKey}`, } // Define the base URL for Descope API const descopeApiBaseURL = `https://${descopeAPIDomain}/v1`; const testUserLoginId = "testUser" + Math.floor(1000 + Math.random() * 9000) + "@gmail.com"; // Must match email to pass validation // Define the test user details const testUser = { loginId: testUserLoginId, email: testUserLoginId, phone: "+11231231234", verifiedEmail: true, verifiedPhone: true, displayName: "Test User", test: true, } ``` ### Descope UI Login Below is a command to login into Descope, using the [Test User Management API](/api/management/users/test-users) and navigating via the user interface, just as a real user would. It should be added in `cypress/support/commands.js`. The `loginViaDescopeUI` command will execute the following steps: 1. Use the [Test User Management API](/api/management/users/test-users) to perform the login (create user and generate OTP code). 2. Then, we enter the user `loginId` and `code` that we just generated to log in via the user interface. Since Descope renders components via the shadow DOM, remember to add the `includeShadowDom: true` field in your `cypress.config.js` as noted above or your tests won't be able to interact with the UI. The other option is to use `.shadow()` in each function call as described in the [Cypress documentation](https://docs.cypress.io/api/commands/shadow) but this may make your code harder to read. ```jsx title="cypress/support/commands.js" // Add the loginViaDescopeUI command Cypress.Commands.add('loginViaDescopeUI', () => { cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/user/create`, headers: authHeader, body: testUser, }) .then(({ body }) => { const loginId = body["user"]["loginIds"][0]; cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/tests/generate/otp`, headers: authHeader, body: { "loginId": loginId, "deliveryMethod": "email" } }) .then(({ body }) => { const otpCode = body["code"] const loginID = body["loginId"] cy.visit('/login') cy.get('descope-wc') .find('input') .type(loginID) // If you haven't set `includeShadowDom: true` in your config (as recommended above), // you'll have to use `.shadow()` in each function call. // So the previous line would look like this: // cy.get('descope-wc').shadow().find('input').type(loginID) cy.get('descope-wc') .find('descope-button').contains('Continue').click() cy.get('descope-wc').find('.descope-input-wrapper').find('input').should('exist') // Assertion added to wait for the OTP code input to appear let otpCodeArray = Array.from(otpCode); // Convert the OTP code string to an array otpCodeArray.forEach((digit, index) => { cy.get(`descope-text-field[data-id="${index}"]`).then($element => { const input = $element[0].shadowRoot.querySelector('input'); input.value = digit; input.dispatchEvent(new Event('change', { bubbles: true })); }); }); cy.get('descope-wc') .find('descope-button').contains('Submit').click() // Customize these steps based on your authentication flow }) }) }) ``` ### Programmatic Login Below is a command to programmatically login into Descope, using the [Test User Management API](/api/management/users/test-users) and set an item in `localStorage` with the authenticated users details, which we will use in our application code to verify we are authenticated under test. The `loginViaDescopeAPI` command will execute the following steps: 1. Use the [Test User Management API](/api/management/users/test-users) to perform the programmatic login (create user, generate OTP code, and verify OTP code). 2. Finally the `refreshToken`and `sessionToken` items are set in `localStorage`. ```jsx title="cypress/support/commands.js" // Add the loginViaDescopeAPI command Cypress.Commands.add('loginViaDescopeAPI', () => { cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/user/create`, headers: authHeader, body: testUser, }) .then(({ body }) => { const loginId = body["user"]["loginIds"][0]; cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/tests/generate/otp`, headers: authHeader, body: { "loginId": loginId, "deliveryMethod": "email" } }) .then(({ body }) => { const otpCode = body["code"] cy.request({ method: 'POST', url: `${descopeApiBaseURL}/auth/otp/verify/email`, headers: authHeader, body: { "loginId": loginId, "code": otpCode } }) .then(({ body }) => { const sessionJwt = body["sessionJwt"] const refreshJwt = body["refreshJwt"] // Note that if the refresh token is empty, // you will need to get it from the headers instead. // This is due to whether your Descope project // returns tokens via cookies or response body /** Default name for the session cookie name / local storage key */ const SESSION_TOKEN_KEY = 'DS'; /** Default name for the refresh local storage key */ const REFRESH_TOKEN_KEY = 'DSR'; // // Store the JWT in the browser's local storage. cy.window().then((win) => { win.localStorage.setItem(SESSION_TOKEN_KEY, sessionJwt); win.localStorage.setItem(REFRESH_TOKEN_KEY, refreshJwt); }); // // Now navigate to the root URL of your application. cy.visit('/') }) }) }) }) ``` We'll also need to clean up the created testing users before starting so we don't go over the limit. This is done with the deleteAllTestUsers function written in the same file. ```jsx title="cypress/support/commands.js" // Add the deleteAllTestUsers command Cypress.Commands.add('deleteAllTestUsers', () => { cy.request({ method: 'DELETE', url: `${descopeApiBaseURL}/mgmt/user/test/delete/all`, headers: authHeader, }) }) ``` With our Descope app setup properly in the Descope Developer console, necessary environment variables in place, and our loginViaDescopeApi and/or loginViaDescopeUI command(s) implemented, we will be able to authenticate with Descope while our app is under test. Below is a test to login as a user using our loginViaDescopeAPI function and verify the welcome page is showing. ```jsx describe('Descope', function () { beforeEach(function () { cy.deleteAllTestUsers() cy.loginViaDescopeAPI() }) it('shows welcome page', function () { cy.contains('Welcome').should('be.visible') }) }) ``` We can also use our loginViaDescopeUI command in the test. Below is our test to login as a user via Descope and run a basic sanity check. ```jsx describe('Descope', function () { beforeEach(function () { cy.deleteAllTestUsers() cy.loginViaDescopeUI() cy.visit('/') }) it('shows welcome page', function () { cy.contains('Welcome').should('be.visible') }) }) ``` ## Running Tests in Parallel If you'd like to run tests in parallel, you'll have to delete test users individually after the run. Here's a command to do so called deleteIndividualTestUser. You'll also need to modify your login command to return the loginId of the user or you can store it in a global variable. ```jsx Cypress.Commands.add('deleteIndividualTestUser', (loginId) => { cy.request({ method: 'DELETE', url: `${descopeApiBaseURL}/v1/mgmt/user/delete`, headers: authHeader, body: { "loginId": loginId } }) }) ``` Here's an example of a test that runs in parallel and deletes the user afterwards. ```jsx describe('Descope', function () { const testUserLoginId = "testuser" + Math.floor(1000 + Math.random() * 9000) + "@gmail.com"; // LoginId match email to pass validation before(function () { cy.loginViaDescopeUI(testUserLoginId) // Modify this function to accept a loginId cy.visit('/') }) it('shows welcome page', function () { cy.contains('Welcome').should('be.visible') }) after(function () { cy.deleteIndividualTestUser(testUserLoginId) }) }) ``` ## Adding Continuous Integration with Github Actions 1. Add `.env` variables to your Github repository 1. Navigate to: Settings → Secrets & Variables → Actions → Add “New Repository Secret” 1. REACT_APP_DESCOPE_PROJECT_ID="__ProjectID__" 2. REACT_APP_DESCOPE_MANAGEMENT_KEY="ManagementKey" 2. Create a `main.yml` file in `.github/workflows` ```yaml name: E2E Tests on: [push] jobs: cypress-run: name: Cypress E2E Tests runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install dependencies run: yarn install --frozen-lockfile - name: Build run: yarn build - name: Cypress run uses: cypress-io/github-action@v5 env: REACT_APP_DESCOPE_PROJECT_ID: ${{ secrets.REACT_APP_DESCOPE_PROJECT_ID }} REACT_APP_DESCOPE_MANAGEMENT_KEY: ${{ secrets.REACT_APP_DESCOPE_MANAGEMENT_KEY }} with: start: yarn start wait-on: "http://localhost:3000" wait-on-timeout: 120 browser: chrome ``` 3. Github Action, completed! 1. You've added a configuration file for a GitHub Actions workflow. It is written in YAML and is designed to perform End-to-End (E2E) testing using Cypress on a repository's code whenever a push event is triggered. ## Descope Rate Limiting Logins Be aware of the rate limit statement in the Descope documentation: Descope SDKs and API endpoints are rate-limited to maintain stable performance and provide a good experience for all users. Descope may limit requests if it detects an unusual spike in requests or abnormal activity in a specific project or across projects. For example, this can happen in a denial-of-service attack. To avoid hitting the rate limit, ensure your app is optimized to make the least number of requests possible based on the need. If a request breaches the rate limit - an error will be returned, together with `Retry-After` header, which will specify when to retry the request. As the size of a test suite increases and [parallelized runs](https://on.cypress.io/parallelization) are utilized to expedite test run duration, this limit can be reached. ## Examples ### Sample App To view an example of setting up E2E tests with Cypress and Descope, please view our [sample app](https://github.com/descope-sample-apps/b2c-retail-sample-app) on Github. ## Other Authentication Methods ### Magic Link Setting up E2E tests via magic link requires a few more steps than the previous methods. First, you'll need to create a custom Cypress command to login via magic link. Then, you'll need to manually go through the flow in your browser to get the host, execution ID, and step ID. Finally, you'll need to visit the magic link in your test and submit the form. ```js Cypress.Commands.add('loginViaUIMagicLink', () => { cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/user/create`, headers: authHeader, body: testUser, }) .then(({ body }) => { const loginId = body["user"]["loginIds"][0]; cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/tests/generate/magiclink`, headers: authHeader, body: { "loginId": loginId, "deliveryMethod": "email" } }) .then(({ body }) => { const loginID = body["loginId"] const token = body["link"].split('t=')[1] // Get the host, execution ID, and step ID from manually going through the flow in your browser // http://localhost:3000/?descope-login-flow=sign-up-or-in%7C%23%722bBWEu4PXO3974IVnTDuczfkkLE_7.end&t=e4cbb953227ee2816a9f801c953b391253b2cb6fb8c520aefa3728745db253587 const host = 'http://localhost:3000/' const executionId = 'sign-up-or-in%7C%23%7C2bBVRwGbJkRVQ6k7N3GqklXHb9v_' const stepId = '7.end' const magicLink = host + '?descope-login-flow=' + executionId + stepId + '&t=' + token; cy.visit('/') cy.get('descope-wc') .find('input') .type(loginID) cy.get('descope-wc') .find('.descope-button').contains('Continue').click() cy.visit(magicLink) cy.get('descope-wc') .find('.descope-button').contains('Submit').click() }) }) }) ``` ### Enchanted Link Similar to Magic Link, setting up E2E tests via enchanted link requires a few more steps than the previous methods. First, you'll need to create a custom Cypress command to login via enchanted link. Then, you'll need to manually go through the flow in your browser to get the host, execution ID, and step ID. Finally, you'll need to visit the enchanted link in your test and submit the form. ```js Cypress.Commands.add('loginViaDescopeUIEnchantedLink', () => { cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/user/create`, headers: authHeader, body: testUser, }) .then(({ body }) => { const loginId = body["user"]["loginIds"][0]; cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/tests/generate/embedded`, headers: authHeader, body: { "loginId": loginId, "deliveryMethod": "email" } }) .then(({ body }) => { const token = body["link"].split('t=')[1] const pendingRef = body["pendingRef"] // Get the host, execution ID, and step ID from manually going through the flow in your browser // http://localhost:3000/?descope-login-flow=sign-up-or-in%7C%23%7C2bBtkd7nNhiEsbDLafLyMMKLgz7_14.end-2bBtkZSoriDJyqk6GVAee7MGSKJ&t=eb58e881ab4e22d3b534ae498c012699f11e2c3a6989e564e7d641c117141bdc const executionId = "sign-up-or-in%7C%23%7C2bBtkd334hiEWbDLaMLyMMKLgz7" const stepId = "14.end" const host = 'http://localhost:3000/' const enchantedLink = host + '?descope-login-flow=' + executionId + "_" + stepId + "-" + pendingRef + '&t=' + token; cy.visit('/') cy.get('descope-wc') .find('input') .type(loginId) cy.get('descope-wc') .find('.descope-button').contains('Sign in with email').click() cy.visit(enchantedLink) }) }) }) ``` ### Embedded OTP or Link Similar to Magic Link, setting up E2E tests via embedded OTP requires a few more steps than the previous methods. The same can be done for embedded link, but the token will simply need to be parsed from the link instead of used directly. ```js Cypress.Commands.add('loginViaDescopeUIEmbeddedOTP', () => { cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/user/create`, headers: authHeader, body: testUser, }) .then(({ body }) => { const loginId = body["user"]["loginIds"][0]; cy.request({ method: 'POST', url: `${descopeApiBaseURL}/mgmt/tests/generate/otp`, headers: authHeader, body: { "loginId": loginId, "deliveryMethod": "Embedded" } }) .then(({ body }) => { const otpCode = body["code"] const loginID = body["loginId"] // Navigate UI and input code as shown above }) }) }) ``` ### Passwords For typing codes into a passwords input, you can simply get the component then type in the code as shown. ```js const code = '123456'; const passcode = cy.get('descope-passcode', { includeShadowDom: true }); code .split('') .reverse() .forEach((digit, index, arr) => { passcode .get('input', { includeShadowDom: true }) .eq(arr.length - index - 1) .type(digit); }); ``` ### Phone Number Input For typing in a phone number, you can get the DOM element that targets a `` element with the specific attributes (type="tel" and placeholder="Phone"). You can then get the resulting jQuery object, access the first (and likely only) DOM element in the jQuery object, access the shadow DOM (which is how Descope renders components), and get the `input` element. Then, you can directly set the value to the phone number. We add the bubble event to make the event more realistic. ```js cy.get('descope-text-field[type="tel"][placeholder="Phone"]').then($element => { const input = $element[0].shadowRoot.querySelector('input'); input.value = "1231231234"; // Phone number input.dispatchEvent(new Event('change', { bubbles: true })); }) ``` # Playwright (/unit-testing/e2e-testing-guides/e2e-playwright) Learn how to test Descope flows with Playwright, an automated testing tool for web applications. # E2E Testing with Playwright Welcome to the guide on testing [Descope](https://descope.com) flows with Playwright. This guide covers creating test users manually for testing; however, you can also see our [Dynamic Test Users Configuration](/test-users#dynamic-test-user-creation) guide if you would like the ability to create test users dynamically as part of the sign-up-or-in process. ## Playwright Installation Make sure you have Playwright already installed within your application. If not, you can do so [here](https://playwright.dev/docs/intro). ## Descope Application Setup To get started with Descope, an application needs to be setup within the Descope Console via the following steps: 1. Visit the Descope Console and create a new project. 2. Enter the desired name for your application. 3. Get your `projectId` found in `Settings/Project` , and create a `managementKey`, found in `Settings/Company/Management Keys/+ Mangement Key` ## Global Setup and Teardown for Descope Authentication ### Configuration First, you'll need a general configuration file to setup Playwright testing. Create a `playwright.config.ts` in the root directory. ```typescript title="playwright.config.ts" import { defineConfig, devices } from "@playwright/test"; const config = { testDir: "e2e", use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL || "http://localhost:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", storageState: "playwright/.auth/user.json", }, webServer: { command: "yarn start", url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, stdout: "ignore", stderr: "pipe", }, globalSetup: require.resolve("./e2e/auth.setup"), globalTeardown: require.resolve("./e2e/auth.teardown"), /* Configure projects for major browsers */ projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ { name: "Mobile Chrome", use: { ...devices["Pixel 5"] }, }, { name: "Mobile Safari", use: { ...devices["iPhone 12"] }, }, /* Test against branded browsers. */ { name: "Microsoft Edge", use: { ...devices["Desktop Edge"], channel: "msedge" }, }, { name: "Google Chrome", use: { ...devices["Desktop Chrome"], channel: "chrome" }, }, ], } // export the defineConfig(config) ``` ### Setup for Authentication To have access to test user credentials within our tests we need to configure Playwright to use the Descope environment variables set in the `.env` file. ```jsx title="e2e/auth.setup.ts" import Descope from "@descope/node-sdk"; import { chromium, type FullConfig } from "@playwright/test"; import * as crypto from "crypto"; require("dotenv").config(); export const authFile = "playwright/.auth/user.json"; async function globalSetup(config: FullConfig) { const browser = await chromium.launch(); const page = await browser.newPage(); const testUser = crypto.randomBytes(20).toString("hex"); process.env.TEST_USER = testUser; const descope = Descope({ projectId: process.env.REACT_APP_DESCOPE_PROJECT_ID, managementKey: process.env.DESCOPE_MANAGEMENT_KEY, }); await descope.management.user.createTestUser(testUser, "test@test.test"); const magiclink = await descope.management.user.generateMagicLinkForTestUser( "email", testUser, "https://test.local" ); const token = magiclink.data.link.split("?t=")[1]; const auth = await descope.magicLink.verify(token); await page.goto(config.projects[0].use.baseURL); await page.evaluate( ([ds, dsr]) => { window.localStorage.setItem("DS", ds); window.localStorage.setItem("DSR", dsr); }, [auth.data.sessionJwt, auth.data?.refreshJwt] ); await page.context().storageState({ path: authFile }); await browser.close(); } // export the globalSetup ``` Make sure the corresponding environment variables exist in your `.env` file ### Teardown after Testing Create a `e2e/auth.teardown.ts` script to clean up test users and reset the state: ```jsx title="e2e/auth.teardown.ts" import Descope from "@descope/node-sdk"; import { type FullConfig } from "@playwright/test"; require("dotenv").config(); async function globalTeardown(config: FullConfig) { const descope = Descope({ projectId: process.env.REACT_APP_DESCOPE_PROJECT_ID, managementKey: process.env.DESCOPE_MANAGEMENT_KEY, }); await descope.management.user.delete(process.env.TEST_USER); } ``` ### Implementing Tests with Authenticated State With the global setup and teardown scripts handling authentication, your tests can focus on the application's functionality: ```jsx title="Home.spec.tsx" import { expect, test } from "@playwright/test"; test("test", async ({ page }) => { await page.goto("/"); await expect(page.getByText(/Hello, U2/i)).toBeVisible(); }); ``` ### Running Playwright tests Simply execute the following command to run tests ```sh title="Terminal" npx playwright test ``` If you don't already have playwright installed, you'll need to install it using the following command: `npx playwright install`. If you're on a linux/mac system, you might also need to run `npx playwright install msedge` to run the Microsoft Edge tests locally, from the root user. That's it! To see a full example of setting up e2e tests with Playwright and Descope, check out the [Descope Playwright React Example](https://github.com/descope-sample-apps/descope-playwright-react-example). ### Other Considerations #### Refresh Token Rotation If [refresh token rotation](/additional-security-features-in-descope/refresh-token-rotation) is enabled in your Descope project, running multiple tests may cause authentication failures. When refresh token rotation is enabled, each use of a refresh token invalidates it and issues a new one. Since the test setup stores the refresh token once during global setup, subsequent tests that trigger token refresh will fail because they're using an invalidated token. **Solutions:** - **Disable refresh token rotation** for your test environment (recommended for E2E tests) - **Create a fresh authenticated session** for each test instead of reusing the global setup - **Implement token refresh handling** in your test setup to update stored tokens after refresh #### WebAuthn Support If you are looking to test with WebAuthn, you may need to set up a virtual authenticator. This can be done as follows: ```jsx const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); // Set up a virtual authenticator const authenticator = await context.newCDPSession(page); await authenticator.send('WebAuthn.enable'); await authenticator.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: false, } }); ``` # Authenticator Apps (TOTP) (/auth-methods/auth-apps) Customize your authentication applications (TOTP) with Descope. # Authenticator Apps (TOTP) Descope supports validating sign-up and sign-ins via Authenticator Applications which provide a Time-based One-time Password (TOTP). Google Authenticator, Microsoft Authenticator, and Authy are examples of authenticator apps. Descope generates the required QR code or key (also called a secret or seed) in order to configure new a new Authenticator. ## Authenticator Apps (TOTP) with Flows This guide will walk you through integrating TOTP Authenticator Apps into your Descope Flows. TOTP (Time-Based One-Time Password) adds an extra layer of security by requiring users to enter a code generated by an authenticator app. ### Flow Actions When using TOTP, the following actions are available: - **Sign Up / TOTP** - Verifies the TOTP code and signs the user up if they do not exist. - **Sign In / TOTP** - Verifies the TOTP code and signs the user in if they already exist; fails if they do not. - **Update User / TOTP** - Links an authenticator app to an existing user, so they can use it as an authentication method in the future. ### How to Use TOTP Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. These actions can be integrated into your application like any other Action. This is an example of using the **Sign Up or In / TOTP** action in a flow: ![generate-totp-secret-flow-action](/assets/sign-up-or-in-totp-action.webp) To verify and set up an authenticator app using TOTP, you'll need to scan the QR code automatically created from the action, and verify the code with Descope. ![totp-qr-code](/assets/totp-qr-code.webp) ### Flow Screens There's not much you need to handle in flow screens when using TOTP. However, you can develop your own screens and customize how users set up their authenticator apps. Descope provides multiple methods for TOTP setup: #### QR Code Setup The most common method for setting up TOTP is by scanning a QR code. When using TOTP actions in your flow, Descope automatically generates a QR code that users can scan with their authenticator app. You can customize the QR code display in your flow screens using the **TOTP QR Code** action. This allows you to control the placement and styling of the QR code in your custom screens. ![totp-qr-code-action](/assets/totp-qr-code-component.webp) To set up TOTP using a QR code: 1. Open your authenticator app (Google Authenticator, Microsoft Authenticator, Authy, etc.) 2. Select the option to add a new account or scan a QR code 3. Point your device's camera at the QR code displayed in the flow screen 4. The authenticator app will automatically configure the account and start generating codes #### Provision URL The Provision URL works best on mobile devices where authenticator apps are installed. On desktop browsers, users may still need to scan the QR code manually using their mobile device's camera. To enhance the user experience, especially for mobile users, you can add a **Provision URL** alongside the TOTP QR code on the setup screen. This URL is a clickable link that redirects users directly to their authenticator app, eliminating the need to manually open the app and scan the QR code. When tapped, the device recognizes the URL scheme and automatically opens a supported authenticator app (such as Google Authenticator, Microsoft Authenticator, or Authy) with the TOTP configuration ready to be added. ![TOTP Provision URL](/assets/totp-provision-url.webp) #### Manual TOTP Setup In some cases, users may prefer to **manually** enter the TOTP key into their authenticator app instead of scanning the QR code. This can be useful when: - The QR code cannot be scanned (e.g., camera issues or accessibility needs) - The authenticator app doesn't support QR code scanning - The user is using a mobile device and prefers to enter the key manually To enable manual setup, you can use the **TOTP Code** component and change the variant to **Text** to display the TOTP secret key (also called a seed) to users in your flow screen. This component automatically shows the key that users can copy and paste into their authenticator app. ![manual-totp-setup](/assets/manual-totp.webp) On some native mobile apps, like Swift-based iOS apps, plain text isn't selectable or copyable. Set the component's screen condition to **read-only** instead of **disable** to keep the key visible and copyable while blocking edits. See [Conditional Components](/flows/screens#conditional-components) for details. ### How to Delete TOTP for users Descoper can remove or delete TOTP for a user who has signed up with it. Once the user is enabled with the TOTP authentication method, their record appears in the user management console. Descope provides an option to clear or reset TOTP for that user if necessary. This can be done by selecting the user record and choosing one of two methods. One method is to click on the three dots on the right and select **Delete TOTP Seed**. Alternatively, you can click **Delete TOTP Seed** at the top of the table. This allows you to reset TOTP for users who have registered with TOTP auth method. ![totp-delete](/assets/totp-user-delete.webp) ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/auth-apps/settings) Customize your authenticator apps (totp) authentication settings with Descope. # Authenticator Apps (TOTP) Settings Customize your Authenticator Apps (TOTP) authentication from the [Descope console (Settings > Authentication Methods > Authenticator Apps (TOTP))](https://app.descope.com/settings/authentication/totp). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Authenticator Apps (TOTP) authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key # Device Authentication (/auth-methods/device-auth) Create OAuth Device Authentication flow with Descope for smart TVs, IoT devices, and other input-constrained devices. # Device Authentication Device Authentication in Descope implements the OAuth 2.0 Device Authorization Grant ([RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628)), enabling secure authentication for input-constrained devices like smart TVs, streaming devices, gaming consoles, and IoT devices. This method allows devices without keyboards or with limited input capabilities to securely obtain user authorization by letting users authenticate on a separate device with better input capabilities. Try out Device Authentication for yourself, at [Device Auth Guru](https://www.deviceauth.guru/)! ## Overview Device Authentication solves the challenge of authenticating users on devices that have limited input capabilities or lack a web browser. Instead of requiring users to enter credentials directly on the device, the flow redirects the authentication process to a secondary device (like a smartphone or computer) where users can easily complete the authentication process. ### Common Use Cases - **Smart TVs and Streaming Devices**: Netflix, YouTube, and other streaming apps - **Gaming Consoles**: PlayStation, Xbox authentication flows - **IoT Devices**: Smart home devices, industrial equipment - **Kiosks and Digital Signage**: Public terminals with limited input - **Voice Assistants**: Smart speakers and voice-controlled devices - **CLI Tools**: Command-line applications and tools. For a working example, check out our [Blog on Authenticating CLI Tools With Descope](https://www.descope.com/blog/post/cli-tool-auth). ## How Device Authentication Works The Device Authentication flow involves several steps across two devices: the requesting device (with limited input) and the authorization device (user's phone/computer). ![Device Code Diagram](/assets/device-code-flow.webp) ### Technical Flow #### Step 1. Request codes (device → Descope): The device calls the **Device Authorization** endpoint to get a `device_code`, `user_code`, `verification_uri` (and `verification_uri_complete`), plus `expires_in` and a polling `interval`. #### Step 2. Show the user what to do: The device displays the **user code** and **verification URL** (or a QR code that encodes `verification_uri_complete`). #### Step 3. User verifies & signs in: On a phone/computer, the user opens the link. A [Descope Flow](/auth-methods/device-auth#verification-flow) confirms the user code and, if needed, authenticates the user (and collects consent) in one seamless step. #### Step 4. Poll for completion (device): The device polls the token endpoint with the `device_code` at the suggested `interval` until it receives success or a terminal error (`authorization_pending`, `slow_down`, `access_denied`, `expired_token`). #### Step 5. Receive tokens & proceed: On success, the token endpoint returns an `access_token` (and a `id_token` and `refresh_token`). The device stores these and calls protected APIs and resources with the `access_token`. ### Implementation Walkthrough #### Step 1: Device Code Request The device initiates the flow by requesting a device code from Descope: Use the `/oauth2/v1/device` endpoint for your Generic OIDC Application, or utilize the specific Device URL for your [OIDC Application](/identity-federation/applications). ```http POST /oauth2/v1/device Content-Type: application/x-www-form-urlencoded client_id=YOUR_CLIENT_ID&scope=openid+profile+email ``` #### Step 2: Device Code Response Descope responds with the necessary codes and URLs. For example: ```json { "device_code": "GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS", // The device verification code "user_code": "WDJB-MJHT", // The end-user verification code "verification_uri": "https://auth.example.com/device", // Your flow hosting URL "verification_uri_complete": "https://auth.example.com/device?user_code=WDJB-MJHT", // Verification URI including user code "expires_in": 1800, // Lifetime in seconds of device_code and user_code "interval": 5 // The minimum amount of time in seconds that the client should wait between polling requests to the token endpoint. } ``` #### Step 3: User Code Display The device displays the user code and instructions: ``` To authenticate this device: 1. Visit: https://auth.descope.com/device 2. Enter code: WDJB-MJHT ``` #### Step 4: User Authorization The user visits the verification URL on their phone/computer, which launches a [Descope Flow](/auth-methods/device-auth) that handles: 1. **User Code Confirmation**: The user enters the displayed user code to link their session to the device 2. **Authentication**: The user completes authentication using any configured Descope authentication methods (password, social login, MFA, etc.) 3. **Device Authentication Confirmation**: The user confirms that they would like to authenticate the device. #### Step 5: Token Polling The device polls Descope for an access token: ```http POST /oauth2/v1/token Content-Type: application/x-www-form-urlencoded grant_type=urn:ietf:params:oauth:grant-type:device_code& device_code=GmRhmhcxhwAzkoEqiMEg_DnyEysNkuNhszIySk9eS& client_id=YOUR_CLIENT_ID ``` #### Step 6: Token Response Once authorized, Descope returns a response including the following: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "refresh_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Il...", "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlNL...", "expires_in": 3600, "scope": "" } ``` ## Verification Flow The verification flow consists minimally of the following steps: ### 1. Input Screen: A screen for the user to input the user code. The input box should have the `form.userCode` context key. Can be auto-filled by including the `user_code` query parameter in the flow URL. ![User code input screen](/assets/usercode-input.webp) ### 2. The `Device Flow User Code Verification` Action Use this action to verify that the user code is correct. ### 3. User Authentication Have the user authenticate using an authentication action or subflow. ### 4. Device authentication approval We recommend confirming with the user that they would like to authenticate the device before using the mandatory `Device Flow Approval` Action. ### 5. Confirmation Screen After the user has given approval, display a confirmation screen to indicate that the device authentication was successful. Note that the flow should end on a Confirmation Screen, and not with the `End` Action. ![Device Verification Flow](/assets/device-verification-flow.webp) # Settings (/auth-methods/device-auth/settings) Customize your device authentication settings with Descope. # Device Authentication Settings Customize your [Device Authentication](/auth-methods/device-auth) Settings from the [Descope console (Settings > Authentication Methods > Device Authentication)](https://app.descope.com/settings/authentication/devicecodes). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Device Authentication can be invoked programmatically via APIs and SDKs. Unlike other authentication methods, the only way to use Device Authentication is through API/SDK calls, so this setting will enable/disable Device Authentication entirely. ## Available Settings This section describes additional details about the configuration options available. ### User Code Format Use 'X' for each character and '-' as a separator to define the code format. Default is `XXX-XXX`. ### User Code Characters Choose whether the user code consists of **Both capital letters and numbers**, **Only capital letters**, or **Only numbers**. ### Expiration Time Set the user code and device code expiration time in minutes/hours. Default is 3 minutes. ### Device Hosting URL The flow hosting URL for your [Device Authentication Verification Flow](/auth-methods/device-auth#verification-flow). This by default can be the [Auth Hosting](/identity-federation/auth-hosting) page or you can use your own hosted page. # Embedded Link (/auth-methods/embedded-link) Customize your embedded link authentication flow with Descope. # Embedded Link An embedded link is a one-time-use token that allows an existing user to authenticate without entering their credentials. Once created, the token can be included in a URL and sent to the user through channels like email, SMS, or in-app messages. When the user clicks the link, they are automatically authenticated if the token is valid and has not expired or been used. Embedded links can also be consumed programmatically, such as in backend workflows where user context needs to be passed securely without direct user interaction. To validate an embedded link, use the magic link verification function provided by the SDK. ## Embedded Link with Flows Embedded Link actions are also available in [Management Flows](/flows/management-flows). This guide will walk you through integrating embedded link actions into your Descope Flows. Embedded links are typically used when you want to perform tasks asynchronously with traditional sign up actions. One notable use case is allowing users to verify their email address after the initial sign up, while already being authenticated with your application. ### Flow Actions While this may enhance your user experience to authenticate faster if utilizing passwords, they will still need to verify their email address before sending a password reset via the normal methods. This guide will cover the abovementioned use case and the use case for sending password reset via embedded link via flows. ### Email Verification After Initial Sign Up For the use case of allowing a user to verify their email after their initial signup, utilize the embedded link Update User action within your flow. For a detailed guide on how to do this, go to the page on this [use case](/flows/use-cases/email-verification). ### Password Reset The password reset configuration is very similar to the above example for email verification; however, here, you would utilize the `Sign In / Embedded Link` action. This action also allows you to configure the URI and Token Expiration. For this flow path, setting the Token expiration lower makes more sense since you expect the user to interact with the email sooner than the email verification use case. ![Configure embedded link sign in action for password reset within Descope flows](/assets/configure-embedded-link-sign-in.webp) The **URI** sets where the user lands to have their token verified, and accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can serve several sites. For example, `{{device.location.scheme}}://{{device.location.hostname}}/reset-password` builds the URL from the domain hosting the flow. A redirect URL your application passes when it starts the flow takes priority over this field; see [redirect URL precedence](/flows/dynamic-keys#redirect-url-precedence). You would then add another send email action similarly to the [above example](#configure-email-sending-action), and then connect the actions to the applicable screens per the example below. ![Configure embedded link sign in action for password reset within Descope flows](/assets/connect-actions-password-reset.webp) If a [Custom Claims Flow Action](/flows/actions/custom-claims) is used before an Embedded Link sign-up, sign-in, or sign-up-or-in action, the custom claims are carried through the Embedded Link authentication and included in the JWT issued after the link is verified. ### Other Embedded Link Actions There are other embedded link use cases that you may later want to utilize within your flow. Descope has other available embedded link actions that you can utilize to update the user's phone, sign up via embedded link, or sign up via embedded link. You can find these actions by clicking the blue `+` sign at the top left, selecting `Action,` and searching for `Embedded Link`. ![Searching for embedded link actions within Descope flows](/assets/search-embedded-link-actions.webp) # Settings (/auth-methods/embedded-link/settings) Customize your embedded link authentication settings with Descope. # Embedded Link Settings Customize your Embedded Link authentication from the [Descope console (Authentication Methods > Embedded Link)](https://app.descope.com/settings/authentication/embeddedlink). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Embedded Link authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Expiration Time: For increased security, we recommend an expiration time of 3-5 minutes. Define length of time after which link or code expires. A shorter expiration time limits how long a malicious actor has to attempt an attack (such as a dictionary or brute force attack) on the code or link. # Magic Link (/auth-methods/magic-link) Customize your magic link authentication flow with Descope. # Magic Link A magic link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service can send magic links via email or SMS texts. The browser tab that is opened after clicking the magic link gets the authenticated session cookies. For example, consider a user that starts the login process on a laptop browser and gets a magic link delivered to their email inbox. When they click the email link, a new browser tab will open and they will be logged in on the new tab. For a full list of customizable settings, see [Magic Link Settings](/auth-methods/magic-link/settings). Magic Link starts the session in a new tab after polling completes. If you wish to start the user session in the existing tab, check out [Enchanted Link](/auth-methods/enchanted-link) instead. ## Magic Link with Flows This guide will walk you through integrating magic link based authentication into your Descope Flows. Magic Links allow your users to authenticate with a single click of a button, sent directly to their email address or phone number. ### Flow Actions When using Magic Link, you have the standard actions for most authentication methods available. - **Sign Up / Magic Link** - Signs the user up, but will not work if user already exists - **Sign Up or In / Magic Link** - Signs the user in, and if user doesn't exist it will automatically sign them up - **Sign In / Magic Link** - Signs the user in, but if the user doesn't exist it will fail. - **Update User / Magic Link** - Will merge Magic Link identity to an existing user ### How to Use Magic Link Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **Sign Up or In** action in a flow: ![sign-up-or-in-flow-action](/assets/sign-up-or-in-magic-link-flow-action.webp) #### URI The **URI** field sets the address in the magic link, where the user lands to have their token verified. Leave it empty to use the [Redirect URL](/auth-methods/magic-link/settings#redirect-url) from your project settings. This field accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can point users at whichever site they started from. For example, `{{device.location.scheme}}://{{device.location.hostname}}/verify` builds the link from the domain hosting the flow. A redirect URL your application passes when it starts the flow takes priority over this field; see [redirect URL precedence](/flows/dynamic-keys#redirect-url-precedence). ### Flow Screens When using Magic Link, there will typically be a screen present for polling, with a horizontal scrolling sidebar, as part of the actions listed above. All you need to do to use magic links, is to connect the actions to a screen that has a **Custom Login ID**, **Email**, or **Phone** input. ![magic-link-polling](/assets/magic-link-polling.webp) You can determine where the user started the flow using the context key `userAgent`, which can be utilized in other conditions or screens. ![magic-link-user-agent](/assets/magiclink-useragent.webp) ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/magic-link/settings) Customize your Magic Link authentication settings with Descope. # Magic Link Settings Customize your magic link settings from the [Descope console (Settings > Authentication Methods > Magic Link)](https://app.descope.com/settings/authentication/magiclink). Magic Link settings can also be overridden per tenant. See [tenant authentication methods](/management/tenant-management/tenant#magic-link). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Magic Link authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Redirect URL The redirect URL is where the user is redirected to after a successful login. The redirect URL will be overridden when specified in the SDK or API call, or by the [URI](/auth-methods/magic-link#uri) field on the magic link flow actions, which also accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls). ### Expiration Time For increased security, we recommend an expiration time of 3-5 minutes. Define length of time after which link or code expires. A shorter expiration time limits how long a malicious actor has to attempt an attack (such as a dictionary or brute force attack) on the code or link. ### Number of Retries and Attempts Timeframe (seconds) Limit the number of communication attempts (email, text) a recipient can receive within the defined timeframe. If the limit is exceeded, no further messages will be sent until the timeframe resets. ### Allow Unverified Recipient Email Addresses or Phone Numbers Enabling this option may increase the risk of spam and fraud. By default, messages are sent only to verified email addresses or phone numbers. When enabled, this option allows messages to be sent to [unverified email addresses or phone numbers](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#understanding-unverified-emails). If a user authenticates using an unverified contact, Descope automatically marks that contact as verified. ## Connectors ### Email Connector Descope supports sending email OTP messages using your email messaging provider, such as AWS SES, SendGrid, or a generic SMTP service. You can configure a email messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported email messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Text Message (SMS) Connector Descope supports sending text messages using your text messaging provider, such as Twilio or Amazon SNS. You can configure a text messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported text messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Fallback Connector To each of the connectors in this authentication method, Descope supports a fallback connector that is available to use to prevent downtimes. Descoper can configure a fallback connector that will be used in case the active connector isn’t available. This fallback connector can either be Descope's default connector or an alternate connector configured for sms/email. ![Fallback for ML](/assets/fallback-magiclink.webp) ## Templates If you are using a customized connector, you can change the template of the email/sms which your user will receive. The default is System. # Enchanted Link (/auth-methods/enchanted-link) Customize your enchanted link authentication flow with Descope. # Enchanted Link If you don't need cross-device login capabilities, but would like the one-click login experience this authentication method provides, check out [Magic Link](/auth-methods/magic-link) instead. **Enchanted Link** is a cross-device authentication method allowing users to log in on one device (e.g., a desktop app) by verifying a unique link sent to their email and matching a number displayed during the login process. These links are exclusively sent via email. ## How Enchanted Links Work Try out Enchanted Link for yourself, at [Enchanted Link Guru](https://www.enchantedlink.guru/)! Enchanted Links enable users to initiate login on one device (the **originating device**) and complete it by clicking a link on another device. The login is validated only when the correct number from the email is matched with the number displayed during the request. The session starts exclusively on the originating device. ## Limitations and Security Considerations ### Phishing Risks Since the session does not follow the link, an attacker with access to the user’s email could potentially log in by selecting the correct number. This makes Enchanted Links more susceptible to phishing than traditional magic links. ### Purpose of Numbers The number-matching process is not designed to counter email compromise but to prompt users to pause and critically evaluate unexpected login requests, reducing susceptibility to phishing. ### Persistent Link Validity Enchanted Links remain valid even if an incorrect number is selected, minimizing user frustration. This does not introduce additional risk, as the method assumes that email compromise cannot be entirely mitigated by link expiration alone. ## Comparing Enchanted Link to Magic Link - **Enchanted Link:** - **Use Case:** Optimized for cross-device logins. - **Security:** More vulnerable to phishing since the session stays on the originating device. - **User Experience:** Promotes awareness through number-matching, reducing accidental misuse. - **Magic Link:** - **Use Case:** Ideal for single-device logins. - **Security:** Less prone to phishing since the session starts on the device where the link is clicked. - **User Experience:** Simpler but less suited for cross-device scenarios. Enchanted Links strike a balance between user convenience and security, especially in scenarios requiring cross-device authentication, while integrating mechanisms to reduce phishing risks. ## Enchanted Link with Flows This guide will walk you through integrating Enchanted Link based authentication into your Descope Flows. ### Flow Actions When using Enchanted Link, you have the standard actions for most authentication methods available. - **Sign Up / Enchanted Link** - Signs the user up, but will not work if user already exists - **Sign Up or In / Enchanted Link** - Signs the user in, and if user doesn't exist it will automatically sign them up - **Sign In / Enchanted Link** - Signs the user in, but if the user doesn't exist it will fail. - **Update User / Enchanted Link** - Will merge OAuth identity to an existing user - **Verify Code / Enchanted Link** - Will verify the enchanted link token is the one that was generated as part of the Enchanted Link authentication and is used when you want to include additional conditions before verification. ### How to Use Enchanted Link Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these Actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **Sign Up or In** action in a flow: ![sign-up-or-in-flow-action](/assets/sign-up-or-in-enchanted-link-flow-action.webp) #### URI The **URI** field sets the address in the enchanted link, where the user lands to have their token verified. Leave it empty to use the [Redirect URL](/auth-methods/enchanted-link/settings#redirect-url) from your project settings. This field accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can point users at whichever site they started from. For example, `{{device.location.scheme}}://{{device.location.hostname}}/verify` builds the link from the domain hosting the flow. A redirect URL your application passes when it starts the flow takes priority over this field; see [redirect URL precedence](/flows/dynamic-keys#redirect-url-precedence). #### Verify Code / Enchanted Link Action The **Verify Code / Enchanted Link** Action can be used when you want to include custom conditions or logic before verifying the link token, such as adding [custom claims](/flows/actions/custom-claims) to the session JWT. By default, enchanted link actions verify the token automatically, so to insert intermediate flow steps before verification you need to select the **Custom Token Verification** checkbox in the corresponding **Sign In / Enchanted Link** or **Sign Up / Enchanted Link** Action in your flow that sends the link. ![verify-code-flow-action](/assets/custom-token-verification-ench-link.webp) This is an example of adding custom claims to your user JWT token and then verifying that token in an Enchanted Link flow: ![verify-code-flow-action](/assets/verify-enchanted-link-action.webp) ### Flow Screens When implementing Enchanted Link authentication, a polling screen with a horizontal scrolling sidebar will be displayed to the user. To enable Enchanted Link functionality, simply connect the authentication actions to a screen containing an email input field. ![enchanted-link-polling](/assets/enchanted-link-polling.webp) You can determine where the user started the flow using the context key `userAgent`, which can be utilized in other conditions or screens. ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/enchanted-link/settings) Customize your enchanted link authentication settings with Descope. # Enchanted Link Settings You can customize your Enchanted Link authentication in the [Descope console (Settings > Authentication Methods > Enchanted Link)](https://app.descope.com/settings/authentication/enchantedlink). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Enchanted Link authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Redirect URL The redirect URL is default URL for the route you implement to verify enchanted link tokens. This is the location to send the user upon successful authentication. The redirect URL will be overridden when specified in the SDK or API call, or by the [URI](/auth-methods/enchanted-link#uri) field on the enchanted link flow actions, which also accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls). ### Expiration Time For increased security, we recommend an expiration time of 3-5 minutes. Define length of time after which link or code expires. A shorter expiration time limits how long a malicious actor has to attempt an attack (such as a dictionary or brute force attack) on the code or link. ### Number of Retries and Attempts Timeframe (seconds) Limit the number of email communication attempts a recipient can receive within the defined timeframe. If the limit is exceeded, no further messages will be sent until the timeframe resets. ### Allow Unverified Recipient Email Addresses or Phone Numbers: Enabling this option may increase the risk of spam and fraud. By default, messages are sent only to verified email addresses or phone numbers. When enabled, this option allows messages to be sent to [unverified email addresses or phone numbers](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#understanding-unverified-emails). If a user authenticates using an unverified contact, Descope automatically marks that contact as verified. ## Connectors Configure the connector to utilize to send the enchanted link message. The default is Descope. ### Email Connector Descope supports sending email OTP messages using your email messaging provider, such as AWS SES, SendGrid, or a generic SMTP service. You can configure a email messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported email messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Fallback Connector For the email connector in this authentication method, Descope supports a fallback connector that is available to use to prevent downtimes. Descoper can configure a fallback connector that will be used in case the active connector isn’t available. This fallback connector can either be Descope's default connector or an alternate connector configured for email. ![Fallback for EL](/assets/fallback-enchanted-link.webp) ## Templates If you are using a customized connector, you can change the template of the email which your user will receive. The default is System. # nOTP Authentication (/auth-methods/notp) nOTP authentication flow with Descope. # nOTP Authentication nOTP (no-tee-pee) allows users to log in via WhatsApp with just a single click, eliminating the need for codes, usernames, and typing. Unlike traditional OTP methods, nOTP doesn't require the company to connect to email servers or SMS providers, which can significantly reduce costs as it scales with the number of users. Try out nOTP for yourself, at [nOTP Guru](https://notp.guru)! ## How does it work? This chart explains the authentication process: ![Descope nOTP process explanation](/assets/notp-process.webp) ![Descope nOTP User Experience](/assets/notp-user-experience.gif) nOTP is not currently supported on [Native Flows](/mobile-sdk/native-vs-browser-flows) on mobile. To use nOTP on mobile, you must use browser-based [flows](/flows). ## nOTP with Flows This guide will walk you through integrating nOTP based authentication into your Descope Flows. With just a single click, nOTP allows your users can log in via WhatsApp, eliminating the need for codes, usernames, and typing. ### Flow Actions When using OAuth Login, you have the standard actions for most authentication methods available. - **Sign Up / nOTP** - Signs the user up, but will not work if user already exists - **Sign Up or In / nOTP** - Signs the user in, and if user doesn't exist it will automatically sign them up - **Sign In / nOTP** - Signs the user in, but if the user doesn't exist it will fail. - **Update User / nOTP** - Will merge OAuth identity to an existing user ### How to Use nOTP Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **Sign Up or In** action in a flow: ![Descope nOTP sign up or in](/assets/notp-flow.webp) When using nOTP, it's important to take note of a few things: - The link that is being sent is not an authentication link. - nOTP verification should be take place during the last steps of the flow. ### Flow Screens nOTP has only a few actions that can be used in a flow. They are the **WhatsApp** button, as well as the **nOTP QR Code** image. ![Descope nOTP screen component](/assets/notp-screen-component.webp) ![notp-code-verify](/assets/notp-qr-code-verify.webp) If you wish to include the QR code in your own custom screen to sign in with Whatsapp with, you can use the **nOTP QR Code** image component in any screen. ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/notp/settings) Customize your nOTP (WhatsApp) authentication settings with Descope. # nOTP (WhatsApp) Settings Customize your nOTP authentication from the [Descope console (Authentication Methods > nOTP)](https://app.descope.com/settings/authentication/notp). A one-time password (OTP) is an automatically generated string sent to the user during the onboarding (sign-up or sign-in) process to authenticate that user. The WhatsApp account will be waiting for the user to insert this OTP to continue the authentication process. ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether nOTP authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Expiration Time Define length of time after which link or code expires. For increased security, we recommend an expiration time of 3-5 minutes. A shorter expiration time limits how long a malicious actor has to attempt an attack (such as a dictionary or brute force attack) on the code or link. ### Allow Unverified Recipient Email Addresses or Phone Numbers Enabling this option may increase the risk of spam and fraud. By default, messages are sent only to verified email addresses or phone numbers. When enabled, this option allows messages to be sent to [unverified email addresses or phone numbers](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#understanding-unverified-emails). If a user authenticates using an unverified contact, Descope automatically marks that contact as verified. ## Connector Configure the connector to utilize to send the nOTP message (See below for connector details). The default is Descope. ### WhatsApp Connector Setup Add your own WhatsApp business account for nOTP authentication from the [Descope console (Connectors > WhatsApp Chat)](https://app.descope.com/connectors/template/whatsapp). This will allow you to customize the messages (verification approval, error) if needed. #### Prerequisites 1. [WhatsApp business account](https://tinyurl.com/whatsapp-get-started-business) 2. [Set up Webhooks](https://developers.facebook.com/docs/whatsapp/cloud-api/guides/set-up-webhooks) #### Connector Setup - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: Describe what your connector is used for. - **Phone Number ID**: The WhatsApp unique phone number ID for the account phone number. See WhatsApp documentation above for more details - **Phone Number**: The WhatsApp account phone number. See WhatsApp documentation for more details - **Token**: The authentication token associated with the phone number Id. - **App Secret**: The app secret associated with the WhatsApp Web Application. - **Webhook Verify Token**: The webhook verify token associated with the WhatsApp Web Application. ## Templates (Verification, Approval, Error) If you are using a customized connector, you can change the templates of the message which your user will receive. The default is System. You can use your WhatsApp business account to set up templates. ![Descope nOTP customize messages](/assets/notp-customize-messages.webp) #### Additional Steps You need to configure the webhook in the WhatsApp Web Application: - **Callback URL**: set `https://api.descope.com/v1/whatsapp/webhook/__ProjectID__` - **Verify Token**: set the webhook verify token associated with the WhatsApp Web Application. Read more about setting up webhooks in the WhatsApp Business API settings on the [Developer Facebook Docs](https://developers.facebook.com/docs/graph-api/webhooks/getting-started#create-endpoint). In order to save the callback URL in WhatsApp app, you need to set the connector in the nOTP authentication page. # Google One Tap (/auth-methods/oauth/google-one-tap) Learn how to add Google One Tap authentication to your application using Descope SDKs and visual workflows. # Google One Tap Descope supports adding Google One Tap to your application through FedCM using the Descope SDKs. The Descope SDKs come with a pre-wrapped FedCM component, which you can easily use to adapt Google One Tap login within your application. This guide will cover configuring your custom Google provider and implementing code within your application to utilize One Tap. This guide also covers running post-authenticated flows after a user has successfully authenticated via Google One Tap. Google One Tap requires you to have your own Google Provider configured with the `Implicit` grant type enabled within Descope's configuration. The [Google Provider Configuration](/auth-methods/oauth/google-one-tap#google-provider-configuration) section below covers this configuration. For a live demo, check out [OneTap.guru](https://www.onetap.guru/) and our [video on adding Google One Tap to your application](https://www.youtube.com/watch?v=nzQn8y6_mbI). ## What is Google One Tap? Google One Tap is a sign-in feature that allows users to log into websites or apps with just one click without remembering or entering passwords. It simplifies authentication using a secure token-based process linked to the user's Google account. This feature can be embedded into websites and apps, offering a seamless, fast login experience that reduces user friction. It's designed to improve user convenience and security by eliminating the need for repeated credential entry. Additionally, it helps businesses increase user sign-ups and engagement by minimizing barriers to entry. ![Descope - Google One Tap Pop Up Example](/assets/google-one-tap-example.webp) ### Supported Browsers Currently Google One Tap is supported on Chrome, Edge, Firefox, and Safari; however, this support is independent of Descope. To view supported One Tap browsers, see [Google's Documentation](https://developers.google.com/identity/gsi/web/guides/supported-browsers#one_tap) ## FedCM vs non-FedCM With FedCM (Federated Credential Management), Google One Tap relies on the browser to provide user session information, and without FedCM, One Tap uses Google's Javascript library, relying on mechanisms like cookies. FedCM is [supported](https://developer.mozilla.org/en-US/docs/Web/API/FedCM_API) by Chromium-based browsers, while all major browsers support the non-FedCM version. You can specify whether or not to utilize FedCM in your implementation through our [customization options](/auth-methods/oauth/google-one-tap#customization). ## Google Provider Configuration Google One Tap requires you to have your own Google Provider configured with the `Implicit` grant type enabled within Descope's configuration. We have a thorough guide on configuring your own Google OAuth provider [here](/auth-methods/oauth/providers/setting-up-your-own-apps/google); however, the below will expand on this configuration. ### Authorized JS Origins and Redirect URIs When it comes to Google One Tap and configuring your Google Client's javascript Origins and Redirect URIs, you need to ensure that you properly configure them to allow Google to allow the required bidirectional traffic for One Tap. Per our [Google Provider Configuration Guide](/auth-methods/oauth/providers/setting-up-your-own-apps/google), we cover that you must include your application URL within the `Authorized Javascript origins`, which is still applicable for One Tap. Within that guide, we also cover the `Authorized redirect URIs`, which would be something like `https://api.descope.com/v1/oauth/callback` or your custom domain `https://auth.myapp.com/v1/oauth/callback`; however, when it comes to Google One Tap, the redirect URI will go back to your application before any further redirects to Descope's callback. This means that when configuring your Google API Credentials for the Google provider, you'll need to include your application URL within the `Authorized redirect URIs`. Below is an example assuming that your application is on `myapp.com` and has a custom custom domain configured. ![Descope - Google One Tap Example configuration of Google Authorized JS Origins and Redirect URIs](/assets/google-one-tap-redirect-js-origins.webp) ### Implicit Grant Type Now that you have successfully configured your Google OAuth Provider to be compatible with Google One Tap, you will also need to make a change within the Google OAuth provider within the Descope console by going to the [OAuth Configuration page](https://app.descope.com/settings/authentication/social), selecting Google, then adding the additional grant type of `Implicit`, and saving the configuration. An example of this configuration can be seen below. ![Descope - Google One Tap Example configuration of implicit grant type](/assets/google-one-tap-implicit-grant-type.webp) ## Implementing One Tap Component You can implement Google One Tap easily with the Descope client SDKs. Use the `sdk.fedcm.onetap.requestAuthentication` method for standard One Tap implementation. To incorporate a post-authentication flow, use `sdk.fedcm.onetap.requestExchangeCode`, which returns an OAuth exchange code instead of a JWT response, allowing you to redirect to a flow before fully authenticating the user. See our [Post-Authentication](/auth-methods/oauth/google-one-tap#post-authentication-flows-with-one-tap) section below for more details. ### Customization Customize the One Tap Component by passing in the following optional props in OneTapConfig: - `auto_select`: boolean on whether the user is [automatically signed in](https://developers.google.com/identity/gsi/web/guides/automatic-sign-in-sign-out) - `prompt_parent_id`: ID of the prompt parent container, used to [set location of the One Tap prompt](https://developers.google.com/identity/gsi/web/reference/js-reference#prompt_parent_id) - `cancel_on_tap_outside`: boolean on whether to cancel the One Tap prompt if the user taps elsewhere on the page. Default is true - `context`: modifies [wording on the One Tap prompt](https://developers.google.com/identity/gsi/web/reference/js-reference#context). Context can be "signin", "signup," or "use". Default is "signin" - `intermediate_iframe_close_callback`: callback function to [handle the intermediate iframe close event](https://developers.google.com/identity/gsi/web/reference/js-reference#intermediate_iframe_close_callback) - `itp_support`: boolean on whether to support [Intelligent Tracking Protection (ITP)](https://developers.google.com/identity/gsi/web/reference/js-reference#itp_support). Default is false - `login_hint`: String to [skip account selection](https://developers.google.com/identity/gsi/web/reference/js-reference#login_hint) if the application knows which user should be signed in - `hd`: [allowed domain](https://developers.google.com/identity/gsi/web/reference/js-reference#hd) that the user can sign in with - `use_fedcm_for_prompt`: boolean on [whether to use FedCM for the prompt](https://developers.google.com/identity/gsi/web/reference/js-reference#use_fedcm_for_prompt). Default is false ```javascript import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; import { useNavigate } from "react-router-dom"; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import { Navigate } from 'react-router-dom'; import { Descope, AuthProvider, useDescope, useSession } from '@descope/react-sdk'; import React, { useEffect, useCallback } from 'react'; let oneTapInitialized = false; const OneTapComp = () => { const sdk = useDescope(); const { isAuthenticated, isSessionLoading } = useSession(); const navigate = useNavigate(); // Moved inside the component const OneTapConfig = {}; // OneTapConfig: { // auto_select: boolean; // prompt_parent_id: string; // cancel_on_tap_outside: boolean; // context: "signup" | "signin" | "use" | undefined; // intermediate_iframe_close_callback: any; // itp_support: boolean; // login_hint: string; // hd: string; // use_fedcm_for_prompt?: boolean; // } const startOneTap = useCallback(async () => { if (oneTapInitialized) return; await sdk.fedcm.onetap.requestAuthentication({provider: 'google', oneTapConfig: OneTapConfig}); oneTapInitialized = true; navigate("/dashboard"); }, [sdk, router]); // sdk.fedcm.onetap.requestAuthentication(options?: { // provider?: string; // oneTapConfig?: OneTapConfig; // loginOptions?: LoginOptions; // onSkipped?: (reason?: string) => void; // onDismissed?: (reason?: string) => void; // onFailed?: (error: Error) => void; // onAuthenticated?: (response: JWTResponse) => void; // }) useEffect(() => { if (!isAuthenticated && !isSessionLoading) { startOneTap(); } }, [isAuthenticated, isSessionLoading, startOneTap]); return null; // Render null or any necessary UI }; function App() { const projectId = "__ProjectID__"; return ( {/* optional for custom domain baseUrl="https://auth.myapp.com"*/} ); } export default App; ``` ```javascript 'use client' import { useDescope, useSession } from "@descope/nextjs-sdk/client"; import { useRouter } from 'next/navigation' import { useEffect } from "react"; let oneTapInitialized = false; const OneTapComp = () => { const oneTap = true; const sdk = useDescope(); const router = useRouter(); const { isAuthenticated, isSessionLoading } = useSession(); const OneTapConfig: = {}; // OneTapConfig: { // auto_select: boolean; // prompt_parent_id: string; // cancel_on_tap_outside: boolean; // context: "signup" | "signin" | "use" | undefined; // intermediate_iframe_close_callback: any; // itp_support: boolean; // login_hint: string; // hd: string; // use_fedcm_for_prompt?: boolean; // } const startOneTap = async () => { if (oneTapInitialized) return; const res: any = await sdk.fedcm.onetap.requestAuthentication({provider: 'google', oneTapConfig: OneTapConfig}); oneTapInitialized = true; router.push("/dashboard") }; // sdk.fedcm.onetap.requestAuthentication(options?: { // provider?: string; // oneTapConfig?: OneTapConfig; // loginOptions?: LoginOptions; // onSkipped?: (reason?: string) => void; // onDismissed?: (reason?: string) => void; // onFailed?: (error: Error) => void; // onAuthenticated?: (response: JWTResponse) => void; // }) useEffect(() => { if (oneTap && !isAuthenticated && !isSessionLoading) { startOneTap(); } }, [isAuthenticated, isSessionLoading]); // Return some JSX here. For example, return null if there's nothing to render: return null; }; export default OneTapComp ``` ```javascript // Create a one-tap.service.ts file within your Angular application. import { Injectable } from '@angular/core'; import Descope from '@descope/web-js-sdk'; import { environment } from '../environments/environment'; @Injectable({ providedIn: 'root' }) export class OneTapService { private sdk: any; private oneTapInitialized: boolean = false; constructor() { const projectId = environment.descopeProjectId; this.sdk = Descope({ projectId: projectId, persistTokens: true, autoRefresh: true }); } const OneTapConfig = {}; // OneTapConfig: { // auto_select: boolean; // prompt_parent_id: string; // cancel_on_tap_outside: boolean; // context: "signup" | "signin" | "use" | undefined; // intermediate_iframe_close_callback: any; // itp_support: boolean; // login_hint: string; // hd: string; // use_fedcm_for_prompt?: boolean; // } // Method to display Google One Tap async displayOneTap(): Promise { if (this.oneTapInitialized) return; try { const resp = await this.sdk.fedcm.onetap.requestAuthentication({provider: 'google', oneTapConfig: OneTapConfig}); console.log("One Tap response:", resp); // Redirect on success window.location.replace("/dashboard"); this.oneTapInitialized = true; } catch (error) { console.error("Failed to display One Tap:", error); } // this.sdk.fedcm.onetap.requestAuthentication(options?: { // provider?: string; // oneTapConfig?: OneTapConfig; // loginOptions?: LoginOptions; // onSkipped?: (reason?: string) => void; // onDismissed?: (reason?: string) => void; // onFailed?: (error: Error) => void; // onAuthenticated?: (response: JWTResponse) => void; // }) } // Method to check if the user is authenticated isAuthenticated(): boolean { const sessionToken = this.sdk.getSessionToken(); if (sessionToken) { return !this.sdk.isJwtExpired(sessionToken); } return false; } // Method to check if One Tap has been initialized isOneTapInitialized(): boolean { return this.oneTapInitialized; } } // Wrap your Angular application with the one tap service within your app.component.ts import { Component, OnInit } from '@angular/core'; import { OneTapService } from './one-tap.service'; import { Router } from '@angular/router'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { constructor(private oneTapService: OneTapService, private router: Router) {} async ngOnInit(): Promise { // Check if the user is authenticated const isAuthenticated = this.oneTapService.isAuthenticated(); if (!isAuthenticated) { // If the user is not authenticated and One Tap is not initialized, show One Tap if (!this.oneTapService.isOneTapInitialized()) { await this.oneTapService.displayOneTap(); } } else { // If user is authenticated, only redirect to dashboard if not already there if (this.router.url !== '/dashboard') { this.router.navigate(['/dashboard']); // Use Angular router for navigation } } } } ``` ```javascript ``` ```html Log In With Descope Flows

Log In With Descope Flows


``` ### FedCM Helper Functions If you are using FedCM, you can utilize additional functions to track user behavior: - `isLoggedIn()`: returns boolean on the logged in state of the user - `isSupported()`: returns boolean on whether the current browser supports FedCM - `onSkip()`: fired when the One Tap prompt is skipped ```javascript const startOneTap = useCallback(async () => { if (oneTapInitialized) return; await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: true}, loginOptions: {}, onSkipped: () => {console.log("One-Tap sign-in was skipped");} }); const isLoggedIn = await sdk.fedcm.isLoggedIn(); const isSupported = await sdk.fedcm.isSupported(); if (isLoggedIn && isSupported) { console.log("One Tap prompt was actually shown."); } oneTapInitialized = true; navigate("/dashboard"); }, [sdk, router]); ``` ```javascript const startOneTap = async () => { if (oneTapInitialized) return; const res: any = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: true}, loginOptions: {}, onSkipped: () => {console.log("One-Tap sign-in was skipped");} }); const isLoggedIn = await sdk.fedcm.isLoggedIn(); const isSupported = if (isLoggedIn && isSupported) { console.log("One Tap prompt was actually shown."); } oneTapInitialized = true; router.push("/dashboard") }; ``` ```javascript try { const resp = await this.sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: true}, loginOptions: {}, onSkipped: () => {console.log("One-Tap sign-in was skipped");} }); console.log("One Tap response:", resp); const isLoggedIn = await sdk.fedcm.isLoggedIn(); const isSupported = await sdk.fedcm.isSupported(); if (isLoggedIn && isSupported) { console.log("One Tap prompt was actually shown."); } // Redirect on success window.location.replace("/dashboard"); this.oneTapInitialized = true; } catch (error) { console.error("Failed to display One Tap:", error); } ``` ```javascript try { const resp = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: true}, loginOptions: {}, onSkipped: () => {console.log("One-Tap sign-in was skipped");} }); const isLoggedIn = await sdk.fedcm.isLoggedIn(); const isSupported = await sdk.fedcm.isSupported(); if (isLoggedIn && isSupported) { console.log("One Tap prompt was actually shown."); } console.log("One Tap response:", resp); // Redirect on success window.location.replace("/dashboard"); oneTapInitialized = true; } catch (error) { console.error("Failed to display One Tap:", error); } ``` ```javascript // Function to display Google One Tap async function displayOneTap() { try { const resp = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: true}, loginOptions: {}, onSkipped: () => {console.log("One-Tap sign-in was skipped");} }); const isLoggedIn = await sdk.fedcm.isLoggedIn(); const isSupported = await sdk.fedcm.isSupported(); if (isLoggedIn && isSupported) { console.log("One Tap prompt was actually shown."); } console.log("One Tap response:", resp); // Redirect on successful One Tap response window.location.replace("./dashboard.html"); } catch (error) { console.error("Failed to display One Tap:", error); } } ``` ### Non-FedCM Helper Functions If you are not using FedCM, you can utilize these functions: - `onSkip(reason)`: fired when the One Tap prompt is skipped - `onDismissed(reason)`: fired when the One Tap prompt is dismissed ```javascript const startOneTap = useCallback(async () => { if (oneTapInitialized) return; await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: false}, loginOptions: {}, onSkipped: (skipped_reason) => {console.log("One Tap sign-in was skipped. Reason:", skipped_reason);}, onDismissed: (dismissed_reason) => {console.log("One Tap sign-in was dismissed. Reason:", dismissed_reason);} }); oneTapInitialized = true; navigate("/dashboard"); }, [sdk, router]); ``` ```javascript const startOneTap = async () => { if (oneTapInitialized) return; const res: any = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: false}, loginOptions: {}, onSkipped: (skipped_reason) => {console.log("One Tap sign-in was skipped. Reason:", skipped_reason);}, onDismissed: (dismissed_reason) => {console.log("One Tap sign-in was dismissed. Reason:", dismissed_reason);} }); oneTapInitialized = true; router.push("/dashboard") }; ``` ```javascript try { const resp = await this.sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: false}, loginOptions: {}, onSkipped: (skipped_reason) => {console.log("One Tap sign-in was skipped. Reason:", skipped_reason);}, onDismissed: (dismissed_reason) => {console.log("One Tap sign-in was dismissed. Reason:", dismissed_reason);} }); console.log("One Tap response:", resp); // Redirect on success window.location.replace("/dashboard"); this.oneTapInitialized = true; } catch (error) { console.error("Failed to display One Tap:", error); } ``` ```javascript try { const resp = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: false}, loginOptions: {}, onSkipped: (skipped_reason) => {console.log("One Tap sign-in was skipped. Reason:", skipped_reason);}, onDismissed: (dismissed_reason) => {console.log("One Tap sign-in was dismissed. Reason:", dismissed_reason);} }); console.log("One Tap response:", resp); // Redirect on success window.location.replace("/dashboard"); oneTapInitialized = true; } catch (error) { console.error("Failed to display One Tap:", error); } ``` ```javascript // Function to display Google One Tap async function displayOneTap() { try { const resp = await sdk.fedcm.onetap.requestAuthentication({ provider: 'google', oneTapConfig: {use_fedcm_for_prompt: false}, loginOptions: {}, onSkipped: (skipped_reason) => {console.log("One Tap sign-in was skipped. Reason:", skipped_reason);}, onDismissed: (dismissed_reason) => {console.log("One Tap sign-in was dismissed. Reason:", dismissed_reason);} }); console.log("One Tap response:", resp); // Redirect on successful One Tap response window.location.replace("./dashboard.html"); } catch (error) { console.error("Failed to display One Tap:", error); } } ``` ### Example Applications To see the experience of OneTap, check out [OneTap.guru](https://www.onetap.guru/). Descope has added Google One Tap examples within various sample applications listed below. - [Next.js](https://github.com/descope-sample-apps/onetap) - [HTML](https://github.com/descope-sample-apps/descope-html-sample-app) - [Angular](https://github.com/descope-sample-apps/angular-sample-app) ## Post Authentication Flows with One Tap In some cases, you may want to run a post-authentication flow after Google One Tap, such as gathering additional user data, running connectors for just-in-time migration, or calling your API on user sign-up. The React code snippet below shows how to use a flow in a modal after One Tap authentication. The key is using the `requestExchangeCode` method for partial authentication, which allows you to authenticate the user after the flow completes. ```javascript import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; import { useNavigate } from "react-router-dom"; import Home from './pages/Home'; import Dashboard from './pages/Dashboard'; import { Navigate } from 'react-router-dom'; import { Descope, AuthProvider, useDescope, useSession } from '@descope/react-sdk'; import React, { useEffect, useCallback } from 'react'; let oneTapInitialized = false; const OneTapComp = () => { const sdk = useDescope(); const { isAuthenticated, isSessionLoading } = useSession(); const navigate = useNavigate(); // Moved inside the component const OneTapConfig = {}; // OneTapConfig { // auto_select: boolean; // prompt_parent_id: string; // cancel_on_tap_outside: boolean; // context: "signup" | "signin" | "use" | undefined; // intermediate_iframe_close_callback: any; // itp_support: boolean; // login_hint: string; // hd: string; // use_fedcm_for_prompt?: boolean; // } const startOneTap = useCallback(async () => { if (oneTapInitialized) return; await sdk.fedcm.onetap.requestExchangeCode({ 'google', OneTapConfig, onCodeReceived: (code) => { window.location.href = `FLOW_REDIRECT_URL/login?code=${code}`; } }); oneTapInitialized = true; }, [sdk, router]); // sdk.fedcm.onetap.requestExchangeCode(options?: { // provider?: string; // oneTapConfig?: OneTapConfig; // loginOptions?: LoginOptions; // onSkipped?: (reason?: string) => void; // onDismissed?: (reason?: string) => void; // onFailed?: (error: Error) => void; // onCodeReceived?: (code: string) => void; // }) useEffect(() => { if (!isAuthenticated && !isSessionLoading) { startOneTap(); } }, [isAuthenticated, isSessionLoading, startOneTap]); return null; // Render null or any necessary UI }; function App() { const projectId = "__ProjectID__"; return ( {/* optional for custom domain baseUrl="https://auth.myapp.com"*/} ); } export default App; ``` ### Example Post One Tap Flow When implementing the post-one-tap authentication flow, you must use the `Load User` action to get the current user's details. Below is an example of how you would utilize the `Load User` action within a post-one-tap authentication flow. ![Descope - Google One Tap Post authentication example flow](/assets/post-one-tap-flow-example.webp) # Social Login (OAuth) (/auth-methods/oauth) Customize your OAuth social login flows with Descope. # Social Login (OAuth) Descope includes preconfigured OAuth applications for testing purposes, but these are limited to **100 total logins per month across all providers**. Once you reach this limit, OAuth login will be disabled until the next month. Therefore, for **production use** we recommend [setting up your own OAuth accounts](/auth-methods/oauth/providers) with custom branding and settings. **Social login (OAuth)** allows users to sign in using their existing accounts from popular platforms such as Google, Facebook, or GitHub. Instead of creating new credentials, users simply click a provider button and authenticate through OAuth. ## What is an OAuth Provider and Application? If you're using a FedRAMP deployment of Descope, you must ensure your OAuth provider is certified for [FedRAMP High](/fedramp/federated-identity-providers). An **OAuth provider** is a service that allows you to authenticate users using their existing accounts from popular platforms such as Google, Facebook, or GitHub. An **OAuth application** is your app's registration directly with an OAuth provider like Google or Facebook. It includes details such as your app name, website URL, and branding assets. Descope **comes with built-in** OAuth applications for all major providers. This means you can begin testing social login right away while Descope manages the OAuth login process in the background. When you're ready to customize or move to production, simply create your own OAuth applications with each provider and update their configurations in the Descope Console. For guides on how to set up your own OAuth applications with Descope, see the [Configuring OAuth Providers](/auth-methods/oauth/providers) guide. ## Social Login (OAuth) with Flows This guide will walk you through integrating OAuth-based social login into your Descope Flows. ### Flow Actions When using OAuth Login, you have the standard actions for most authentication methods available. - **Sign Up / OAuth** - Signs the user up, but will not work if user already exists - **Sign Up or In / OAuth** - Signs the user in, and if user doesn't exist it will automatically sign them up - **Sign In / OAuth** - Signs the user in, but if the user doesn't exist it will fail. - **Update User / OAuth** - Will merge OAuth identity to an existing user This is an example of using the **Sign Up or In** action in a flow: ![sign-up-or-in-flow-action](/assets/sign-up-or-in-oauth-flow-action.webp) ### How to Use OAuth Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. There are 7 modifiable parameters for the OAuth actions: #### Step name The name of the flow action to use. #### Redirect URL URL to redirect to after successful authentication with the OAuth provider. This field accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can return users to whichever site they started from. For example, `{{device.location.scheme}}://{{device.location.hostname}}/welcome` sends the user back to the domain hosting the flow. This overrides the redirect URL configured for the OAuth provider, but not one your application passes when it starts the flow. See [redirect URL precedence](/flows/dynamic-keys#redirect-url-precedence), for more details. #### Prompt All OAuth actions can include a **Prompt** parameter, that can alter the behavior of the OAuth provider when redirecting to it. The different `prompt` values can be: - **Login** - The login prompt forces the user to re-authenticate, regardless of whether they are already logged in or have an active session with the OAuth provider. - **Consent** - The consent prompt forces the OAuth provider to re-display the consent screen, asking the user to agree to the requested permissions (scopes) again, even if they have already granted consent. - **Select Account** - The select_account prompt forces the user to select which account they want to use if they have multiple accounts logged in with the OAuth provider. - **None** - The none prompt forces the authentication to complete without showing any UI to the user. #### Use a default provider Override to specify a specific OAuth provider to authenticate with. This is useful if you want to use generic buttons to login to a specific OAuth provider, like Google or Facebook. This is not needed if using the [pre-generated OAuth provider buttons](/flows/screens/buttons#sign-in-buttons) in the Screen editor. #### Open in Popup OAuth login will open in a popup. #### Login Hint `login_hint` parameter to pass to the OAuth provider. This can be used to pre-fill the username/email field on the OAuth provider's login page. #### Force Non-Native OAuth Force web-based OAuth login instead of [native OAuth login](/auth-methods/oauth/with-sdks/mobile#native-oauth) on mobile. ### Flow Screens When using OAuth, you can use either the **Default Provider** buttons or normal buttons and connect them to the various OAuth actions mentioned above. ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/oauth/settings) Customize your social login (OAuth) authentication settings with Descope. # Social Login (OAuth) Settings Configure your social login (OAuth) settings from the [Descope Console](https://app.descope.com/settings/authentication/social) under **Settings → Authentication Methods → Social Login**. ## Enable Method in API and SDK The **Enable method in API and SDK** toggle controls whether OAuth authentication can be invoked programmatically via APIs and SDKs. * **When enabled**: Authentication works via flows, APIs, and SDKs * **When disabled**: Authentication only works with flows or calls made with a valid management key This global setting applies to all OAuth providers. Individual providers can have their own overrides for this setting. ## Connection Settings Every OAuth provider will have a **Connection Settings** section that allows you to configure the provider's connection details. This section will include the provider's client ID, client secret, and redirect URI. In addition, you'll be able to configure other settings specific to the provider, such as the **Prompt** and whether or not to use **PKCE**. ## User Attribute Mapping The **User Attribute Mapping** section allows you to map the user attributes from the provider to the user attributes in your application. For example, if the provider returns an `email` attribute, you can map it to the `email` attribute in your application. Every OAuth provider will allow you to map any attributes from the provider's `id_token` or `/userinfo` endpoint. ## Provider-Specific Settings Each OAuth provider has its own configuration options that vary depending on whether you're using: * **Default providers** (e.g., Google, Microsoft, GitHub) - See the [default providers](/auth-methods/oauth/providers#default-providers) documentation. * **Custom providers** - See the [custom providers](/auth-methods/oauth/providers/custom-providers) documentation. For detailed configuration instructions, refer to the documentation for your specific provider type. # One-time Password (OTP) (/auth-methods/otp) Customize your one-time password (OTP) authentication flow with Descope. # One-time Password (OTP) A one-time password (OTP) is an automatically generated string sent to the user during the sign-up or sign-in process to authenticate that user. The OTP can be sent to an email address, phone number (as a voice message), or a mobile phone (as an SMS/text message). A typical method for implementing OTP has two sets of functionality you need to program: user interaction and session verification. For a full list of customizable settings, see [OTP Settings](/auth-methods/otp/settings). ## OTP with Flows This guide will walk you through integrating OTP-based authentication into your Descope Flows. One Time Passwords (OTPs) allow your users to authenticate using a code sent to their devices, either in an email, SMS, or voice message, without the need for a password. ### Flow Actions When using OTP Login, you have the standard actions for most authentication methods available. - **Sign Up / OTP** - Signs the user up, but will not work if user already exists - **Sign Up or In / OTP** - Signs the user in, and if user doesn't exist it will automatically sign them up - **Sign In / OTP** - Signs the user in, but if the user doesn't exist it will fail. - **Update User / OTP** - Will merge the OTP user to an existing user ### How to Use OTP Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **Sign Up or In** action in a flow: ![sign-up-or-in-flow-action](/assets/sign-up-or-in-otp-flow-action.webp) ### Flow Screens When using OTP, there will typically be a screen where you input your OTP code, which is included as part of the actions above. All you need to do to use OTP, is to connect the actions to a screen that has a **Custom Login ID**, **Email**, or **Phone** input. ![otp-code-verification](/assets/otp-code-verification.webp) ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. #### Failed Verification Attempts When a user runs out of OTP attempts, they get the same action error as someone who just mistyped the code once, there's no way to distinguish the two from the error alone. To tell them apart, check the [remainingOTPAttempts](/flows/conditions/remaining-otp-attempts) dynamic value. It returns the number of attempts the user has left, and returns empty once they're locked out. This lets you warn users as they approach the limit, and show a custom message once they're locked out, instead of the generic verification error. You configure the attempt limit itself in [OTP Settings](/auth-methods/otp/settings#verification-attempts). # Settings (/auth-methods/otp/settings) Customize your one-time password (OTP) authentication settings with Descope. # OTP Settings Customize your one-time password (OTP) authentication settings from the [Descope console (Settings > Authentication Methods > One-time Password)](https://app.descope.com/settings/authentication/otp). OTP settings can also be overridden per tenant. See [tenant authentication methods](/management/tenant-management/tenant#one-time-password-otp). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether OTP authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Domain The domain is the URL domain used in email or text message sent to the end user. ### Expiration Time For increased security, we recommend an expiration time of 3-5 minutes. Define length of time after which link or code expires. A shorter expiration time limits how long a malicious actor has to attempt an attack (such as a dictionary or brute force attack) on the code or link. ### Number of Retries and Attempts Timeframe (seconds) Limit the number of communication attempts (email, text, or voice) a recipient can receive within the defined timeframe. If the limit is exceeded, no further messages will be sent until the timeframe resets. ### Verification Attempts Limit how many times a user can enter an incorrect OTP code before the code is invalidated. Once the limit is reached, the code is discarded and the user gets a max attempts exceeded error ([E061103](/common-errors)) instead of an invalid code error ([E061102](/common-errors)). To continue, the user has to restart authentication and request a new code. When running a flow, you can check how many attempts a user has left using the [remainingOTPAttempts](/flows/dynamic-keys) dynamic value. Use it to warn users as they approach the limit, or to show a custom message once they're locked out. See [remainingOTPAttempts Condition](/flows/conditions/remaining-otp-attempts). ### Allow Unverified Recipient Email Addresses or Phone Numbers Enabling this option may increase the risk of spam and fraud. By default, messages are sent only to verified email addresses or phone numbers. When enabled, this option allows messages to be sent to [unverified email addresses or phone numbers](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#understanding-unverified-emails). If a user authenticates using an unverified contact, Descope automatically marks that contact as verified. ## Connectors A full list of messaging related connectors can be found on our [Messaging Connectors Guide](/connectors/connector-configuration-guides/messaging) page. ### Email Connectors Descope supports sending email OTP messages using your email messaging provider, such as AWS SES, SendGrid, or a generic SMTP service. You can configure a email messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported email messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Text Message (SMS) Connectors Descope supports sending text messages using your text messaging provider, such as Twilio or Amazon SNS. You can configure a text messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported text messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Voice Message Connectors Descope supports sending voice OTP messages using your voice messaging provider, such as Twilio. You can configure a voice messaging connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported voice messaging connectors. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and customize the template if you would like. ### Instant Messaging (WhatsApp) Connectors Descope supports sending text messages using your text messaging provider Twilio Verify. You can configure this connector by going to the [connectors page](https://app.descope.com/connectors) within the Descope console and searching for the supported text messaging connector. Then, on the [OTP authentication method](https://app.descope.com/settings/authentication) page, you can select the configured connector and test the feature by providing a test phone number. This functionality requires no template. ### Fallback Connector To each of the connectors in this authentication method, Descope supports a fallback connector that is available to use to prevent downtimes. Descoper can configure a fallback connector that will be used in case the active connector isn’t available. This fallback connector can either be Descope's default connector or an alternate connector configured for sms/voice/email. ![Fallback for OTP](/assets/fallback-otp.webp) ## Templates If you are using a customized connector, you can change the template of the email/sms which your user will receive. The default is System. # Passkeys (WebAuthn) (/auth-methods/passkeys) Customize your WebAuthn authentication flows ( \ biometrics \ passkeys) with Descope. # Passkeys (WebAuthn) Biometrics within the context of WebAuthn lets you authenticate end users using the strong authenticators that are now often built right into devices, including biometrics (fingerprint, facial, or iris recognition) and secure hardware keys (passkeys) like those provided by Yubico, CryptoTrust, or Thedis. Try out Passkeys for yourself, at [Passkeys Guru](https://passkeys.guru)! ## Domain Specific Passkeys Descope's passkey implementation is domain-specific, which means that passkeys are tied to the domain where they were created. In scenarios where you have multiple applications on multiple different domains using the same Descope project, you can utilize the `user.webAuthn` key in a flow condition to prompt for passkeys. Descope will check whether a valid passkey has been created for the specific domain the user is on. If **true**, it means that a passkey has been stored for the current domain only. ![passkey domain specific](/assets/passkey-specific-domain.webp) ## Physical Keys Passkeys can also be implemented using physical security keys like YubiKeys. To learn more about YubiKey authentication, check out our blog [here](https://www.descope.com/learn/post/yubikey-authentication). ## Passkeys with Flows This guide will walk you through integrating Passkey-based authentication into your Descope Flows. Passkeys offer a secure, passwordless login experience based on `FIDO2` and `WebAuthn` standards. ### Flow Screens When using Passkeys, you can use the **Passkey** component to enable them in your Flow screens. There is also the **Biometrics** action, which also operates according to the `WebAuthn` standards, but enforces the use of biometrics to login specifically. #### Passkey Autofill Passkeys rely on a unique identifier to associate them with a user. This is typically a phone number or email address. Descope offers an **autofill** feature that will allow the user to select their Passkeys that exists on the associated domain, without having to type it in. ![enable-passkey-autofill](/assets/enable-passkey-autofill.webp) ### Flow Actions When using Passkeys, the following actions are available: - **Sign Up / Passkeys** - Registers a new user with a Passkey; will fail if user already exists. - **Sign Up or In / Passkeys** - Registers or authenticates the user with a Passkey. - **Sign In / Passkeys** - Authenticates existing user with a Passkey; will fail if user does not exist. - **Update User / Passkeys** - Updates the user's information after Passkey authentication. Each of these actions supports standard error handling, including scenarios where a user cancels the passkey prompt (e.g., closing the authentication window or dismissing the biometric dialog). In these cases, an "authentication aborted" error is exposed and can be handled as part of your flow logic. #### How to Use Passkeys Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. These actions are straightforward and can be integrated into your application like any other Action. This is an example of using the **Authenticate with Passkey** action in a flow: ![authenticate-with-passkey-flow-action](/assets/sign-up-or-in-passkey-flow-action.webp) #### Restrict Types of Passkeys When using Passkeys, you as the Descoper can restrict the types of Passkeys allowed for the user to sign up or in. - **Platform Passkeys** - Passkeys that are stored on the local device or profile of the user. - **External Passkeys** - Passkeys stored on other devices that are shared via security keys or mobile phones over Bluetooth. You can control this restriction in the dropdown here, underneath the Passkey flow actions: ![passkey-flow-action](/assets/passkey-flow-action.webp) #### Allow Passkeys Without User Verification Enabling this will allow your users to authenticate with passkeys, without using biometrics or by using a security key **without** a PIN. ### Check if Device Supports WebAuthn Before allowing a user to authenticate with passkeys, you may want to check if the user's device supports WebAuthn, and have them authenticate with a different method if not. You can use the `device.webAuthnSupport` key within a condition to check this. ![check if webauthn supported](/assets/webauthn-supported.webp) ### Multiple Passkeys per User Descope doesn't limit a user to a single passkey. Each successful **Update User / Passkeys** action registers an additional passkey on the user's account — for example, one per device or browser — without affecting any passkeys the user already has. This lets users sign in with whichever device is on hand, rather than being tied to the one they originally signed up with. End users can view every passkey registered to their account, and remove a specific one, through the [User Profile Widget](/widgets/users#passkeys). ### Error Handling Passkey errors are handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. #### Passkey Failure Logging When a WebAuthn ceremony fails or is canceled, Descope captures additional context from the client SDK and writes it to the [Troubleshooting Logs](https://app.descope.com/audits/tlogs). This applies to all passkey finish actions: **Sign Up**, **Sign In**, **Sign Up or In**, and **Update User**. The following fields are logged on failure: | Field | Description | | --- | --- | | `failure` | The raw `DOMException` name from the browser (e.g. `NotAllowedError`, `InvalidStateError`). | | `failure_reason` | A best-effort categorization of the failure from the client SDK, such as user cancellation, timeout, or no available devices. | | `failure_message` | The raw `DOMException` message from the browser, truncated to 256 characters. | These fields are intended for support and troubleshooting. They do not affect the user-facing passkey experience or authentication outcomes. **Already-registered device**: If a user attempts to add a passkey that is already present on the device, the browser returns an `InvalidStateError`. Descope catches this and returns the friendlier error: **"This device has already been added as a passkey."** The `failure_reason` and `failure_message` fields are still logged in this case. # Settings (/auth-methods/passkeys/settings) Customize your passkeys authentication settings with Descope. # Passkeys Settings Customize your WebAuthn authentication from the [Descope console (Settings > Authentication Methods > Passkeys)](https://app.descope.com/settings/authentication/webauthn). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether passkeys authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Display Name The **Display Name** is the human-readable name shown to users when they create or use a passkey. By default, it uses your project name. You can set it to any text you want—for example, your product or company name. Whether users see the display name depends on their password manager. Some, such as Bitwarden, show it; others only show the top-level domain. ### Top Level Domain The configured domain defines where end users can set up biometric authentication. The top-level Biometrics (WebAuthn) domain restricts where users are allowed to authenticate, applying to both the domain itself and all its subdomains. By default, Descope automatically derives this top-level domain from the request origin. Changing the top-level domain for Biometrics (WebAuthn) in the Descope UI can invalidate existing users if the new domain no longer matches the one they originally registered with. Users who rely solely on WebAuthn will be unable to log in and must be deleted and recreated. Users with other verified authentication methods can still sign in through those methods, though registering with WebAuthn again may create a new credential. ### Android Fingerprints When a passkey operation runs inside an Android app, the app reports its origin as an APK key hash (`android:apk-key-hash:...`) rather than your web origin. Use **Android Fingerprints** to declare which Android apps can complete passkey operations in your project. Add the SHA-256 fingerprints of the Android keystores used to sign your Android APK, in colon-separated hex format. Each fingerprint is 32 pairs of hex digits separated by colons. Find the Android keystore file used to sign your Android app and run this command: `keytool -list -v -keystore my_keystore.jks` What the list contains changes how Descope treats Android origins: - **Empty (default)**: Descope uses the origin recorded when the passkey operation started and checks no fingerprints. - **One or more fingerprints**: Descope accepts passkey sign-up, sign-in, and add-device operations from Android apps signed with a listed key. An app that reports an APK key hash with no matching fingerprint gets error [`E067020`](/common-errors). Clients that report a web origin, including browsers and apps that open the flow in a system browser or Custom Tab, are unaffected. The list covers every Android app in your project, not only the app you added it for. Include each signing key you use, such as your release key, your debug key, and, if you use Play App Signing, the app signing key Google generates rather than your upload key. Miss one and that build can no longer use passkeys. If your Android app hosts a Descope flow in its own WebView, see [Passkeys with Mobile SDKs](/auth-methods/passkeys/with-sdks/mobile) for how this setting applies. # Passwords (/auth-methods/passwords) Customize your password authentication flow with Descope. # Passwords The Passwords Authentication Method lets you authenticate end users using a secret string of characters known only to the user. Descope recommends using an email address as the user identifier; this allows you to utilize passwordless methods like Magic Link in addition to passwords. These methods could be used for authentication when users forget their password or need to reset it easily. ## Passwords with Flows This guide will walk you through integrating Password-based authentication into your Descope Flows. Passwords are pretty self explanatory, but there are important aspects to how to manage their use in the flow screens and with flow actions. ### Flow Actions When using Passwords, you have the standard actions for most authentication methods available. - **Sign Up / Password** - Signs the user up, but will not work if user already exists - **Sign In / Password** - Signs the user in, but if the user doesn't exist it will fail. - **Update Password** - Will add a password, onto an existing user's identity for login capabilities. The user must already be signed in at this point in the flow. - **Replace Password** - Will allow a user to update their current password, using their existing password. - **Send Password Reset** - Will send a password reset email to the user, after typing in their email address. - **Expire Password** - Will allow admins to expire the current password for a logged-in user. ### How to Use Password Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these actions are pretty simple, and you can drop them in your flow like any other Action. This is an example of using the **Sign In / Password** action in a flow: ![sign-in-flow-action](/assets/sign-in-password-flow-action.webp) ### Flow Screens When using Passwords, there are a few different screen components you can choose to use in your flow. #### Email / Phone vs. Username When using Passwords, it's common to use a username instead of a phone or email address as the unique login ID of the user. In that case, you'll want to make sure you use the **Custom Login ID** component, instead of the **Email** or **Phone** component in your flow to sign the user in and up. This is so that you can create any username you want, without it having to be a sanitized as an email address or phone number. ![custom-login-id-component](/assets/custom-login-id-component.webp) You can just rename the component to **Username**, and whatever is used in that field will be what the sign up or sign in actions afterwards rely upon to identify the user in question. #### Entering Passwords in Screens There are two main components for entering a password. - **Login Password** - You use this component for your sign in screens. This will not work with new users and the sign up actions. - **New Password** - You use this component for your sign up screens. This will not work with existing users and the sign in actions. The **Sign Up / Password** and **Update Password** actions accept the password from either component, so you can use **Login Password** on a screen where the confirmation field and policy previewer would be out of place. See [Using It to Set a Password](/flows/screens/inputs/passwords#using-it-to-set-a-password) for details. ### New Password Component Features The **New Password** component also has some additional features in the behavior tab, designed to help sign the user up more securely. #### Password Confirmation You can require a re-confirmation of the entered password with this feature enabled. ![password-confirmation](/assets/password-confirmation.webp) #### Policy Previewer If you have password strength enforcement enabled that will show as a bar under the password input even if the policy previewer is disabled. You can have your users preview the password policy you set for them under [Authentication Methods -> Passwords](https://app.descope.com/settings/authentication/password), as they are typing their new password. ![policy-previewer-password](/assets/policy-previewer-passwords.webp) #### Tenant-Based Password Policies You can define password policies on a per-tenant basis, allowing different tenants to enforce different password strictness rules. Password policies are evaluated based on the tenant context available to the flow at runtime. Once the tenant is provided to the flow, Descope automatically enforces and exposes the corresponding password policy requirements so they can be displayed on password input screens. The password policy text shown in the policy previewer can be translated using Descope's [Localization](/management/localization) feature. In order to pass tenant context into the flow, for use of tenant-level password policies, you can: - **Manually set a Tenant in the Flow** - This ensures that the flow always applies the password policy associated with that tenant. Because the tenant is known when the flow renders initially, the password screen can immediately enforce the correct password strictness and display the tenant-specific password requirements. - **Separate Email and Password Inputs** - This allows the flow to dynamically select the relevant tenant-level policy based on the user's email domain before presenting the password input screen to the user. In this scenario, the user will first be prompted to enter their email address, and once the tenant is known to the flow, the correct password policy will be displayed on the password input screen. ![Tenant Based Password Policy Example](/assets/password-strictness-flow.webp) ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. ### PIN Codes as Passwords In addition to traditional alphanumeric passwords, Descope also supports using PIN codes as the user's password. PIN codes are simply shorter numeric passwords that can be collected using the [**One Time Code**](/flows/screens/inputs/one-time-code) input component in your flow screens. From the perspective of authentication, PIN codes are treated the same as passwords and can be used with all the same flow actions (Sign In, Sign Up, Update, Replace, etc.). This allows you to offer a simpler login experience in cases where full password complexity is not required, while still maintaining control through password policies and reset flows. # Settings (/auth-methods/passwords/settings) Customize your password authentication settings with Descope. # Password Settings Customize your Password authentication flow from the [Descope console (Settings > Authentication Methods > Passwords)](https://app.descope.com/settings/authentication/password). ### Password Policy Password policy forces users to select more robust passwords. We have chosen a default policy that corresponds with current best practices. You can change the password policy to make it more or less restrictive. Note that if you desire more restrictions, it may be harder for your users to remember the password they have chosen, and if you choose a less restrictive policy, passwords may be more easily compromised. Password policy can also be overridden at a tenant level. More information about tenant level password policy can be found [here](/management/tenant-management/tenant#passwords). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Password authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. | Setting | Details | | ------- | --------| | Enable method in API and SDK | This toggle switch enables or disables the authentication method from being available for use within API and SDK | | Minimum Password Length | Require users to choose a password equal to or longer than the number of characters specified. | | Require at least one letter (any case) | Require users to use at least one alphabetic character (a-z or A-Z) in their password. | | Require at least one lowercase character | Require users to use at least one lowercase character in their password. | | Require at least one uppercase character | Require users to use at least one uppercase character in their password. | | Require at least one number | Require users to use at least one numeric character (0-9) in their password. | | Require at least one special character | Require users to use at least one non-alphanumeric character in their password. | | Disallowed characters | Characters that must not appear in the password. Leave empty to allow any character otherwise permitted by the policy. | | Disallow passwords identical to the user's email | Block passwords that match the user's full email address, or the portion before `@` (case-insensitive). | | Enable Password Expiration | When enabled, the user's password will expire after a specified period (in weeks), and the user will have to change their password. | | Prevent Password Reuse | Specify how many previously used user passwords Descope will remember. When selecting a new password (e.g., after reset or password expiration), Descope will not allow using any previously used passwords. | | Lock account after x attempts | When a user enters an incorrect password more than x times, the user will be locked and unable to log in again. | | Temporary lock after x attempts, for y minutes | When a user enters an incorrect password more than x times, the user will be temporarily locked and unable to log in for y minutes. After y minutes the user will be able to log in again. | | Password Strength Enforcement | Enforces a minimum password strength that passwords must match or exceed in order to be registered successfully. | | Password Reset Email Connector | The messaging provider used to send password reset emails (sent via Magic Link). The default is Descope. You can read more about messaging connectors [here](/connectors/connector-configuration-guides/messaging). | | Template | If you are using a [custom messaging connector](/connectors/connector-configuration-guides/messaging), you can change the template of the email which your user will receive. The default is System. | ### Password Expiration With **Enable Password Expiration** on, Descope sets each password's expiration date from the last time that password changed, plus the configured period. The date you changed the setting has no effect. A new expiration period therefore applies to existing passwords as soon as you save it: - Shortening the expiration period can cause passwords to expire all at once. If you lower it from 20 weeks to 4 weeks, every user whose password was last changed more than 4 weeks ago will be required to set a new one the next time they log in. - Lengthening the period can make expired passwords valid again, as long as the new period hasn't yet elapsed since the user's last password change. - Turning the setting off stops Descope from enforcing expiration. Turning it back on causes Descope to recalculate expiration from each user's last password change. If you expire a password yourself, through the **Set/Expire Password** action in the console, the [Expire User Password](/api/management/users/expire-user-password) API, or the `Expire Password` flow action, it stays expired whatever period you configure. The user must set a new password, which restarts the clock. To check whether a user's password has expired inside a flow, use the [`user.passwordExpired` dynamic key](/flows/dynamic-keys#user). Tenant-level password policies follow the same rule. For a user who belongs to several tenants, Descope applies the shortest expiration period. See [tenant password settings](/management/tenant-management/tenant#passwords). ### Reset Password Email This email will be sent to the user via the Magic Link method when the end user initiates a password reset process (e.g. when the user clicks the “forgot my password” link or when triggered by the admin in the Descope Console or API). On the authentication methods page, you can find the Reset Password Email settings. Here you can customize the email connector used to send the reset password email, as well as the email template. ![Reset Password Settings](/assets/reset-password-settings.webp) ### Allow Unverified Recipient Email Addresses or Phone Numbers Enabling this option may increase the risk of spam and fraud. By default, messages are sent only to verified email addresses or phone numbers. When enabled, this option allows messages to be sent to [unverified email addresses or phone numbers](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#understanding-unverified-emails). If a user resets their password using an unverified contact, Descope automatically marks that contact as verified. ### Reset Password In flows You can also use the `Send Password Reset` action within your flow. ![Send Password Reset](/assets/send-password-reset.webp) Within this action, you can customize the email connector used to send the reset password email, as well as the email template. Additionally, you can select custom token verification, so that you can include additional conditions before verifying. This is useful in detecting if an email scanner has clicked on the magic link in the email, and prevent token verification in that scenario. ![Password Reset Action](/assets/password-reset-action.webp) The action's **URI** field sets where the user lands to complete the reset, and accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can serve several sites. For example, `{{device.location.scheme}}://{{device.location.hostname}}/new-password` builds the URL from the domain hosting the flow. Left empty, the reset link falls back to the [Redirect URL](/auth-methods/magic-link/settings#redirect-url) in your magic link settings, since Descope sends password reset emails via Magic Link. ### Set/Reset Password Option You can use the [`user.passwordExpired` dynamic key](/flows/dynamic-keys#user) in your flow to check if the user's password has expired. Descopers now have the ability to set or reset user's password from the User's page under manage users. This will help in setting them the password for their first login. In the user's table, against a user, click on the actions menu (3-dots) and select the "Set/Expire Password" option. ![Set or Expire User Password](/assets/set-reset-password.webp) Once you click on this setting, you get the following dialog box with options to set a temporary password for that user. You can toggle to force the user to change their password on their next login. ![Set Temp Password](/assets/set-temp-password.webp) This follows by Descope presenting you a temporary password for that user to login next time with. ![Display Temp Password](/assets/display-temp-password.webp) ### Password Error Sensitivity If "Hide sensitive error information" under Password Policy settings has been enabled, the users will get a generic password failure message, but if there's a need to display specific password error message, make sure to toggle off this option. With toggle enabled, the response would have the following error information displayed. This hides any sensitive information on the error. ![Hide Sensitivity Enabed Password](/assets/password-error-hide-info.webp) However, once the toggle is off, the error message provides detailed feedback specific to the entered password, offering the user greater clarity about the issue. ![Hide Sensitivity Disabled Password](/assets/password-error-unhide-info.webp) # Push Authentication (/auth-methods/push) Add push notification authentication to your mobile app using the Descope mobile SDKs. Includes console setup, connector configuration, and sample code. # Push Authentication Push authentication lets a user approve a sign-in request using their mobile device, assuming they are already signed in to an app that is linked to the same Descope project. For example, the user might already be signed in to an app on their phone, and they're trying to sign in on their laptop browser to the website. Using push authentication, Descope sends a push notification to their mobile device. The user taps **Approve** in the notification or in an in-app UI, and the sign-in completes. The mobile app has two responsibilities: 1. **Enroll** the device by registering its push token with Descope while the user has an active session. 2. **Handle** the incoming push notification and report the user's decision back to Descope. ## Set Up Push Notifications Push authentication leverages the standard platform push services, so the app must first be able to receive normal push notifications. This is standard iOS/Android setup and is documented by Apple and Google. - **iOS (APNs)** — Enable the *Push Notifications* capability, request notification permission, and register for remote notifications to obtain a device token. See Apple's [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns). - **Android (FCM)** — Add Firebase to your app, include the Firebase Messaging SDK, and implement a `FirebaseMessagingService` to receive messages and the FCM registration token. See Google's [Set up a Firebase Cloud Messaging client app on Android](https://firebase.google.com/docs/cloud-messaging/android/client). Once your app can receive a push token and display notifications, continue below. ## Enable Push Authentication Configure the [Apple Push Notification (APN)](/connectors/connector-configuration-guides/messaging/apple-push-notification) and [Firebase Cloud Messaging (FCM)](/connectors/connector-configuration-guides/messaging/firebase-cloud-messaging) connectors that deliver the notifications in the Descope Console. Then, in the [Push Authentication settings](/auth-methods/push/settings), select these configured connectors for each platform: iOS and Android. ## Handle the Notification Payload Descope sends the notification with a `transactionId` that identifies the pending sign-in. Your app reads this value and passes it to the SDK when the user responds. The payload also includes `userId` and `projectId` for reference. The APNs payload carries the transaction ID alongside a standard `aps` alert. The category is `descope_sign_in`, which you can use to attach custom notification actions. ```json APNs payload { "aps": { "alert": "You have a new sign-in request", "sound": "default", "category": "descope_sign_in", "content-available": 1 }, "transactionId": "", "userId": "", "projectId": "__ProjectID__" } ``` Read the values from the notification's `userInfo` when it arrives. ```swift Swift func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { guard let transactionId = userInfo["transactionId"] as? String else { completionHandler(.noData) return } // Show a notification with Approve / Deny actions that carry the transactionId, // then call Descope.push.finish() when the user responds. showPushAuthNotification(transactionId: transactionId) completionHandler(.newData) } ``` Descope sends a data message so your `FirebaseMessagingService` is invoked whether the app is in the foreground or background. Read the values from `message.data`. ```json FCM payload { "data": { "title": "Sign-in Request", "body": "You have a new sign-in request", "transactionId": "", "userId": "", "projectId": "__ProjectID__" } } ``` ```kotlin Kotlin override fun onMessageReceived(message: RemoteMessage) { val transactionId = message.data["transactionId"] ?: return val title = message.data["title"] ?: "Sign-in Request" val body = message.data["body"] ?: "" // Show a notification with Approve / Deny actions that carry the transactionId, // then call Descope.push.finish() when the user responds. showPushAuthNotification(transactionId, title, body) } ``` ## Using the SDK Both SDK functions require the `refreshJwt` from an active `DescopeSession`, so the user must already be signed in on the device. ### Enroll the Device Register the device's push token after the user signs in and grants notification permission. Call `enroll` again whenever the platform issues a new token. On iOS the device token arrives as `Data` in `didRegisterForRemoteNotificationsWithDeviceToken`. Convert it to a hex string before enrolling. Set `development` to `true` for debug builds that use the APNs sandbox, and `false` for production/TestFlight builds. ```swift Swift func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { let token = deviceToken.map { String(format: "%02x", $0) }.joined() guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } Task { do { try await Descope.push.enroll(token: token, development: false, refreshJwt: refreshJwt) } catch { print("Failed to enroll device: \(error)") } } } ``` On Android the token comes from FCM (`onNewToken` or `FirebaseMessaging.getInstance().token`). ```kotlin Kotlin val refreshJwt = Descope.sessionManager.session?.refreshJwt ?: return try { Descope.push.enroll(token = fcmToken, refreshJwt = refreshJwt) } catch (e: DescopeException) { // Handle enrollment errors } ``` ### Approve or Deny a Request When the user responds to the notification, call `finish` with the `transactionId` from the payload and whether the request was approved. ```swift Swift func completePushAuth(transactionId: String, approved: Bool) async { guard let refreshJwt = Descope.sessionManager.session?.refreshJwt else { return } do { try await Descope.push.finish(transactionId: transactionId, approved: approved, refreshJwt: refreshJwt) } catch { print("Failed to complete push auth: \(error)") } } ``` ```kotlin Kotlin suspend fun completePushAuth(transactionId: String, approved: Boolean) { val refreshJwt = Descope.sessionManager.session?.refreshJwt ?: return try { Descope.push.finish(transactionId, approved, refreshJwt) } catch (e: DescopeException) { // Handle errors } } ``` After `finish` succeeds, the pending sign-in on the other device completes with the matching result. # Settings (/auth-methods/push/settings) Configure push authentication and its APNs and FCM connectors with Descope. # Push Authentication Settings Configure [Push Authentication](/auth-methods/push) from the [Descope console (Settings > Authentication Methods > Push)](https://app.descope.com/settings/authentication/push). ![Push Authentication Settings](/assets/push-authentication-settings.webp) ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Password authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Connectors If you only ship an app for one platform (iOS or Android), configure the matching connector and leave the other empty. Push notifications are delivered through messaging connectors configured on the [Connectors](https://app.descope.com/connectors) page. Create the connector for each platform you support and select it in the settings above. ### APNs (iOS) Create an [Apple Push Notification (APN)](/connectors/connector-configuration-guides/messaging/apple-push-notification) connector and provide your Team ID, Bundle ID, Key ID, and Private Key (`.p8`) from your Apple Developer account. Then, assign the connector in this settings page. ### FCM (Android) Create a [Firebase Cloud Messaging (FCM)](/connectors/connector-configuration-guides/messaging/firebase-cloud-messaging) connector and provide your Firebase service account key (and optionally a Notification Channel ID). Then, assign the connector in this settings page. # Recovery Codes (/auth-methods/recovery-codes) Recovery codes with Descope. # Recovery Codes Recovery codes serve as backup authentication codes that help users regain access to their account when they can't use their primary second factor authentication method, such as a [TOTP App](/auth-methods/auth-apps) or [device with passkey](/auth-methods/passkeys). These codes provide a secure way to reset multi-factor authentication (MFA) when primary methods are unavailable. ## Recovery Codes with Flows This guide will walk you through integrating Recovery Codes into your Descope Flows. ### Flow Actions When using Recovery Codes, there is both the initial generation action and the sign in action: - `Recovery Codes / Generate` - Will generate recovery codes for the user according to your [project settings](/auth-methods/recovery-codes/settings). - `Sign In / Recovery Code` - Enable users to sign in using a recovery code. Recovery codes are single-use only. After a successful `Sign In / Recovery Code` action runs, the used code becomes invalid. Additionally, generating new recovery codes through the `Recovery Codes / Generate` action will invalidate all previously existing codes. ### Flow Screens When displaying recovery codes after the `Recovery Codes / Generate` action, use the **Recovery Codes** component. ![Recovery codes display component](/assets/recovery-codes-display.webp) When having the user enter their recovery code before the `Sign In / Recovery Code` action, use the **Recovery Code Input** component. ![Recovery code input component](/assets/recovery-code-input.webp) For working examples, refer to our [Add MFA with TOTP and Recovery Codes](https://app.descope.com/flows?template=add-totp-mfa-with-recovery-codes) and [Sign In with TOTP or Recovery Codes](https://app.descope.com/flows?template=sign-in-with-totp-or-recovery-codes) flow templates. ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/recovery-codes/settings) Customize your recovery codes settings with Descope. # Recovery Codes Settings Customize your [Recovery Codes](/auth-methods/recovery-codes) Settings from the [Descope Console (Authentication Methods > Recovery Codes)](https://app.descope.com/settings/authentication/recoverycodes). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Recovery Codes authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Number of Recovery Codes Provided Upon Each Generation Define the number of recovery codes provided upon each generation. Number of recovery codes can be between 1 and 10. Default is 10. ### Lock After x Attempts Choose whether or not to block access after a set number of attempts. Disabled by default. Number of attempts can be between 2 and 10. ### Temporary Lock After x Attempts, For y Minutes Temporarily lock user from using recovery codes after a number of failed attempts, for a specified period of time. Disabled by default. Number of attempts can be between 2 and 10. ![Recovery Codes Settings](/assets/recovery-codes-settings.webp) # Security Questions (/auth-methods/security-questions) Customize your security questions mfa flow with Descope. # Security Questions Descope Security Questions let you verify an already authenticated user by having them answer one or more questions with answers known only to the user. It is designed to be used post-authentication and with other authentication methods to allow for a more secure password reset and step-up functionality. ## Security Questions with Flows This guide will walk you through integrating Security Question-based MFA into your Descope Flows. Security questions are pretty self explanatory, but there are important aspects to how to manage their use in the flow screens and with flow actions. ### Flow Actions When using Security Questions, there is the initial setup action and the verification action for MFA: - **Security Questions / Setup** - Will enable an authenticated user to set up security questions. - **Security Questions / Verify** - Verifies authenticated user's answer to a security question. ### How to Use Security Questions Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. These actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **Security Questions / Setup** action in a flow: ![Security Questions Setup Flow](/assets/security-questions-setup-flow.webp) This is an example of using the **Security Questions / Verify** action in a flow: ![Security Questions Setup Flow](/assets/security-questions-verify-flow.webp) ### Flow Screens When using the Security Questions screen components the user must be authenticated before reaching these components in your flow. #### Entering Security Questions in Screens There are two main components Security Questions. - **Security Questions Verify** - You use this component for your MFA screens. One question is chosen at random. - **Security Questions Setup** - You use this component for your sign up screens. This will not work with the verify action. To see an example flow with Security Questions go to [Flows](https://app.descope.com/flows)-> Start from template -> Security Questions ### Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Settings (/auth-methods/security-questions/settings) Customize your security questions authentication settings with Descope. # Security Questions Settings Customize your Security Questions from the [Descope console (Settings > Authentication Methods > Security Questions)](https://app.descope.com/settings/authentication/securityquestions). ## Enable Method in API and SDK The Enable method in API and SDK toggle controls whether Security Questions authentication can be invoked programmatically via APIs and SDKs. - **When enabled**: Authentication works via flows, APIs, and SDKs - **When disabled**: Authentication only works with flows or calls made with a valid management key ## Available Settings This section describes additional details about the configuration options available. ### Questions List The default setup includes five security questions, which you can customize by modifying, removing, or adding your own. Add or remove questions from this list to make them available to user during setup. Number of questions can be between 2 and 50. Default is 5. ### Require at least x Questions from the End User Upon Setup The number of questions the user must answer during setup. During MFA verification, the user will be asked to answer a subset of these questions. Number of questions can be between 1 and 50. Default is 1. ### Require at least x questions from the end user during verification The number of questions the user must answer during verification. The questions will be randomly selected from the list of questions the user has answered during the initial setup. Number of questions can be between 1 and 50. Default is 1. ### Lock Account After x Attempts When a user answers questions incorrectly more than x times, the user will be locked and unable to log in again. Disabled by default. Number of attempts can be between 2 and 10. ### Temporary Lock After x Attempts, For y Minutes When a user answers questions incorrectly more than x times, the user will be temporarily locked and unable to log in for y minutes. After y minutes the user will be able to log in again. Disabled by default. Number of attempts can be between 1 and 10, default is 3 attempts. Number of minutes can be between 1 and 1440, default is 5 minutes. # Getting Started (/auth-methods/sso/getting-started) Add SSO to your app with the Descope backend SDK, then connect each customer's IdP with the SSO Setup Suite. # Getting Started with SSO Descope SSO is authentication middleware for your app. It runs the SAML/OIDC handshake with each customer's identity provider, and you keep your own user database and sessions. After SSO, you validate a Descope token once, look up the user, and start your session the way you already do. The easiest path has three parts: 1. Wire two backend routes with the Descope backend SDK (start and callback). Your frontend only needs a "Sign in with SSO" control, so you don't rebuild the rest of login. 2. Connect the customer's IdP with the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) (or MockSAML when you're just verifying the code). 3. Sign in once, end to end. If you want Descope to own the full login UI and sessions, add the [SSO action](/auth-methods/sso/with-flows) to a Flow and follow the main [Getting Started](/getting-started) guide. This page is for keeping your own auth stack. If your app runs the SSO handshake in the browser or on a device rather than on your server, use the [Client SDKs](/auth-methods/sso/with-sdks/client) or [Mobile SDKs](/auth-methods/sso/with-sdks/mobile) instead. ## What You'll Build 1. **[Add start + callback with the backend SDK](#step-1-add-sso-with-the-backend-sdk)** 2. **[Connect an IdP with the SSO Setup Suite](#step-2-connect-an-idp-with-the-sso-setup-suite)** (or MockSAML to test first) 3. **[Test end-to-end](#step-3-test-end-to-end)** SSO is always configured per tenant (one customer org, with one or more IdP connections). See the [SSO overview](/auth-methods/sso) if that model is new. ## Before You Begin 1. A [Descope project](https://app.descope.com) and its [Project ID](https://app.descope.com/settings/project) 2. One empty [tenant](https://app.descope.com/tenants) for the customer, or a dedicated test tenant. You don't need the IdP configured yet. 3. An app or API where you can add two routes | Term | Meaning | | ---- | ------- | | **Tenant** | Your customer's organization in Descope | | **SSO connection** | That tenant's SAML or OIDC link to their IdP | | **Descope token** | JWTs from a successful exchange. Validate them, then mint your own session | ## Step 1: Add SSO with the Backend SDK Your server starts SSO and exchanges the authorization code, so the code never sits in the browser. After the exchange, validate the Descope token, look up the user in your database, and create your session. This works the same as any other login method you already support. ### Start Endpoint: Pass the User's Email The simplest UX is to have the user type their work email. Your backend calls `sso.start` with that email, and Descope looks up the tenant whose [SSO Domain](/auth-methods/sso#option-1-sso-domains) matches (for example, `acme.com` resolves to Acme's IdP). You get back the authorization URL for the right tenant. You can also pass a tenant ID or name when your app already knows the org, whether from a subdomain, a picker, or an invite. For the full option list (`loginHint`, `ssoId`, `prompt`, `forceAuthn`, and more), see [Backend SDK start options](/auth-methods/sso/with-sdks/backend#start-options). ```javascript import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: process.env.DESCOPE_PROJECT_ID }); // POST /auth/sso body: { "email": "alex@acme.com" } export async function startSso(req, res) { const email = req.body.email; const redirectUrl = 'https://app.example.com/auth/sso/callback'; // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. loginHint pre-fills the user at OIDC IdPs. const resp = await descopeClient.sso.start(email, redirectUrl, undefined, undefined, undefined, false, email); if (!resp.ok) { return res.status(400).json(resp.error); } // Redirect the user's browser to the returned URL. res.redirect(resp.data.url); } ``` ```ts title="app/api/auth/sso/route.ts" import { NextRequest, NextResponse } from 'next/server'; import { sdk } from '@/lib/descope'; // POST /api/auth/sso body: { "email": "alex@acme.com" } export async function POST(req: NextRequest) { const { email } = await req.json(); const redirectUrl = 'https://app.example.com/api/auth/sso/callback'; // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. loginHint pre-fills the user at OIDC IdPs. const resp = await sdk.sso.start(email, redirectUrl, undefined, undefined, undefined, false, email); if (!resp.ok) { return NextResponse.json(resp.error, { status: 400 }); } // Redirect the user's browser to the returned URL. return NextResponse.redirect(resp.data.url); } ``` ```python from descope import DescopeClient from flask import redirect import os descope_client = DescopeClient(project_id=os.environ["DESCOPE_PROJECT_ID"]) # POST /auth/sso body: { "email": "alex@acme.com" } def start_sso(email: str): redirect_url = "https://app.example.com/auth/sso/callback" # Descope matches the email domain to the tenant's SSO Domain and returns # that tenant's IdP authorization URL. login_hint pre-fills the user at OIDC IdPs. resp = descope_client.sso.start(tenant=email, return_url=redirect_url, login_hint=email) # Redirect the user's browser to the returned URL. return redirect(resp["url"]) ``` ```go // GET/POST /auth/sso func startSso(w http.ResponseWriter, r *http.Request) { ctx := context.Background() email := "alex@acme.com" // or a tenant ID/name when you already know the org returnURL := "https://app.example.com/auth/sso/callback" // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL, and writes the redirect to w. _, err := descopeClient.Auth.SSO.Start(ctx, email, returnURL, r, nil, w) if err != nil { // handle error return } // The SDK wrote the redirect to w. The URL is also returned if you want to redirect yourself. } ``` ```java // GET/POST /auth/sso (email = "alex@acme.com") SAMLService ss = descopeClient.getAuthenticationServices().getSAMLService(); String returnURL = "https://app.example.com/auth/sso/callback"; // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. String url = ss.start("alex@acme.com", returnURL, null); // Redirect the user's browser to the returned URL. return new RedirectView(url); ``` ```ruby # GET/POST /auth/sso (params[:email] = "alex@acme.com") # Descope matches the email domain to the tenant's SSO Domain and returns # that tenant's IdP authorization URL. url = descope_client.saml_sign_in( tenant: params[:email], return_url: 'https://app.example.com/auth/sso/callback' ) # Redirect the user's browser to the returned URL. redirect_to url, allow_other_host: true ``` ```php // POST /auth/sso (email = "alex@acme.com") // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. $response = $descopeSDK->auth->sso->signIn( "alex@acme.com", "https://app.example.com/auth/sso/callback" ); // Redirect the user's browser to the returned URL. header('Location: ' . $response['url']); exit; ``` ```csharp // GET/POST /auth/sso (email = "alex@acme.com") var email = "alex@acme.com"; string redirectUrl = "https://app.example.com/auth/sso/callback"; // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. var authorizationUrl = await descopeClient.Auth.Sso.Start(tenant: email, redirectUrl: redirectUrl); // Redirect the user's browser to the returned URL. return Redirect(authorizationUrl); ``` Use the **same** `redirectUrl` you will implement as the callback below. ### Callback Endpoint: Exchange the Code After the IdP, Descope redirects to your callback with `?code=…`. Exchange it on the server, then start your session. ```javascript // GET /auth/sso/callback?code=... export async function ssoCallback(req, res) { const { code } = req.query; const resp = await descopeClient.sso.exchange(code); if (!resp.ok) { return res.status(401).json(resp.error); } // resp.data has sessionJwt, refreshJwt, and user. // Validate the token and start your own session (see below). const { sessionJwt, refreshJwt, user } = resp.data; res.redirect('/'); } ``` ```ts title="app/api/auth/sso/callback/route.ts" import { NextRequest, NextResponse } from 'next/server'; import { sdk } from '@/lib/descope'; // GET /api/auth/sso/callback?code=... export async function GET(req: NextRequest) { const code = req.nextUrl.searchParams.get('code'); if (!code) { return NextResponse.json({ error: 'missing code' }, { status: 400 }); } const resp = await sdk.sso.exchange(code); if (!resp.ok) { return NextResponse.json(resp.error, { status: 401 }); } // resp.data has sessionJwt, refreshJwt, and user. // Validate the token and start your own session (see below). return NextResponse.redirect('https://app.example.com/'); } ``` ```python # GET /auth/sso/callback?code=... def sso_callback(code: str): resp = descope_client.sso.exchange_token(code=code) # resp has sessionJwt, refreshJwt, and user. # Validate the token and start your own session (see below). return resp ``` ```go // GET /auth/sso/callback?code=... ctx := context.Background() code := r.URL.Query().Get("code") authInfo, err := descopeClient.Auth.SSO.ExchangeToken(ctx, code, w) if err != nil { // handle error } // authInfo has the session and refresh JWTs and the user. // Validate the token and start your own session (see below). ``` ```java // GET /auth/sso/callback?code=... AuthenticationInfo info = ss.exchangeToken(code); // info has the session and refresh JWTs and the user. // Validate the token and start your own session (see below). ``` ```ruby # GET /auth/sso/callback?code=... jwt_response = descope_client.saml_exchange_token(code) # jwt_response has the session and refresh JWTs and the user. # Validate the token and start your own session (see below). ``` ```php // GET /auth/sso/callback?code=... $response = $descopeSDK->auth->sso->exchangeToken($code); // $response has the session and refresh JWTs and the user. // Validate the token and start your own session (see below). ``` ```csharp // GET /auth/sso/callback?code=... var authRes = await descopeClient.Auth.Sso.Exchange(code: code); // authRes has the session and refresh JWTs and the user. // Validate the token and start your own session (see below). ``` ### Validate the Token and Start Your Session `sso.exchange` returns Descope session and refresh JWTs along with the user's details. Validate the session JWT before you trust it. Validation checks the signature and expiry against your project's public keys, and it returns the token's claims. See [backend session validation](/sessions/validation/backend) for the exact function in your SDK, and [What's in the token](/sessions#whats-in-the-token) for the claims a Descope session JWT carries. Once the token is valid, you have everything you need to sign the user in on your side. The claims carry the identity basics: the Descope user ID in `sub`, the user's `email`, and their tenant memberships and roles. Use those to find the user in your own database, or create the record on first login. From there, start a session however you already do for other login methods, whether that is setting a session cookie, signing your own JWT, or creating a server-side session. SSO doesn't change this part. If you need details that aren't in the token (a custom attribute, the full profile, or group memberships you didn't add as claims), fetch them from Descope with the [Management SDK](/management/user-management/sdks), using the `sub` claim as the user ID. Keep the JWT small and pull extra data only when you need it. For Next.js, `createSdk` from [`@descope/nextjs-sdk/server`](/getting-started/nextjs) exposes the same `sdk.sso` and `sdk.management`, so the route handlers in the tabs above use the exact same calls. ## Step 2: Connect an IdP with the SSO Setup Suite With routes in place, configure the tenant's SSO connection. Use the SSO Setup Suite, not the raw Console form and not the Management API for this path. The suite walks through any SAML or OIDC IdP (Okta, Entra, Google Workspace, or generic), attribute/group mapping, SSO Domains, a connection test, and SCIM. 1. Open your tenant in the [Console](https://app.descope.com/tenants). 2. Generate a [SSO Setup Suite link](/auth-methods/sso/sso-setup-suite#accessing-the-sso-setup-suite) and open it yourself (or send it to the customer's IT admin). 3. Complete IdP setup in the suite. 4. Set the **SSO Domain** (e.g. `acme.com`) so `sso.start('alex@acme.com')` resolves to this tenant. 5. Run the suite's **connection test**. That is the same flow you will use for every real customer later. To verify your code before you have a real IdP, point a test tenant at [MockSAML](https://mocksaml.com) (`https://mocksaml.com/api/saml/metadata`), add SSO Domain `example.com`, then call start with `user@example.com`. See [Mock SAML testing](/management/tenant-management/sso/mock-saml-testing) for the walkthrough. Manual Console fields and Management SDK/API automation are documented elsewhere when you need them: [SAML](/auth-methods/sso/saml) · [OIDC](/auth-methods/sso/oidc) · [Configure SSO with SDKs](/management/tenant-management/sso/sdks). ## Step 3: Test End-to-End Run one full login and confirm each hop works: 1. Trigger your start route with an email whose domain matches the tenant's SSO Domain (or the `user@example.com` MockSAML user). Your backend should respond with a redirect to the IdP. 2. Authenticate at the IdP (or MockSAML). 3. Confirm the browser lands back on your callback route with a `code` query parameter. 4. Confirm `sso.exchange` succeeds, your token validates, and your own app session is created. You should now be logged in. 5. Open [Audits](https://app.descope.com/audits) and confirm a `LoginSucceeded` event for the user. If the exchange fails, look up the error in the [SSO error codes](/other-troubleshooting/sso-troubleshooting#sso-error-codes). So far you have tested SP-initiated login, where the user starts from your app. Users can also start from their identity provider instead. Most IdPs, like Okta, show each connected app as a tile on the user's dashboard, and clicking that tile logs them straight into your app. This is called IdP-initiated login, and it is easy to forget to test because it never touches your sign-in page. It only works once you have set a [Post Authentication Redirect URL](/auth-methods/sso/settings#post-authentication-redirect-url), which tells Descope where to send the user afterward. Configure that URL, then try [IdP-initiated login](/sso/idp-initiated). Users who first authenticate via SSO become **SSO-only**. Plan [identity merging](/sso/merging-sso-identities-risk) and SSO Domain association before production if those users might already exist with password/OTP. ## Next Steps - [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) for the full suite, SCIM, and embedding - [Backend SDK reference](/auth-methods/sso/with-sdks/backend) for `loginHint`, `ssoId`, `prompt`, `forceAuthn`, and more - [JIT provisioning](/sso/jit-provisioning), [mapping](/sso/sso-mapping), and [multi-SSO](/sso/multi-sso) - [Launch checklist](/auth-methods/sso/launch-checklist) and [troubleshooting](/other-troubleshooting/sso-troubleshooting) # SSO (Single Sign-on) Authentication (/auth-methods/sso) Customize your SSO (Single Sign-On) authentication flow with Descope. # SSO (Single Sign-On) If you're using a FedRAMP deployment of Descope, you must ensure your SSO provider is certified for [FedRAMP High](/fedramp/federated-identity-providers). Descope supports SAML 2.0 and OAuth 2.0 / OIDC for Single Sign-On (SSO), so you can connect to any identity provider (IdP) that speaks either protocol. To use SSO you need to: 1. Configure your project's [SSO Settings](/auth-methods/sso/settings) under [Authentication Methods → SSO](https://app.descope.com/settings/authentication/sso) in the Descope Console 2. Configure an [SSO connection](#configuring-sso-for-a-tenant) for at least one [tenant](/management/tenant-management) SSO is configured **per tenant**, not at the project level. You need at least one tenant with SSO set up before users can sign in with SSO. ## Built for B2B SSO Descope is a fully multi-tenant based authentication platform. You can onboard each customer as a [tenant](/management/tenant-management) with one or multiple SSO connections, then hand their admins self-service configuration through the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) and [Admin Portal](/widgets/admin-portal). Every tenant can also have its own SCIM provisioning, roles, and Flow styling. ## Choose Your Integration Approach Two common ways to use Descope for SSO: - **Descope runs authentication:** Use [Descope Flows](/getting-started) for login and sessions. SSO will be one of the [actions](/flows/actions) in the flow. - **You keep your own sessions:** Use Descope only for SSO via the [backend SDK](/auth-methods/sso/with-sdks/backend). At login, your backend validates a Descope token, then signs the user in the way it already does. You still get [Admin Portal](/widgets/admin-portal), [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), RBAC, and tenant-level settings. (Client / mobile SDK guides exist for SPAs and native apps that must run the handshake on-device.) If you're adding SSO beside an existing auth stack, start with [Getting Started with SSO](/auth-methods/sso/getting-started). If Descope is your full auth layer, use the main [Getting Started guide](/getting-started) instead. ## IdP and SP When we talk about SSO, we usually mean letting your customers sign into *your* app with *their* IdP. If you want Descope to **be** an IdP for other applications, see [Federated Apps](/identity-federation/applications) instead. You'll see these two terms everywhere in SSO docs: - **Identity Provider (IdP)**: the system that authenticates the user. For your customers that's usually Okta, Microsoft Entra ID, Google Workspace, or similar. - **Service Provider (SP)**: the application the user is trying to open. That's your product. Your app is the SP. Descope acts as the SP toward the IdP, so you don't have to implement SAML or OIDC federation yourself. ## Tenant Identification Methods Because SSO is configured per tenant, Descope has to know which tenant a user belongs to before it can send them to the right IdP. That step is often called **home realm discovery**. You can identify the tenant in a few ways: A single tenant can also use **multiple SSO providers**, so different groups in the same org can log in through different IdPs. For more information on how to configure different SSO providers within one tenant, see our guide on [multiple SSO providers](/sso/multi-sso). ### Option 1: SSO Domains This is different than the tenant-level [email domain](/management/tenant-management/tenant#email-domain) which is only really used for tenant's that **don't use SSO**. Configure SSO domains for a tenant in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), or manually under **Authentication Methods → SSO** when you select a tenant on the [Tenants](https://app.descope.com/tenants) page. Add the email domains that should use this tenant's IdP. Users with those domains are routed to that SAML/OIDC connection. With the **backend SDK**, pass the user's email into `sso.start` (for example `sso.start('alex@acme.com', redirectUrl)`). Descope looks up the matching SSO Domain and returns the authorization URL for that tenant's IdP, so you don't have to resolve the tenant ID yourself. See [Getting Started](/auth-methods/sso/getting-started#start-endpoint-pass-the-users-email) and [Backend SDK start options](/auth-methods/sso/with-sdks/backend#start-options). | Tenant Name | SSO Domain | User's Accessibility | | - | - | - | | SSO Tenant 1 | company-a.com | Users signing in from `@company-a.com` authenticate with `SSO Tenant 1`'s IdP | | SSO Tenant 2 | company-x.com, company-y.com | Users from `@company-x.com` or `@company-y.com` authenticate with `SSO Tenant 2`'s IdP | ### Option 2: Tenant Slug or ID For more on flow input parameters, see [Auth Helpers](/client-sdk/auth-helpers). If one tenant spans multiple domains, or domain association doesn't fit your IdP, pass a tenant slug or ID into the flow instead. That routes the user to the right tenant regardless of email domain. Use a slug or ID when: - Several domains sit under one tenant - Your IdP doesn't support domain-based routing - You want to decide the tenant in your app, not from the email address ## Configuring SSO for a Tenant Each tenant gets its own SAML or OIDC connection. Prefer the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite): generate a link and either send it to the customer's IT admin or open it yourself when you're configuring SSO on their behalf. The suite works with effectively any SAML or OIDC IdP (templates for common providers, plus generic config), and covers attribute/group mapping, a connection test, and SCIM. If the Setup Suite isn't on your plan, configure the tenant manually with the [SSO with SAML](/auth-methods/sso/saml) or [SSO with OIDC](/auth-methods/sso/oidc) guide. Migrating from another auth provider or a homegrown SSO stack? You can often keep the customer's IdP settings as-is. See the [SSO migration guide](/migrate/sso). ## Where to Go Next | Goal | Doc | | ---- | --- | | First working SSO login | [Getting Started with SSO](/auth-methods/sso/getting-started) | | Customer self-service setup (any IdP) | [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) | | Project SSO + Setup Suite + Admin Portal as code | [Terraform → SSO Settings and Admin Portal](/managing-environments/terraform#sso-settings-and-admin-portal) | | Multiple IdPs on one tenant | [Multiple SSO providers](/sso/multi-sso) | | Create users on first login | [JIT provisioning](/sso/jit-provisioning) | | Map groups and attributes (RBAC + FGA) | [SSO mapping](/sso/sso-mapping) | | SP- vs IdP-initiated | [SSO login flows](/sso/idp-initiated) | | Directory sync | [SCIM](/management/tenant-management/scim) | | Cert / metadata rotation | [Certificate and metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation) | | Go-live / errors | [Launch checklist](/auth-methods/sso/launch-checklist), [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting) | # Go-Live Checklist (/auth-methods/sso/launch-checklist) A checklist to run through before enabling SSO for real customers in production with Descope. # SSO Go-Live Checklist Run through this before turning on SSO for real customers in production. Each item links to the relevant guide. ## Configuration - [ ] SSO is enabled at the [project level](/auth-methods/sso/settings) (**Authentication Methods → SSO**). - [ ] Each customer [tenant](/management/tenant-management) has a SAML or OIDC connection configured and tested. - [ ] Users are routed to the right tenant, either by [SSO Domain or tenant slug/ID](/auth-methods/sso#tenant-identification-methods). - [ ] [Attribute and group/role mapping](/sso/sso-mapping) are configured and verified. For OIDC, confirm the IdP actually sends a groups claim (see [OIDC group claims](/auth-methods/sso/oidc#group-claims-oidc)). - [ ] [Post-authentication redirect URLs](/auth-methods/sso/settings#post-authentication-redirect-url) are set (project and/or tenant level) and use HTTPS in production. This is **required** for [IdP-initiated](/sso/idp-initiated#what-you-must-configure) login. ## Application - [ ] Your app initiates SSO and handles the callback, using [Flows](/auth-methods/sso/with-flows) or the [backend](/auth-methods/sso/with-sdks/backend) / [client](/auth-methods/sso/with-sdks/client) SDK. - [ ] Your backend validates the returned Descope token, and the tenant it belongs to, before creating a session. - [ ] You've handled SSO failures gracefully (see the [SSO error codes](/other-troubleshooting/sso-troubleshooting#sso-error-codes)). - [ ] You've tested both [SP-initiated and IdP-initiated](/sso/idp-initiated) login. ## Test End-to-End - [ ] Tested with a real IdP, or with a [Mock SAML tenant](/management/tenant-management/sso/mock-saml-testing) if you don't have one yet. - [ ] Confirmed users land authenticated with the roles you expect. - [ ] Checked the [Audit page](https://app.descope.com/audits) for the `LoginSucceeded` event and reviewed any failures. ## Customer Self-Service (Recommended) - [ ] Decided how customers configure their own IdP: hand off an [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) link, [embed it](/sso/sso-setup-suite-embed), or use the [Admin Portal](/widgets/admin-portal). - [ ] Confirmed the SSO Setup Suite plan availability for your project. ## Production - [ ] Using your production Project ID (and production access keys for management calls). - [ ] Descope can reach the IdP endpoints. If the IdP restricts inbound traffic, allowlist [Descope's static IPs](/how-to-deploy-to-production/public-static-ips). - [ ] Reviewed the [identity-merging behavior](/sso/merging-sso-identities-risk) and configured SSO Domain association to avoid duplicate accounts. - [ ] Prefer IdP **metadata URLs** where possible, and know how you'll handle [certificate / metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation). # Project-Level Settings (/auth-methods/sso/settings) Customize your single sign-on (SSO) authentication settings with Descope. # Project-Level SSO Settings These settings apply to all tenants using SSO in your project. You can configure them in the Descope Console under [Authentication Methods --> SSO](https://app.descope.com/settings/authentication/sso) settings, or as code with [Terraform](/managing-environments/terraform#sso-settings-and-admin-portal) (`authentication.sso` / `sso_suite_settings`). ## General Settings ![SSO General Settings](/assets/sso_general_settings.webp) ### Convert Existing Users to SSO-Only When enabled (the default), this setting controls how users become SSO-only. Once a user authenticates through SSO, they can only sign in with SSO afterward. To ensure that SSO/SCIM merging occurs, the existing user must belong to a *single* tenant. Merging is controlled by this project-level setting and applies to SCIM provisioning as well, whether or not the tenant has SSO enabled. #### What You Should Know 1. **When Tenant Exists Before User**: If a tenant is already set up with a domain association and a user signs in with an email matching that domain, the user will be added to the tenant. If the tenant later enables SSO, and the user signs in via SSO, the user will be converted to SSO-only authentication 2. **When User Exists Before Tenant**: If a user already has an account before a tenant is created and that tenant is later created with the same domain/SSO configuration for that domain, the user won't be automatically associated with the tenant. If the user tries to sign in via SSO with the same email, a new SSO-only account will be created, resulting in duplicate accounts. #### Auto-Association Configuration To handle the "User Created First" scenario, you can enable auto-association in your specific tenant settings: - **Email Domain Auto-Association**: Automatically associate existing users with matching email domains to newly created tenants - **SSO Domain Auto-Association**: When enabled, existing users will be: - Associated with the tenant if their email matches the SSO domain - Converted to SSO users when they first authenticate via SSO - Merged with their existing account instead of creating a duplicate ![email domain](/assets/email-domain-setting.webp) ![sso domain](/assets/sso-domain-setting.webp) Auto-association helps prevent duplicate accounts by merging existing users with their SSO identities when the email addresses match. This is particularly useful when users have existing accounts with personal emails that later become their SSO email addresses. ### Allow Duplicate SSO Domains Across Tenants When multiple tenants share the same SSO domain, such as different departments within the same organization, you must give users a way to select their specific tenant before initiating SSO authentication. To support this, make sure the **Allow duplicate SSO domains across tenants** setting is enabled. Then, follow the steps in our [flow guide](/auth-methods/sso/with-flows#allowing-duplicate-sso-domains-across-tenants) to allow the user to select the tenant they want to authenticate into. ### Block Login on Email Domain Mismatch When enabled, Descope validates that the email address returned by the Identity Provider (IdP) during SSO authentication belongs to one of the domains configured for that SSO connection. If the email domain does not match, the login attempt is blocked and the flow fails. This setting applies to both **SAML** and **OIDC** SSO connection types. This same rule applies to email addresses written through [SCIM provisioning](/management/tenant-management/scim). When SSO is enabled for the tenant, or when the tenant has **SSO Domains** configured (referring to the tenant-level SSO Domains list), email updates made through SCIM are validated against those SSO domains. For tenants that have neither SSO enabled nor any SSO domains configured, this check is skipped for SCIM. When a SCIM write fails this check, Descope rejects the request with HTTP `400` and the SCIM error detail `Email domain does not match configured SSO domain.` The user is not created or updated, the rejection is recorded as a `SCIMEvent` [audit event](/audit-trails-and-integrations/audit-events/scim-audit-events) with the `error` field set (when SCIM audit logging is enabled for your organization), and the IdP reports the operation as failed - most IdPs retry it on their next sync cycle, so the user keeps failing to provision until either the email domain is corrected in the IdP or the domain is added to the tenant's **SSO Domains** list. This applies to SCIM user create, replace (`PUT`), and update (`PATCH`) requests that carry an email address. By default, Descope trusts the email field returned by the IdP as-is. In most setups this is fine, but in multi-tenant or federated IdP environments, a single IdP can issue tokens for users across many domains. Without domain validation, a user whose IdP email happens to belong to an unexpected domain could complete SSO login when they shouldn't. Enabling this setting provides a built-in enforcement layer so you do not need to add a custom scriptlet to your flow to inspect and block mismatched emails. This is a project-level setting. It applies to all tenants and both SAML and OIDC SSO connections within your project. Make sure each tenant's **SSO Domains** list is correctly configured before enabling this — if SSO domains are not set for a tenant, logins for that tenant may be blocked unexpectedly. ### Post Authentication Redirect URL - **Default Redirect URL**: The URL where users are redirected after successful SSO authentication. This can be: - A static URL (e.g., `https://myapp.com/dashboard`) - A dynamic URL using tenant information: - `{{tenant.domain}}` - The tenant's domain - `{{tenant.name}}` - The tenant's name - `{{tenant.id}}` - The tenant's unique ID - `{{tenant.selfProvisionDomain}}` - The email domain that allows tenant self provisioning ![SSO dynamic redirect URL configuration](/assets/sso-dynamic-redirect-url.webp) Example of a dynamic redirect URL in action: ![SSO dynamic redirect URL example](/assets/sso-dynamic-redirect-example.webp) This can be overridden by: - Tenant-specific redirect URLs - The [Redirect URL](/auth-methods/sso/with-flows#redirect-url-optional) field on the SSO flow action - URLs specified in the SDK or API calls ### User Attributes - **Mandatory Attributes**: Define which Descope attributes must be populated when receiving SSO information. This ensures that your SSO configuration provides all necessary user data. Alongside standard user attributes (Email, Given Name, and so on), you can add **Groups** to this list. When **Groups** is mandatory, tenant admins must map a **Groups Attribute Name** before they can save the SSO configuration: the field becomes required in the tenant's SAML settings in the Console, and in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite#attribute-mapping-user-and-group) for both SAML and OIDC. You catch a missing or misspelled group attribute name during setup, instead of after customer users sign in without the roles or FGA relations they need. #### Requiring an SSO Domain **SSO Domains** is also selectable in **Mandatory Attributes**. Selecting it requires whoever configures a tenant's SSO connection (manually, through the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), or via the [Management SDK](/management/tenant-management/sso/sdks)/[API](/api/management/tenants/sso)) to add at least one **SSO Domain** to that tenant before the connection can be saved. The other entries in this list map a claim from the IdP; this one doesn't. If a tenant has [SSO Enabled](/management/tenant-management/tenant#sso-enabled) (`tenant.enforceSSO`) but no SSO domain configured, Descope has no domain to match users against and can't route them to their SSO connection, so users in that tenant can never sign in. Adding **SSO Domains** to Mandatory User Attributes prevents this misconfiguration by requiring a domain up front. See [Enforcing SSO](/flows/conditions/sso-enforced) for how `tenant.enforceSSO` is used in a flow. This can be set at the project level (applying to all tenants) or overridden per tenant. In Terraform, the project-level version is the [`require_sso_domains`](/managing-environments/terraform#project-sso-settings) attribute. ### Fine-Grained Authorization (FGA) - **Mappable FGA Types**: Select which FGA schema types are available for tenant admins (and the SSO Setup Suite) when mapping SSO groups to FGA relations. This does not create the maps themselves. It only limits which types appear in the pickers. How to define IdP group → FGA relation maps (`fgaMappings` / ReBAC groups mappings): [SSO user and group mapping → Groups → FGA](/sso/sso-mapping#groups-to-fga-relations). ### Role Mapping: Add vs. Override By default (**Add roles**), when an SSO login or [SCIM sync](/management/tenant-management/scim) resolves roles from IdP groups, Descope adds those roles to whatever roles the user already has on that tenant — including roles you assigned manually in the console. Switch this to **Override roles** and Descope instead treats the **group → role mapping** as the source of truth: on every SSO login or SCIM sync, the user's tenant roles are replaced with exactly what the mapping resolves to. Roles the mapping doesn't produce, including ones assigned by hand or left over from a previous mapping, are removed from that tenant at the next login or sync. If no group matches, [Default Roles](/management/tenant-management/scim#default-roles) apply instead. This is a project-level setting and applies to both SAML and OIDC. In Terraform it's the [`allow_override_roles`](/managing-environments/terraform#project-sso-settings) attribute under `authentication.sso`. These general settings apply to SSO across your whole project. Individual tenants can also have their own configuration on top of them. ## SSO Setup Suite Settings The SSO Setup Suite allows your customers to self-configure their SSO integration. These settings control how the suite works across all tenants. These settings can be overridden in the [tenant settings](/management/tenant-management/tenant#sso-setup-suite-configuration). ![SSO Setup Suite Settings](/assets/sso_setup_suite_settings.webp) ### Access Control Configure who can access and use the SSO Setup Suite: - **RBAC Permissions**: Defines the permissions assigned to the SSO Suite user, which influence the available roles shown in the group mapping section. - **FGA Permission**: Required FGA permissions to access the suite ### FGA Resource Tenant ID This setting applies at the project level. Configure it once, and it covers how Descope looks up FGA resources for every tenant in the [group → FGA mapping](/sso/sso-mapping#groups-to-fga-relations) picker, in both the SSO Setup Suite and the Console. ![SSO FGA Resource Tenant ID](/assets/fga_resource_tenant_id.webp) Descope replaces the `{{tenantID}}` placeholder with each tenant's raw ID during that lookup. If your FGA relations store tenant-scoped resource IDs with a fixed prefix and/or suffix (for example `org:` instead of ``), set **Prefix** and/or **Suffix** here to match. Prefix and Suffix only affect that resource lookup. The Default Relations fallback (see [Groups to FGA Relations](/sso/sso-mapping#groups-to-fga-relations)) substitutes `{{tenantID}}` on its own, ignoring Prefix and Suffix. ### Styling You can apply a [custom style](/management/styles) for the SSO setup suite, that can customize the appearance of the SSO Setup Suite to match your brand. ### SSO Suite Features You can specify the enabled functionalities of the SSO Setup Suite. Choose whether or not to include SSO Configuration, SAML, OIDC, SCIM Configuration, [Cross App Access](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration), Role Mapping, FGA Mapping, SSO Domains, and/or JIT. Role Mapping and FGA Mapping are separate chips in the Setup Suite Settings, so you can choose to show tenant admins only the mapping section that applies to their setup. Projects configured as Group Mapping will continue working: if the combined Group Mapping option is still in use, removing it hides both the Role Mapping and FGA Mapping sections. Switch to the separate chips to control the two sections independently. ![SSO Setup Suite Mapping Chips](/assets/sso_setup_suite_settings_mapping_chips.webp) #### SCIM-Only Suite Removing **SSO Configuration** turns the suite into a SCIM-only experience: tenant admins can generate and manage their SCIM token, but the SSO connection itself is hidden, along with everything that only serves it (SAML, OIDC, SSO Domains, JIT, and SSO testing). Use this when you configure the SSO connection yourself and only want the tenant admin to set up provisioning. Requests to configure or test SSO are rejected by the API as well, not just hidden in the UI. SSO Configuration and SCIM Configuration cannot both be removed - the suite has to keep at least one of the two, otherwise there is nothing for the tenant admin to set up. In Terraform, this is the [`hide_sso`](/managing-environments/terraform#sso-setup-suite-settings) attribute of `sso_suite_settings`. #### Force Domain Verification Require the SSO Setup Suite User to verify their domain before configuring SSO for their tenant. To complete the verification, the SSO Setup Suite User will need to add a DNS TXT record. Configuration details are available within the SSO Domains tab of the SSO Setup Suite. ![SSO Setup Suite Domain Verification](/assets/s4-domain-verification.webp) ### Show Help Contact Add a support email address that will be displayed within the SSO Setup Suite UI. When configured, IT admins will see this contact information throughout the setup process, giving them a clear path to reach out if they encounter any issues. ![SSO Setup Suite Help Contact](/assets/s4-help-contact.webp) ### Invitation Configuration Control how users are invited to set up SSO: - **Connector**: Which email connector to use for sending the invitation email. - **Email Template**: Customize the invitation email content and design - **Expiration**: Set how long the SSO Setup Suite invitation remains valid # SSO Setup Suite (/auth-methods/sso/sso-setup-suite) Streamline SSO configuration with Descope's SSO Setup Suite. Enable self-service IdP setup, SCIM provisioning, and seamless integration for your B2B customers. # SSO Setup Suite Configuring SSO with a customer's IdP is fiddly when you do it by trading screenshots and metadata URLs. The SSO Setup Suite is the guided UI for that work, and it handles effectively any SAML or OIDC identity provider, plus SCIM. It includes templates for common IdPs (Okta, Entra ID, Google Workspace, and others) and a generic path when there isn't a named template. One flow covers: - IdP configuration (SAML or OIDC) - User attribute and group mapping - A connection test (including viewing the assertion) - SCIM provisioning - [Cross App Access](#cross-app-access-xaa-configuration), so the customer can let their agents reach your product securely from their own IdP Use it for: - **Customer self-service.** Generate a link and send it to their IT admin. - **Configuring on their behalf.** Generate the same link in the Console and open it yourself, which is usually better than editing the tenant SSO form by hand. Project-level suite options (which tabs to show, style, support contact, invite emails) can be managed as code. See [Terraform → SSO Setup Suite Settings](/managing-environments/terraform#sso-setup-suite-settings). The SSO Setup Suite is a paid-plan feature. Check the [pricing page](https://www.descope.com/pricing) or your Descope plan to confirm availability. While developing on a plan without it, you can configure SSO manually in the [Console](https://app.descope.com/tenants) or via the [management API](/management/tenant-management). See [Getting Started with SSO](/auth-methods/sso/getting-started). ## SSO Setup Suite Tutorial Prefer a fully hosted, brandable experience for your tenant admins? The [Admin Portal](/widgets/admin-portal) surfaces the Setup Suite alongside other admin widgets. You can also [embed the Setup Suite](/sso/sso-setup-suite-embed) directly in your app and [scope access with RBAC](/sso/sso-setup-suite-rbac). The video below will walk you through the experience of configuring an IdP using the Descope SSO Setup Suite. ## Accessing the SSO Setup Suite The SSO Setup Suite can be accessed in three ways: 1. [Generated Link with Token](/auth-methods/sso/sso-setup-suite#method-1-generated-link-with-token): Generate a link that includes a temporary token with Tenant Admin permissions 2. [Direct Access with Authentication](/auth-methods/sso/sso-setup-suite#method-2-direct-access-with-authentication): Have the user authenticate first, then access the suite directly 3. [Embedded in Your Application](/sso/sso-setup-suite-embed): Embed the suite directly in your application using an iframe, with support for theme customization and other query parameters ### Method 1: Generated Link with Token You can generate the SSO Setup Suite link for your user in a few ways. The generated link includes a temporary token as a query parameter that provides the necessary Tenant Admin permissions. To direct a tenant admin to a specific IdP instead of our gallery of available IdPs, you can add a `target` query parameter to the generated link. For example, to send an admin straight to the Descope SAML setup, add `target=sso:descope:saml`: `__BaseURL__/sso/setup/__ProjectID__?t=&target=sso:descope:saml` The following is a non-exhaustive list of available `target` values: | IdP | SAML | OIDC | SCIM | | --- | --- | --- | --- | | Google Workspace | `sso:google:saml` | | | | Okta | `sso:okta:saml` | `sso:okta:oidc` | `scim:okta:saml` | | Microsoft Entra ID | `sso:entraid:saml` | `sso:entraid:oidc` | `scim:entraid:generic` | | Microsoft AD FS | `sso:adfs:saml` | | | | PingFederate | `sso:pingfederate:saml` | | | | PingOne | `sso:pingone:saml` | | | | OneLogin | `sso:onelogin:saml` | | `scim:onelogin:saml` | | Keycloak | `sso:keycloak:saml` | `sso:keycloak:oidc` | | | JumpCloud | `sso:jumpcloud:saml` | | `scim:jumpcloud:saml` | | Auth0 | `sso:auth0:saml` | `sso:auth0:oidc` | | | ClassLink | `sso:classlink:saml` | | | | CyberArk | `sso:cyberark:saml` | | `scim:cyberark:saml` | | Descope | `sso:descope:saml` | | | | Duo | `sso:duo:saml` | | | | LastPass | `sso:lastpass:saml` | | | | miniOrange | `sso:miniorange:saml` | | | | Salesforce | `sso:salesforce:saml` | | | | Shibboleth | `sso:shibboleth:saml` | | | | Generic IdP | `sso:generic-idp:saml` | `sso:generic-idp:oidc` | `scim:generic-idp:generic` | #### Manual Generation of SSO Configuration Link On your tenant's configuration page in the [Descope console](https://app.descope.com/tenants), click the `Generate Link` button to generate a link you can share with your customer's administrator, or open yourself when you're configuring SSO for them. ![Generating the Descope SSO Setup suite link within the tenant console.](/assets/sso-setup-suite-console.webp) Once the link has been generated, you can copy it from the Descope UI or enter the recipient's email and send it. You don't need a separate per-IdP setup guide. Generate the Setup Suite link, open it in your browser, pick the IdP template (or generic SAML/OIDC), and complete the same flow your customer would, including SCIM if they need directory sync. Once you have configured your custom domain, the URL generated will have your custom domain (ex: `auth.example.com`) rather than `api.descope.com`. ![Sending the Descope SSO Setup suite link to user via email within the tenant console.](/assets/sso-setup-suite-console-2.webp) #### Automated Generation of SSO Configuration Link You'll most commonly want to generate and share an SSO Suite Link programmatically with your customer's administrator via Descope Flows, API, or SDK. The sections below outline ways to automate sending the link to your users. ##### **Generate the SSO Setup Suite Link via Descope SDK** You can use the Descope management SDK to generate and send the SSO configuration link as well. ```javascript // Args: // tenantId: the tenant ID to generate the link for const tenantId = "T2..." // expireDuration: The expiration parameter takes a number to add in milliseconds to the current time. const expireDuration = 10000000 // ssoId (optional): If provided, it indicates which SSO Profile within the tenant to configure for the SSO Setup Suite Link. // email (optional): If provided, the link will also be emailed to the recipient. // templateId (optional): If provided, this template will be used to email the user. const link = await descopeClient.management.tenant.generateSSOConfigurationLink(tenantId, expireDuration); ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID: the tenant ID to generate the link for tenantId := "T2..." // expireDuration: The expiration parameter takes a number to add in milliseconds to the current time. expireDuration := 10000000 // ssoID (optional): If provided, it indicates which SSO Profile within the tenant to configure for the SSO Setup Suite Link. // email (optional): If provided, the link will also be emailed to the recipient. // templateID (optional): If provided, this template will be used to email the user. res, _err := descopeClient.Management.Tenant().GenerateSSOConfigurationLink(ctx, tenantID, 3000) ``` ##### **Generate the SSO Setup Suite Link via Descope API** You can use the API to [generate and send the SSO Configuration Link](/api/management/tenants/admin-links/generate-tenant-admin-link-sso). ##### **Generate SSO Setup Suite Link via Descope Flows** You can use the `Generate SSO setup suite admin link` action in a flow when you're creating a tenant and building your onboarding flow. Once you have generated the link via the action, you can use the dynamic key of `{{adminLinks.ssoConfiguration}}` within a link within a flow screen or send it to a user via an email or SMS connector. Below is an example flow that checks if the user is a new user to the tenant, gives the user the tenant admin permission, and generates the link. ![Generating the Descope SSO Setup suite link within a flow example.](/assets/example-sso-setup-link-flow.webp) ![Sending the Descope SSO Setup suite link within a messaging connector action.](/assets/send-sso-setup-link-via-connector.webp) #### Revoking the SSO Setup Suite Link If you manually revoke/expire the SSO Setup Suite Link, you can do so from within the tenant's configuration page in the [Descope console](https://app.descope.com/tenants) by clicking the `Revoke Link` button. You can also revoke the SSO Setup Suite Link via the [Descope API](/api/management/tenants/admin-links/revoke-tenant-admin-link-sso). It is also possible to use the Descope management SDK to revoke the SSO configuration link. ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID: the tenant ID to revoke the link for tenantId := "T2..." // ssoID (optional): If provided, it indicates which SSO Profile within the tenant to revoke for the SSO Setup Suite Link. res, _err := descopeClient.Management.Tenant().RevokeSSOConfigurationLink(ctx, tenantID, ssoID) ``` ### Method 2: Direct Access with Authentication Alternatively, instead of generating a link with a temporary token, you can have users authenticate first and then access the SSO Setup Suite directly. #### Authenticate User The user needs to be authenticated and have a valid cookie on your Descope domain (either something like `api.descope.com`, or your [custom domain](/how-to-deploy-to-production/custom-domain) if you have one configured). This can be achieved by having the user authenticate through a sign-in flow on your Descope domain. #### Access the SSO Setup Suite Once authenticated, the user can visit the SSO Setup Suite directly using this URL pattern: ``` https:///sso/setup/__ProjectID__?tenantId= ``` Before allowing access to the SSO Setup Suite, there is a check to ensure that the user has the necessary Tenant Admin permissions through the refresh token, eliminating the need for a temporary token in the URL. For embedding the SSO Setup Suite in your application and additional customization options (including theme control), see the [Embedding SSO Setup Suite](/sso/sso-setup-suite-embed) guide. ## Use Your Own Custom Domain Follow these steps if you'd like to use your own custom domain, or if you're already using one and it's not appearing in the SSO Setup Suite (for example, in the "ACS URL"): 1. Ensure your custom domain (e.g., `auth.example.com`) is correctly configured in your Descope project settings. For more details, see [Custom Domains](/how-to-deploy-to-production/custom-domain). 2. If you've recently updated your custom domain, make sure to re-sync the tenant. You can do this by clicking the `Use Custom Domain` button in the **SAML** section: ![Update SSO Setup Suite with new custom domain.](/assets/update-sso-custom-domain.webp) ## Using the SSO Setup Suite To help your IT admins during setup, you can configure a support email address that will be displayed throughout the SSO Setup Suite. This gives them an easy way to reach out for help if they encounter any issues. Learn more about [Help Contact Configuration](/auth-methods/sso/settings#show-help-contact). ### SSO Configuration Once you have given the SSO Setup Suite link to your customer's administrator, they can start configuring their SSO IdP by clicking the SSO configuration button. ![Using the Descope SSO Setup Suite.](/assets/sso-configuration-suite-1.webp) #### Identity Provider (IdP) Selection Once you have started the configuration process, you'll see the screen for selecting the IdP. The user can choose from the list of available IdP configuration guides, or manually configure SAML 2.0 or OIDC from the options below. Selecting `Show More` expands the list of supported providers. If the user chooses one of the supported providers that supports either SAML or OIDC, there will be button options to select which one. ![Using the Descope SSO Setup Suite to select IdP provider.](/assets/sso-configuration-suite-2.webp) #### Service Provider Information Once the user has selected a provider, the guide populates and walks the user through configuring their SAML/OIDC provider. It includes the tenant-specific data that needs to be copied into the company's IdP settings. ![Using the Descope SSO Setup Suite to configure the IdP application.](/assets/sso-configuration-suite-3.webp) #### Attribute Mapping (User and Group) Once the user has started configuring the application, the user will walk through the next section to configure User and Group Attribute Mapping. This is where you can configure the data prepopulated to the user, such as email, name, groups, and any user's custom attribute. Descope prepopulates the Attribute Name field with default values based on the selected IdP, which eases the IT / Tenant Admin configuration process. You can only map existing attributes in the Descope project, so ensure you create the applicable [custom user attributes](/management/user-management#custom-user-attributes) or [roles](/authorization/role-based-access-control) to allow your customers to map to. If there are attributes or roles you prefer to keep hidden, assign them permissions that are not included in the Tenant Admin role or mark them as [hidden roles](/authorization/role-based-access-control#setting-default-and-hidden-roles). ![Using the Descope SSO Setup Suite to configure the user and group mapping.](/assets/sso-configuration-suite-4.webp) If your project marks **Groups** as a [mandatory attribute](/auth-methods/sso/settings#user-attributes), **Groups attribute name** becomes required here for both SAML and OIDC. The tenant admin sees a "Groups attribute name is required" validation error and can't move past this step until they map it. That way, they catch a missing or misspelled group attribute name during setup, before role or FGA mapping problems show up once users start signing in. #### Identity Provider Information The Identity Provider Information section is where the user provides the IdP information for the Descope tenant. This would be the metadata URL or various configurations, such as the SSO URL, Entity ID, and certificate. ![Using the Descope SSO Setup Suite to configure the IdP information on the Descope tenant.](/assets/sso-configuration-suite-5.webp) When you enter the certificate manually, the Setup Suite checks that it's a well-formed X.509 certificate: PEM-encoded with `-----BEGIN CERTIFICATE-----` / `-----END CERTIFICATE-----` headers, or bare base64 without them. An invalid value triggers an inline error before you save it, rather than a failure on the tenant's first login attempt. The check doesn't confirm the certificate is unexpired or that it matches the IdP's actual signing key — a validly formatted but stale certificate can still break login. See [Certificate and Metadata Rotation](/management/tenant-management/sso/cert-and-metadata-rotation) and [SSO Troubleshooting](/other-troubleshooting/sso-troubleshooting) if logins fail after setup. #### Assign Users and Groups You will be prompted to add user and group assignments within the IdP application when you visit the assign users and groups section of the SSO Setup Suite. ![Using the Descope SSO Setup Suite to assign users and groups within the IdP Application.](/assets/sso-configuration-suite-6.webp) #### SSO Domains Within the SSO domains step, the user can configure the tenant's SSO company domain. This domain, utilized during the [SSO flow action](/auth-methods/sso/with-flows#flow-actions), will automatically redirect users to the tenant based on the domain in their email address. When [force domain verification](/auth-methods/sso/settings#force-domain-verification) is enabled in the SSO Setup Suite Settings, the user will have to first verify their domain by adding a DNS TXT record. ![SSO Setup Suite Domain Verification](/assets/s4-domain-verification.webp) ![Using the Descope SSO Setup Suite to configure the SSO Domains.](/assets/sso-configuration-suite-7.webp) #### Testing After the IT administrator has completed the IdP configuration, they can validate the setup using the **testing page**. This process redirects them to the configured IdP for authentication. Upon successful sign-in, they'll be redirected back to view the resulting SAML or OIDC assertions, along with any relevant errors. The test results will also display the **generated user profile and assigned roles**, allowing the admin to verify that user and group/role attribute mappings are working as expected. If your project marks **Groups** as a [mandatory attribute](/auth-methods/sso/settings#user-attributes) and the tenant has JIT provisioning enabled, running this test also checks whether the IdP actually returned any group values, not just whether **Groups attribute name** is filled in. If the SAML assertion or OIDC token comes back with no groups, the test fails with `E062028` ("Mandatory groups attribute is missing in the assertion"), even when the attribute name field itself looks correctly mapped. This usually means the test user isn't assigned to any group at the IdP, or the attribute/claim name Descope is reading doesn't match what the IdP actually sends. See [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting#sso-error-codes) for more error codes. Below is an example of the response within the test page. ![Using the Descope SSO Setup Suite to test the IdP configuration and user creation.](/assets/sso-configuration-suite-8.webp) For user/group mapping issues, you can refer to the `Login Success / Login Failure` and `LoginStarted` [audit events](/audit-trails-and-integrations/audit-events#sso-related-fields), in the [Audits](https://app.descope.com/audits) section of the Descope Console that pertain to SSO. To know the moment a tenant admin finishes configuring SAML or OAuth SSO through the Setup Suite, create a [Management Flow](/flows/management-flows) with a start trigger on the `SAMLSettingsModified` or `OAuthSettingsModified` audit event. ### SCIM Configuration #### Identity Provider (IdP) Selection Once you have entered the SCIM configuration section, you'll be prompted to select your IdP. Selecting `Show More` expands the list of supported providers. You can also configure SCIM generically by selecting the General icon. Note that user attribute mapping is taken from the above SSO configuration, group mapping is shared with SSO logins, and any IdP custom attribute is supported. ![Using the Descope SSO Setup Suite to configure SCIM.](/assets/sso-configuration-suite-9.webp) #### Configure SAML SCIM Provisioning Once the user has selected a provider, the guide will populate and walk the user through configuring SCIM within the provider application. ![Using the Descope SSO Setup Suite to configure SCIM application within the IDP.](/assets/sso-configuration-suite-10.webp) ##### URL and Access Key Generation While working through the SCIM configuration, you will be given the base URL for provisioning, which, if you have a custom domain configured, this URL will automatically be updated with your custom domain like `auth.example.com`. The configuration wizard will prompt you to generate the key to authenticate the SCIM actions. This will create a formatted access key with the [correct permissions](/management/tenant-management/scim). ![Using the Descope SSO Setup Suite to generate SCIM provisioning URL and authorization bearer.](/assets/sso-configuration-suite-11.webp) #### Finishing the SCIM Configuration Once you have finished configuring SCIM, you can click the finish button, which will return you to the start of the wizard if you need to make any additional changes. ![Showing an example of a completed Descope SSO Setup Suite.](/assets/sso-configuration-suite-12.webp) ### Cross App Access (XAA) Configuration This relates to the **[customer-managed agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags)** side of Cross App Access, where you accept XAA tokens (ID-JAGs) your customers' IdPs issue. It is separate from [managing agents in your enterprise](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags), where Descope mints XAA tokens for agents you run. If you host an MCP server for enterprise customers, those customers can let their own users and agents reach it from the identity provider they already run, with no second login or consent screen. That is [Cross App Access](/agentic-identity-hub/enterprise-managed-authorization) (XAA): Descope validates the **ID-JAG** assertions the customer's IdP issues and returns an access token for your server. The Cross App Access section of the Setup Suite is where the customer's IT admin configures that for their own organization, in the same place they already set up SSO and SCIM. They register their workforce IdP as a trusted issuer for their tenant (issuer URL and JWKs URL), and Descope then accepts assertions signed by that IdP. Whether this section appears is controlled by the [SSO Suite Features](/auth-methods/sso/settings#sso-suite-features) setting, alongside the SSO and SCIM sections. ![Cross App Access (XAA) section in the SSO Setup Suite](/assets/sso-configuration-suite-14.webp) Because trust is scoped to the tenant, each customer's assertions only ever buy access within their own tenant. An ID-JAG minted by one customer's IdP can never reach another customer's data. This is what the setup process looks like in the SSO Setup Suite: ![Using the Descope SSO Setup Suite to configure Cross App Access (XAA)](/assets/sso-configuration-suite-13.webp) You can also configure the same settings yourself in the Console. For every field (Resource Server Details, Trusted Issuer, JIT, attribute and group mapping), see [tenant Cross-App Access](/management/tenant-management/sso/cross-app-access). The Setup Suite path exists so the customer can do it without involving you, in the same session where they configure SSO. # Flows (/auth-methods/sso/with-flows) Learn about how to utilize SSO based authentication in Flows. # Single Sign On (SSO) with Flows You can add SSO to a Descope Flow as an authentication action. SSO lets your users sign in through external identity providers like Okta, Azure, and Ping Identity over either SAML or OIDC. Our complete SSO configuration guide can be found on the [overview](/auth-methods/sso) page. ## Flow Actions When using SSO, you have only one authentication related action available. However, there are others related to configuring SSO. - **SSO** - Redirects the user to their corresponding SSO provider, configured for their specific tenant. - **Generate SSO Configuration Admin Link** - Generates a link that can be used with an [Email Connector](/connectors/connector-configuration-guides/messaging) to be sent to a Tenant Admin. - **Revoke SSO Configuration Admin Link** - Revokes access to the SSO Configuration Portal, based on the user's refresh token. ### How to Use SSO Actions To learn more about Actions in general, you can refer to our [guide](/flows/actions) on them. Most of these actions are pretty simple, and you can drop them in your application like any other Action. This is an example of using the **SSO** action in a flow: ![sso-flow-action](/assets/sign-up-or-in-sso-flow-action.webp) #### Redirect URL (Optional) The default URL the end user is redirected to after a successful SSO sign-in. Setting it here overrides the [Post Authentication Redirect URL](/auth-methods/sso/settings#post-authentication-redirect-url) configured in your project settings, and any tenant-specific redirect URL. This field accepts [dynamic values](/flows/dynamic-keys#dynamic-redirect-urls), so one flow can serve several sites. For example, `{{device.location.scheme}}://{{device.location.hostname}}/dashboard` returns the user to the domain hosting the flow. Tenant keys such as `{{tenant.domain}}` also work here; see [Post Authentication Redirect URL](/auth-methods/sso/settings#post-authentication-redirect-url) for the full list. A redirect URL your application passes when it starts the flow takes priority over this field. See [redirect URL precedence](/flows/dynamic-keys#redirect-url-precedence), for more details. #### SSO Login Hint Inside of the SSO flow action, you can add an OIDC login hint. This value (such as the user’s email or username) will be sent to the Identity Provider (IdP) during the SSO authentication request. For example, if you're collecting the user's email earlier in the Flow, you can pass it here to pre-fill the username/email field on the IdP’s login page and streamline the login process. ![sso-login-hint](/assets/sso-login-hint.webp) #### Verify Initiated Email Matches IdP Response Enable **Verify initiated email matches IdP response** inside the SSO flow action to require that the email (or NameID/external ID) the identity provider returns matches the email that started the login, case-insensitively. If they don't match, the action errors out instead of completing sign-in. This guards against a user typing one address into your app while an existing IdP session authenticates a different account. This needs an email available when the action runs, such as one collected from an earlier screen in the flow. This check has no effect on [IdP-initiated logins](/sso/idp-initiated), since there's no email from your side to compare the IdP's response against. ![sso-verify-initiated-email](/assets/sso-verify-initiated-email.webp) #### SSO Enforced/Enabled Condition If you wish for the flow to automatically handle the choice between using SSO or another authentication method like OTP with just one email input field, depending on if SSO is enabled and/or enforced, you can implement this using a **condition** block in your flow. Refer to the [Enforcing SSO Condition doc](/flows/conditions/sso-enforced) for more implementation details. ![sso routing condition](/assets/sso-routing-condition.webp) ## Flow Screens When using SSO, you can use the either use conditions to determine if you want to redirect a user via the **SSO** action, or you can use the **SSO** button in the flow to default to using the SSO action. ![sso-button](/assets/sso-button.webp) If you use the **SSO** button with an email that is not associated with a tenant, or doesn't have SSO configured, it will return an error in the screen. ### Tenant Input Components Once you've configured a tenant name or SSO domain for your tenant, you should be able to sign in with either of them. #### Tenant Name If you wish to sign in with the **name** (slug) of your tenant, use the **Tenant Name** input component: ![Descope slug name SSO Flows for specific Domains edit screen 2](/assets/tenant-name-component.webp) #### Tenant Domain If you wish to sign in with the **SSO Domain** configured under Authentication Methods → SSO on the tenant, use the **Tenant Domain** input component: ![Descope slug name SSO Flows for specific Domains](/assets/tenant-sso-domain-component.webp) #### Using Form Context Keys in Conditions You can utilize the values of `form.tenantDomain` and `form.tenantName` in conditions as well, which are directly fed from the respective components mentioned above. This will allow you to create custom user experiences for specific tenants, identified by either name or domain. ![form-tenant-inputs](/assets/form-tenant-inputs.webp) ### Allowing Duplicate SSO Domains Across Tenants When multiple tenants share the same SSO domain, users need a way to select their specific tenant before initiating SSO authentication. To support this, ensure that the [Allow duplicate SSO domains across tenants setting](/auth-methods/sso/settings#allow-duplicate-sso-domains-across-tenants) is enabled. Collect the user's email address as part of your flow, then check whether multiple tenants exist for that email domain using the `ssoDomainsTenantsCount` key in the condition block. If the value is greater than one, display the screen with the **Switch Tenant** component to allow the user to choose the appropriate tenant. Once a tenant is selected, proceed with SSO authentication using that tenant. ![tenant selector flow](/assets/flow-dup-sso-domain.webp) ## Tenant Flow Parameter Similar to how the tenant input components listed above help you associate users logging in with specific tenants, you can also use the `tenant` flow parameter to do the same. This is referenced in the [Client SDK](/client-sdk/descope-components#descope-flow-component) reference as well, but this parameter allows you to pre-set the tenant before the flow even renders, so the flow doesn't need to rely on the user to identify the correct tenant associated with the user. If you're embedding the Descope flow component in your app, you can just include this as a parameter in the component declaration itself: ```tsx title="app/sso-login.tsx" 'use client' import { Descope } from '@descope/nextjs-sdk/client' export default function SSOLogin() { return ( console.log('Logged in!', e.detail.user)} onError={(e) => console.log('Login failed', e)} /> ) } ``` ```tsx title="SSOLogin.tsx" import { Descope } from '@descope/react-sdk' const SSOLogin = () => ( console.log('Logged in!', e.detail.user)} onError={(e) => console.log('Login failed', e)} /> ) export default SSOLogin ``` If you're using Descope Applications with our Auth Hosting application, you can read about the `tenant` parameter [here](/identity-federation/auth-hosting#more-customization-options). Don't have an IdP handy? Configure a [Mock SAML tenant](/management/tenant-management/sso/mock-saml-testing) to test the full SSO round-trip in minutes. ## Error Handling Error handling is handled like any other action. You can refer to our [Flow Error Handling](/handling-flow-errors/customizing-flow-errors) guide for more details. # Audit Events (/audit-trails-and-integrations/audit-events) This article covers project-level and company-level audit events that Descope logs. # Audit Events Descope exposes a set of audit events that you can query through the [Descope UI](https://app.descope.com/audits), and also using the [Management SDKs](/audit-trails-and-integrations/sdks) or [Search Audit](/api/management/audit/search-audit) API. Audit events come in two scopes: - **Project-level events** record activity inside a single Descope project: end-user logins, user and tenant changes, flow updates, and so on. - **Company-level events** record actions taken across your Descope company, such as creating or deleting projects, managing management keys, and changing company settings. Company events appear in the same audit table and SDK responses as project events, tagged with `Level=Company`. See [Company-level auditing](/audit-trails-and-integrations#company-level-auditing) for how to filter them. For more information on SCIM audit events, see the [SCIM Audit Events](/audit-trails-and-integrations/audit-events/scim-audit-events) article. ## Project-level Audit Events These events are logged within a single project and are scoped to that project's audit trail. When the action performed is considered sensitive, the type is defined as "Warning". | Name | Type | More information | |--------------------------------|-------------|---------------------------------------------------------------| | UserCreated | Information | If [self registration](https://docs.descope.com/knowledgebase/general/selfregistration/) is available, the "Actor ID" will be the same as the "User ID". Otherwise, the actor will be the Descoper or the used Management Key. | | UsersCreated | Information | Multiple users created. | | UserDeleted | Information | User deleted. | | UserModified | Information | User modified. | | UsersModified | Information | Multiple users modified. | | UsersDeleted | Information | Multiple users deleted. | | UsersExported | Information | One or more users exported from the console. | | AccessKeyCreated | Information | Access key created. If the key is a SCIM key, the "data" section includes `scim: true` | | AccessKeyDeleted | Information | Access key deleted. If the key is a SCIM key, the "data" section includes `scim: true` | | AccessKeyModified | Information | Access key modified. If the key is a SCIM key, the "data" section includes `scim: true` | | AccessKeysDeleted | Information | Multiple Access Keys Deleted. | | LoginSucceed | Information | If impersonation was performed, the "Method" field will be "Impersonate". | | LoginFailed | Warning | Reason for failure is shown inside the "Data" section, under "error_message". | | LoginExceedMaxAttempts | Warning | Indicates that max attempts for user has been reached, user is disabled. | | LoginStarted | Information | Indicates that a login process has started for multi-step authentication methods, like SSO, passkey, OTP and more. | | LoginStartedFailed | Warning | Indicates that the LoginStarted event had failed. When the user did not complete the process correctly, or there is a problem with the authentication setup. | | UserRefresh | Information | Only available in verbose mode. | | ExternalSessionMigrationSuccess | Information | An external session token was exchanged for a Descope session. | | ExternalSessionMigrationFailure | Warning | An external session token could not be exchanged for a Descope session. The reason for failure is shown inside the "Data" section, under "error_message". | | PermissionCreated | Information | Permission created. | | PermissionModified | Information | Permission modified. "Data" contains the "permission_id" that has been affected. | | PermissionDeleted | Warning | Permission deleted. "Data" contains the "permission_id" that has been affected. | | RoleCreated | Information | Role created. "Data" contains the "role_id" and "role_name" of the role that was created. Includes tenant association if created at tenant level. | | RoleModified | Information | Role modified. "Data" contains the "role_id" that has been affected. | | RolesDeleted | Warning | Role deleted. "Data" contains the "role_id" that has been affected. | | RolesImported | Warning | Role imported. "Data" contains the "role_id" that has been affected. | | UsersRolesAssociationsModified | Information | User/Role or User/Tenant-Role associations changed (assigned, removed, set, or recalculated). See [Role Association Audit Detail](/audit-trails-and-integrations#role-association-audit-detail), for more details. | | AuthzNamespaceCreated | Information | A [ReBAC namespace](/authorization/rebac/define-schema) was created. | | AuthzNamespaceModified | Information | A ReBAC namespace was modified. | | AuthzNamespaceDeleted | Information | A ReBAC namespace was deleted. | | AuthzRelationDefinitionCreated | Information | A ReBAC [relation definition](/authorization/rebac/define-schema) was created. | | AuthzRelationDefinitionModified| Information | A relation definition's target type or permission expression changed as part of a schema update. | | AuthzRelationDefinitionDeleted | Information | A ReBAC relation definition was deleted. | | AuthzSchemaDeleted | Information | The entire ReBAC schema, including all namespaces and relation definitions, was deleted. | | AuthzRelationsCreated | Information | One or more ReBAC [relations](/authorization/rebac/create-relations) were created. | | AuthzRelationsDeleted | Information | One or more ReBAC relations were deleted. | | ProjectSettings | Information | Project Settings modified. | | TenantSettings | Information | Contains the tenant's ID in the "data" section. | | MagicLinkSettings | Information | Magic Link related settings were changed. | | EnchantedLinkSettings | Information | Enchanted Link related settings were changed. | | OTPSettings | Information | OTP related settings were changed. | | SAMLSettings | Information | SAML related settings were changed. | | OAUTHSettings | Information | OAuth related settings were changed. | | WebauthnSettings | Information | Webauthn related settings were changed. | | TOTPSettings | Information | TOTP related settings were changed. | | MessageProviderSettings | Information | Message Provider related settings were changed. | | PasswordSettings | Information | Password Settings / Policy Changed. | | CustomAttributeAdded | Information | Custom Attribute created. | | CustomAttributeDeleted | Information | Custom Attribute deleted. | | ConnectorModified | Information | A connector has been modified. | | ConnectorCreated | Information | A connector has been created. | | ConnectorDeleted | Information | A connector has been deleted. | | CustomAttributesMissing | Error | Missing User / Tenant Custom Attribute. This indicates that the tenant or user selected does not have the required attribute that was used inside a flow / SDK. | | TenantDeleted | Information | A tenant has been deleted. | | TenantCreated | Information | A tenant has been created. | | TenantCustomAttributeModified | Information | A Tenant's custom attribute has been modified. | | TenantDomainModified | Information | A tenant's email domain has been modified. | | TenantProvisioningModified | Information | A Tenant's Provisioning Settings has been modified. | | TenantCustomAttributeAdded | Information | A Tenant's custom attribute has been added. | | TenantCustomAttributeDeleted | Information | A Tenant's custom attribute has been deleted. | | FlowsDeleted | Information | Contains the flow id & name that has been deleted in the data. | | ThemeUpdated | Information | A theme has been updated. | | FlowCreated | Information | Contains the flow id & name that has been created in the data. | | FlowUpdated | Information | Contains the flow id & name that has been changed in the data. | | CreatePassword | Information | A password has been created for a user. | | ChangePassword | Information | A password has been changed for a user. | | ExpirePassword | Information | A password has been expired for a user. | | Delete Password | Information | A password has been deleted for a user. | | RemovePasskeys | Information | A passkey has been removed for a user. | | SignKeyGeneratedRevoked | Information | The project's JWK (signing key for validating JWTs) has been rotated. | | SSOConfigurationLinkGenerated | Information | SSO configuration link was generated. The generated link can be found inside data under "link". The link's expiration time can be found inside data under "expiration_time". | | SSOConfigurationLinkRevoked | Information | SSO configuration link was revoked. The revoked link can be found inside data under "link". | | InvalidIPEventData | Information | An attempt to use access key filtered by IP was denied. The Remote Address holds the origin IP Address. | | UserTemporaryPasswordLock | Warning | A user's password has been temporarily locked due to too many failed login attempts. The lock's expiration time can be found inside data under `endTime` | | ThirdPartyAppConsentCreated | Information | An inbound app consent was created. The related scopes and ID of the inbound application can be found inside the "data" section. | | ThirdPartyAppConsentModified | Information | An inbound app consent was modified. The related ID of the inbound application can be found inside the "data" section. | | ThirdPartyAppConsentDeleted | Information | An inbound app consent was revoked. The related scopes and ID of the inbound application can be found inside the "data" section. | | ThirdPartyApplicationCreated | Information | An inbound app was created. The related ID, whether or not it was [dynamically registered](/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr), and the verification status of the inbound application can be found inside the "data" section. | | ThirdPartyApplicationModified | Information | An inbound app was updated. The related ID and verification status of the inbound application can be found inside the "data" section. | | ThirdPartyApplicationDeleted | Warning | An inbound app was deleted. The related ID of the inbound application can be found inside the "data" section. | | OutboundAppCreated | Information | An outbound app was created. The related ID of the outbound application can be found inside the "data" section. | | OutboundAppDeleted | Warning | An outbound app was deleted. The related ID of the outbound application can be found inside the "data" section. | | OutboundAppAccessControlRuleCreated | Information | An outbound app access control rule was created. The inbound app association, conditions, and accessible outbound app scopes can be found inside the "data" section. | | OutboundAppAccessControlRuleModified | Information | An outbound app access control rule was modified. The inbound app association, conditions, and accessible outbound app scopes can be found inside the "data" section. | | OutboundAppAccessControlRuleDeleted | Warning | An outbound app access control rule was deleted. The related ID of the access control rule can be found inside the "data" section. | | OutboundAppAccessControlAccessDenied | Warning | A [Policy](/policies) denied access to a **Connection** or outbound credential: the client requested scopes no active rule permits. The **Data** section includes the client, Connection, and requested scopes. See [Policy violations](/audit-trails-and-integrations/audit-events#policy-violations). | | ActiveUsersHour | Information | The number of active users in the last hour. | | ActiveUsersDay | Information | The number of active users in the last day. | | ActiveUsersMonth | Information | The number of active users in the last month. | | ActiveAccessKeysHour | Information | The number of active access keys used for machine-to-machine authentication in the last hour. | | ActiveAccessKeysDay | Information | The number of active access keys used for machine-to-machine authentication in the last day. | | ActiveAccessKeysMonth | Information | The number of active access keys used for machine-to-machine authentication in the last month. | | ActiveConsentHour | Information | The number of active consents that resulted in token exchanges in the last hour. | | ActiveConsentDay | Information | The number of active consents that resulted in token exchanges in the last day. | | ActiveConsentMonth | Information | The number of active consents that resulted in token exchanges in the last month. | | SCIMEvent | Information | Detailed audit for a SCIM provisioning call (user or group create, update, patch, or delete).

**Data** includes `scim_action`, `scim_request`, and on success `scim_result` and optional `Change`; failures include `error`.

See [SCIM Audit Events](/audit-trails-and-integrations/audit-events/scim-audit-events). | | ManagementFlowTriggered | Information | Detailed audit for a Management Flow execution.

**Data** includes `flow_id`, `flow_execution_id`, flow start time, and `triggering_event_type` (for example, `UserCreated`).

When the trigger is user-related, user context appears as `user_id`, `users_ids`, and `users_login_ids`. For other event types, **Data** reflects the context of that event instead. | ### Policy violations When a [Policy](/policies) denies a token request, Descope logs a **Warning** audit event. These events appear in the [Agentic Activity Dashboard](/management/project-settings/project-dashboard#agentic-activity-dashboard) and in the [audit trail](https://app.descope.com/audits). | Name | Type | More information | |------|------|------------------| | `OutboundAppAccessControlAccessDenied` | Warning | A client was denied **Connection** scopes because no policy permitted the exchange. The **Data** section includes the client, Connection, and requested scopes. | Resource scope denials (for example, an MCP client requesting tool scopes your policies do not allow) are also logged as **Warning** events. Search the audit trail by client ID or filter on Warning-type events; the **Data** section includes the Resource, requested scopes, and the denial reason. To investigate a spike in violations, start with the dashboard widgets, then drill into individual events in the audit trail or via the [Search Audit API](/api/management/audit/search-audit). ## Company-level Audit Events Company-level events record actions that affect the whole Descope company, not a single project: creating or deleting projects, managing management keys, and changing company settings. They appear in the same audit table as project events with a `Level=Company` tag, and you can isolate them by filtering on that level in the [Descope console](https://app.descope.com/audits) or in the [Search Audit](/api/management/audit/search-audit) API. Company-level events are visible to Descopers with the **Company Admin** role or another role that grants audit access at the company scope. See [Descoper Roles](/management/company-settings#descoper-roles) for details. | Name | Type | More information | |--------------------------------|-------------|-------------------------------------------------| | ProjectCreated | Information | A new project was created in the company. The **Data** section contains the new `project_id`. | | ProjectDeleted | Warning | A project was deleted from the company. The **Data** section contains the deleted `project_id`. | | CompanySettingsModified | Information | Company-level settings were changed (for example, SSO enforcement, MFA enforcement, or Descoper roles). | | ManagementKeyCreated | Information | A management key was created. The **Data** section contains the key's `id` and `name`. | | ManagementKeyModified | Information | A management key was modified. The **Data** section contains the key's `id`. | | ManagementKeyDeleted | Information | A management key was deleted. The **Data** section contains the key's `id`. | | ManagementKeysCreated | Information | Multiple management keys were created in a single operation. | | ManagementKeysModified | Information | Multiple management keys were modified in a single operation. | | ManagementKeysDeleted | Information | Multiple management keys were deleted in a single operation. | ## Fields The fields below appear on both project-level and company-level audit events (company-level events also include a `Level=Company` tag). * Actor ID - Identifies who performed the action on the entity: * **Descoper actions**: the Descoper's user ID. * **Management API or SDK**: the management key ID tied to that call. This also applies to actions taken within a Management Flow when the flow is triggered by the SDK or API. * **Custom audit events**: the value you set on the [Generate Audit Event](/flows/actions/generate-audit-event#attributing-the-actor) action, including a dynamic value drawn from the flow context. * User ID - The destination user that the action was performed on. * Action - The action performed. * Occurred - Date of occurrence. * Device - The source device of the action, could be "Desktop", "Mobile", etc. Can also reflect the SDK that was used - e.g. "NodeJS". * Method - The authentication method used. * Remote Address - IP address (v4/v6) of the origin of the request. * Login IDs - The primary identification for the authentication. * Country - Origin of the request, most of the times, bound to the IP Address. * Data - Holds the entire request sent to Descope's API in a JSON format. Displays raw information about the entire request including more details about the user, their device, the flow ID and execution ID, etc. * SP SAML / OIDC request - Only For LoginStarted Event. Contains details of the SP request initiated by Descope for SSO. * IdP SAML / OIDC response - Only for LoginSucceed / LoginFailed events. Contains the IdP response for SSO. * Generated user from IdP SAML / OIDC response - Only for LoginSucceed. Contains the generated user object from the IdP response for SSO. * Generated roles from IdP SAML / OIDC response - Only for LoginSucceed. Contains the generated roles from the IdP response for SSO. * External Request ID - Identifier you can pass in any SDK/API call to correlate Descope events with your system logs. ### Using External Request ID The External Request ID allows you to correlate Descope audit events with your application's logging system. You can pass this identifier in SDK calls, and it will appear in the audit event's data field. For example: ```javascript import { Descope } from '@descope/react-sdk' const App = () => { // Generate a unique request ID (you can use any format that works for your system) const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`; return ( ); }; ``` ```javascript import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: 'your-management-key' }); // Create user with external request ID const requestId = 'user-creation-batch-001'; await descopeClient.management.user.create( 'user@example.com', // loginId { email: 'user@example.com', displayName: 'John Doe' }, // user details requestId // external request ID ); ``` The External Request ID will appear in the audit event's `data` field, allowing you to correlate Descope events with your application logs for better debugging and monitoring. ### SSO Related Fields If you wish to troubleshoot your tenants' SSO user/group mapping as well as the configuration, you can use these specific fields to do so: You can easily search for these using the keyword "SSO" in the search bar. **LoginSuccess / LoginFailure** This event is triggered when a user logs in successfully or fails to log in using SSO. ![audit login success](/assets/audit-login-success.webp) **LoginStarted** This event is triggered when a user starts the login process using SSO. ![audit login started](/assets/audit-login-started.webp) **SAML** * IdP SAML response. the SAML response from the IdP. Contains the authenticated user's SAML assertion, including the user's attributes and groups. * Generated user from IdP SAML response. Contains the derived user object that was generated as a result of the user's assertion in the SAML response. * Generated roles from IdP SAML response. Contains the derived roles that were generated as a result of the user's assertion in the SAML response. **OIDC** * Identity Provider (IdP) OIDC response. Contains the response from the IdP's `userinfo` endpoint, including the user's profile, groups, and roles. * Generated user from the IdP OIDC response. Contains the derived user object and roles that were generated as a result of the IdP's `userinfo` endpoint response. ### Login Started Failed The `LoginStartedFailed` audit indicates that there was an issue starting or during the login action performed. The reason can vary from a faulty Descope flow setup to a misconfiguration in the authentication method settings. In some scenarios where there is an issue with a login action, the `LoginStartedFailed` event can indicate what authentication method was used, the origin of the device, and the user's loginId used for the login attempt. Here are some examples for when this event can be triggered: 1. Starting a passkey authentication for a user that has no passkey set. 2. Starting an OTP / email authentication, when there are issues with the connector setup. ## Creating Custom Audit Events Beyond what is provided by default by Descope, you may need to introduce additional events to provide greater visibility and transparency into your product's integration with Descope. With Descope, you can create custom audit events as well, using the [Management API](/api/management/audit/create-audit-event), the [`Generate Audit Event` Flow Action](/flows/actions/generate-audit-event), or the [Descope SDK](/audit-trails-and-integrations). ## Shipping Logs You can use our out-of-the-box [connectors](https://app.descope.com/connectors) to [ship the audit](/audit-trails-and-integrations/audit-trail-streaming) event to different third-party applications, such as DataDog, Segment and HubSpot to [orchestrate the user journey](https://www.descope.com/blog/post/hubspot-segment-connector). ## Security Related Fields For detailed information about security-related fields in audit events, including J4A fingerprinting and ASN (Autonomous System Number) data, please refer to our dedicated [Request Security Headers](/additional-security-features-in-descope/request-security-headers) article. These fields provide valuable insights into potential security threats and the network infrastructure of request origins, helping you identify and mitigate security risks more effectively. ## Audit Widget When your project supports multiple tenants, Descope lets you hand over the audit logs to your customers using the audit log management widget. This widget helps you automate exposing this kind of audit logs to your customers by embedding an out-of-the-box component in your app. To read more about the widget, click [here](/widgets/admins#audit-widget). ## Correlating Audit and Troubleshooting Logs When debugging issues that users might face, it is crucial to know the context of the user's actions, whether they tried to log in with a specific authentication method or even if they reached the point in the flow where OTP is involved. The troubleshooting logs contain a **flow execution ID**. Matching this value with the audit's `correlation_id` (inside the data field) is a powerful tool that provides you with the context of what the user did and where they encountered an issue, making your troubleshooting process more efficient. # SCIM Audit Events (/audit-trails-and-integrations/audit-events/scim-audit-events) Learn how Descope records detailed audit entries for SCIM provisioning, including requests, results, membership changes, and errors. # SCIM Audit Events Descope can generate an audit event for every SCIM call your IdP makes against a tenant (create, update, patch, or delete for users and groups). Each event fires off a `SCIMEvent` **Action** that stores structured data so you can see what was sent in the request, what changed, and whether the call succeeded. These events are different from the generic user-related audit events created from SCIM, such as `UserCreated` or `UserModified`. Those may still appear when Descope updates users or groups, regardless of if SCIM audit events are enabled or not. This page covers user/group provisioning related audit events. The `AccessKeyCreated` / `AccessKeyModified` / `AccessKeyDeleted` audit events related to a SCIM access key are distingued by the [`scim` field](/audit-trails-and-integrations/audit-events). ## Overview When SCIM audit logging is enabled for your organization, Descope writes one audit row per qualifying SCIM operation. The **Action** itself will be titled `SCIMEvent` and **Type** is Information, including for failures. If there was a failure than the `error` field will be set under **Data**. SCIM-specific keys on the row are documented in the [Data Fields Reference](#data-fields-reference) table below. Standard audit **Data** fields (such as `request_details`, `correlation_id`, and network or device context when available) are populated the same way as described in [Audit Events](/audit-trails-and-integrations/audit-events). If SCIM is not enabled for the tenant on a given request, Descope may skip writing a SCIM audit entry for that call. If you expect events but see none, confirm [SCIM configuration](/api/management/tenants/scim) and that your license includes SCIM. ## Where to Find SCIM Audits You can inspect SCIM audits in the following places: 1. **Descope Console** — Open [Audits](https://app.descope.com/audits) and filter or search for action **`SCIMEvent`**. 2. **Management API / SDKs** — Call [Search Audit](/api/management/audit/search-audit) with `actions` containing `SCIMEvent`, optionally combined with tenant or text search. ## Data Fields Reference | Key | Description | |-----|-------------| | `scim_action` | The SCIM operation; see [SCIM actions](#scim-actions). | | `scim_request` | What the IdP sent: object built from the inbound SCIM body (non-meaningful internal fields may be omitted). | | `scim_result` | On success, SCIM-shaped object for the resource after the call (or a small identifier map for deletes). Omitted when the operation fails. | | `Change` | When present, a normalized field-level diff (same shape as on `UserModified`), including for some creates and for group or membership updates. | | `error` | Present on failure; human-readable error message. | ## SCIM Actions The **`scim_action`** value is always one of the following: | `scim_action` | Operation | |---------------|-----------| | `scim_create_user` | Create user | | `scim_update_user` | Replace user (PUT) | | `scim_patch_user` | Patch user | | `scim_delete_user` | Delete user | | `scim_create_group` | Create group | | `scim_update_group` | Replace group (PUT) | | `scim_patch_group` | Patch group | | `scim_delete_group` | Delete group | For user operations, **User ID** and **Login IDs** on the audit row are set when Descope resolves the affected user. For group-only operations, those user-oriented columns may be empty; use **`scim_request`** / **`scim_result`** and **`Change`** for group and membership details. ## Group Membership in `Change` For group create, update, and patch, membership updates may appear under **`Change`** using these keys: | Key | Description | |-----|-------------| | `added_members` | Member identifiers added. The list is included only when its size is within Descope's limit (see below). | | `added_members_count` | Always set to the number of added members. | | `removed_members` | Member identifiers removed (same list/count rules as `added_members`). | | `removed_members_count` | Always set to the number of removed members. | | `replaced_members` | Member identifiers after a replace operation (for example, via PATCH). Same list/count rules as `added_members`. | | `replaced_members_count` | Always set to the number of members in the replacement set. | If the number of members in a list exceeds Descope's maximum (default **40** per list), Descope records the corresponding `*_count` field but does **not** include the full member list, so audit payloads stay bounded while still showing how many members were affected. ## Examples ### Successful User Creation Expand the audit **Data** in the console or API response. It will resemble: ```json JSON { "scim_action": "scim_create_user", "scim_request": { "userName": "alice@example.com", "displayName": "Alice Example" }, "scim_result": { "id": "u1" }, "correlation_id": "xx", "request_details": {} } ``` Exact keys depend on your IdP and attribute mapping. **`Change`** may also be present on create when Descope records provisioned fields. ### Failed Operation ```json JSON { "scim_action": "scim_create_user", "scim_request": { "userName": "alice@example.com" }, "error": "user already exists" } ``` # Descope Components (/client-sdk/descope-components) Learn about the Descope components and how to implement them within your application. # Descope Components Descope Components are components used to render Descope Flows and provide context in your application. This includes the UI elements and logic as defined in your project console. Before using the Descope Components, make sure to [import and initialize the relevant SDK.](/client-sdk/initialize-sdk) ## Auth Provider The `AuthProvider` is the main component that wraps your application and provides the necessary context for the Descope SDK to work. ### Base URL Configuration Here is an example of configuring the `AuthProvider` with a custom domain: ```javascript // Using custom domain for baseUrl ``` #### baseUrl The `baseUrl` is the URL (your [custom domain](/how-to-deploy-to-production/custom-domain), if configured) where all backend/flow requests will go to from your client application. This includes authentication requests, flow interactions, and API calls to Descope services. When using a custom domain for Descope, you should set this to your custom domain URL instead of the default Descope URLs. #### baseStaticUrl The `baseStaticUrl` is specifically for the URL where the SDK will fetch static assets for flow rendering, such as CSS files, images, and other static resources. This is typically your custom domain + "/pages" (e.g., `https://auth.yourcompany.com/pages`). #### baseCdnUrl The `baseCdnUrl` is the URL of the custom CDN that you can use to host your static assets. This is used if you do not wish to use `descopecdn.com` as the default CDN. Both the `baseUrl` and `baseStaticUrl` are recommended to be set when using a [custom domain](/how-to-deploy-to-production/custom-domain), or if you're on a [private cloud deployment](/how-to-deploy-to-production/private-cloud) of Descope. ### Request Hooks The AuthProvider can be configured with hooks to intercept HTTP requests for the purposes of injecting custom headers, logging, monitoring, or transforming responses. See [Request Hooks](/client-sdk/descope-components/request-hooks) for more information. ### Cookie Configuration Options This section specifically applies to session tokens. If you're looking for the docs on refresh token management with cookies, see our docs [here](/security-best-practices/refresh-token-storage#handling-refresh-tokens-in-cookies). You can choose to store the session token in a cookie instead of localStorage. This is done by passing the `sessionTokenViaCookie` prop to the `AuthProvider` component. The Descope SDK will then automatically store the session token on the `DS` cookie. Session tokens can grow in size, especially when adding user authorization claims (roles and permissions) or custom claims. Only store your session token with cookies, if you can ensure the session token will stay small in size (less than 2kb). Here are the available cookie configuration options, in the `AuthProvider` component: | Option | Type | Description | Default | |-----------|-------------------------------|----------------------------------------------------------|---------| | sameSite | `Strict`,`Lax`,`None` | Controls the cookie's same-site policy. | `Strict` | | secure | `boolean` | Specifies whether the cookie is sent over secure (HTTPS) connections. | `true` | | cookieName | `string` | Session token cookie name. Change to avoid conflicts across projects on the same domain. | `DS` | | domain | `string` | The domain the session token cookie is set on. This lets the client SDK set the session token on whatever domain you want, such as a parent domain to share it across subdomains. | Current domain or [Project-level cookie settings](/security-best-practices/session-token-storage#managing-with-cookies) | If you're managing the [**session token** as a cookie](/security-best-practices/session-token-storage#managing-with-cookies), all of these client SDK cookie configuration settings will be ignored. To configure a custom cookie name, do so in the [End action](/flows/actions/end-action) of your flow instead. Here is an example of setting `sameSite`, `secure`, `cookieName`, and `domain` explicitly: ```tsx ``` ### Example Configuration of AuthProvider (for each SDK) ```javascript import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( ); }; ``` ```javascript import { AuthProvider } from '@descope/nextjs-sdk'; export default function RootLayout({ children }) { return ( {children} ); } ``` ```javascript import descopeSdk from '@descope/web-js-sdk'; const myProjectId = 'xxx'; const sdk = descopeSdk({ /* Descope Project ID (Required) */ projectId: myProjectId, sessionTokenViaCookie: true, baseUrl: "https://auth.app.example.com" baseStaticUrl: "https://auth.app.example.com/pages" }); ``` ```javascript import { createApp } from 'vue'; import App from './components/App.vue'; import descope from '@descope/vue-sdk'; const app = createApp(App); app.use(descope, { projectId: 'project-id', sessionTokenViaCookie: true, baseUrl: "https://auth.app.example.com" baseStaticUrl: "https://auth.app.example.com/pages" }); ``` ```javascript import { DescopeAuthModule } from '@descope/angular-sdk'; ... @NgModule({ declarations: [ ... ], imports: [ ... DescopeAuthModule.forRoot({ projectId: environment.descopeProjectId, baseUrl: environment.descopeBaseUrl || '', baseStaticUrl: environment.descopeBaseStaticUrl || '', sessionTokenViaCookie: true }) ], ... }) export class AppModule {} ``` Now, whenever you call `fetch()`, the cookie will automatically be sent with the request. Descope backend SDKs also support extracting the token from the DS cookie. The session token cookie, by default, is set as a `Secure` cookie and therefore will only be sent over HTTPS connections. In addition, some browsers (e.g. Safari) may not store `Secure` cookie if the hosted page is running on an HTTP protocol. ## Descope Flow Component Customize the Descope Flow Component by passing in the following props: - `theme`: theme can be "light", "dark" or "os", which auto selects a theme based on the OS theme. Default is "light" - `styleId`: style Id can be the id of the style you wish to run your flows with - `themeOverride`: override primary and secondary colors for light and/or dark modes at runtime. See [themeOverride](#theme-override) below. - `debug`: debug can be set to true to enable debug mode - `redirectUrl`: Redirect URL for OAuth and SSO (will be used when redirecting back from the OAuth provider / IdP), or for "Magic Link" and "Enchanted Link" (will be used as a link in the message sent to the the user) - `redirectAfterSuccess`: Redirect URL after the flow's success. - `redirectAfterError`: Redirect URL upon error in the flow. - `autoFocus`: autoFocus can be true, false or "skipFirstScreen". Default is true. - true: automatically focus on the first input of each screen - false: do not automatically focus on screen's inputs - "skipFirstScreen": automatically focus on the first input of each screen, except first screen - `errorTransformer`: errorTransformer is a function that receives an error object and returns a string. The returned string will be displayed to the user. For more information, refer to [Customizing Flow Errors.](/handling-flow-errors/customizing-flow-errors#customizing-flow-errors) - `locale`: locale can be [any supported locale](/management/localization#language-support) which the flow's screen translated to. If not provided, the locale is automatically taken from the browser. - `logger`: logger is an object describing how to log info, warn and errors. - `dismissScreenErrorOnInput`: when used, the general error message that appears on screen on user input will be cleared once the form is being re-edited. The value should be set to true. For information on parameters that can be utilized within your flow (e.g. tenant, screen inputs, etc.), refer to the [Flow Inputs Doc.](/flows/dynamic-keys/flow-inputs) ```javascript import { Descope } from '@descope/react-sdk' const App = () => { return ( // themeOverride={themeOverride} // debug={true} // locale="en" // redirectUrl= // redirectAfterSuccess="/" // redirectAfterError="/error-page" // autoFocus="skipFirstScreen" // errorTransformer={errorTransformer} /> ) } ``` ```javascript import { Descope } from '@descope/nextjs-sdk'; const Page = () => { return ( // redirectAfterSuccess="/" // redirectAfterError="/error-page" // autoFocus="skipFirstScreen" // errorTransformer={errorTransformer} /> ); }; ``` ```javascript import descopeSdk from '@descope/web-js-sdk'; const myProjectId = 'xxx'; const sdk = descopeSdk({ /* Descope Project ID (Required) */ projectId: myProjectId, }); await sdk.flow.start( 'flowId', { // redirectUrl: // locale: } ); ``` ```javascript ``` ```javascript > ``` ### Theme Override Use `themeOverride` to tweak **primary** and **secondary** colors without creating a separate [style file](/management/styles). Below is an example of a `themeOverride` object: ```javascript const themeOverride = { light: { globals: { colors: { primary: { main: '#FFFFFF', dark: '#710A86', light: '#DA5DF3', highlight: '#EEB6FA', contrast: '#FFFFFF', }, secondary: { main: 'black', dark: '#6410BC', light: '#636C74', highlight: '#005700', contrast: '#A24309', }, }, }, }, dark: { globals: { colors: { primary: { main: '#BD10E0', dark: '#6410BC', light: '#636C74', highlight: '#005700', contrast: '#A24309', }, secondary: { main: 'black', dark: '#6410BC', light: '#636C74', highlight: '#005700', contrast: '#A24309', }, }, }, }, }; ``` ### Triggered Events - `onReady`: onReady is an event that is triggered when the flow is ready to be displayed. It's useful for showing a loading indication before the page is ready. - `onSuccess`: Function that executes when authentication succeeds. User information is returned and accessible with e.detail.user. - `onError`: Function that executes with authentication fails. ```javascript import { Descope } from '@descope/react-sdk' const App = () => { return ( { console.log('Flow is ready'); }} onSuccess={(e) => console.log(e.detail.user)} onError={(e) => console.log('Could not log in!')} /> ) } ``` ```javascript import { Descope } from '@descope/nextjs-sdk'; const Page = () => { return ( { console.log('Flow is ready'); }} onSuccess={(e) => console.log(e.detail.user)}; onError={(e) => console.log('Could not log in!')}; /> ); }; ``` ```javascript ``` ```javascript ``` ## Default Flows Use default flow components that render the Descope component with a predefined flow ID. ```javascript import { SignInFlow } from '@descope/react-sdk' // you can choose flow to run from the following import { SignUpFlow } from '@descope/react-sdk' import { SignUpOrInFlow } from '@descope/react-sdk' const App = () => { return ( console.log('Logged in!')} onError={(e) => console.log('Could not logged in!')} /> ) } ``` ```sh import { Descope } from '@descope/nextjs-sdk'; // you can choose flow to run from the following import { SignInFlow, SignUpFlow, SignUpOrInFlow } from '@descope/nextjs-sdk' const Page = () => { return ( console.log('Logged in!')} onError={(e) => console.log('Could not logged in!')} /> ); }; ``` ```html ``` ## Widgets Widgets are Descope components that let you delegate operations to your customers. Whether your implementation is tenant-based or not, you can embed these components inside your application to allow your customers to perform various self-service operations. Customize the widgets by passing in the following props. These customizations can be applied to any of the available widgets: - `theme`: theme can be "light", "dark" or "os", which auto selects a theme based on the OS theme. Default is "light" - `widgetId`: widgetId is the ID of the widget you wish to use. - `onLogout`: onLogout is an event that is triggered when the user logs out. It's useful for redirecting the user to the login page. - `onReady`: onReady is an event that is triggered when the widget is ready to be displayed. It's useful for showing a loading indication before the page is ready. ```javascript import { UserProfile } from '@descope/react-sdk'; { console.log('Widget is ready'); }} onLogout={() => { // add your own logout callback here window.location.href = '/login'; }} /> ``` ```javascript import { UserProfile } from '@descope/nextjs-sdk'; { console.log('Widget is ready'); }} onLogout={() => { // add your own logout callback here window.location.href = '/login'; }} /> ``` ```html ``` ```javascript ``` ```javascript ``` You can learn more about Widgets and their implementation on the [Widgets Overview doc](/widgets). # Request Hooks (/client-sdk/descope-components/request-hooks) Intercept and observe HTTP requests made by the Descope SDK using beforeRequest, afterRequest, and transformResponse hooks. # Request Hooks The Descope SDK exposes a `hooks` configuration option that lets you intercept every HTTP request the SDK makes. This is useful for injecting custom headers, logging, monitoring, or transforming responses before the SDK processes them. Hooks are available in the **React SDK** (via the `AuthProvider` component) and the **WebJS SDK** (via `createSdk`). ## Available Hooks | Hook | When it runs | Can modify request? | Can modify response? | |---|---|---|---| | `beforeRequest` | Before each outbound request | Yes | No | | `afterRequest` | After each response is received | No | No | | `transformResponse` | After response is received, before SDK processes it | No | Yes | ## beforeRequest Runs before every outbound SDK request. Receives the full request config and **must return it** — modified or unmodified. Use this to inject headers, append query parameters, or log outbound traffic. ```typescript type BeforeRequest = (config: RequestConfig) => RequestConfig; type RequestConfig = { path: string; method: string; headers?: HeadersInit; queryParams?: Record; body?: any; token?: string; }; ``` ```javascript import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( { config.headers = { ...config.headers, 'X-Correlation-Id': crypto.randomUUID(), }; return config; }, }} > ); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__', hooks: { beforeRequest: (config) => { config.headers = { ...config.headers, 'X-Correlation-Id': crypto.randomUUID(), }; return config; }, }, }); ``` ## afterRequest Runs after a response is received. Receives both the original request config and the `Response` object. The return value is ignored — use this hook for logging and monitoring only. ```typescript type AfterRequest = (req: RequestConfig, res: Response) => void | Promise; ``` ```javascript import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( { console.log(`[Descope] ${req.method} ${req.path} → ${res.status}`); }, }} > ); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__', hooks: { afterRequest: async (req, res) => { console.log(`[Descope] ${req.method} ${req.path} → ${res.status}`); }, }, }); ``` ## transformResponse Runs after the response arrives but before the SDK parses it. The response object is extended with a `cookies` field containing parsed `Set-Cookie` values. Return the (optionally modified) response. ```typescript type TransformResponse = (res: Response & { cookies: Record }) => Promise; ``` This hook is primarily useful in environments where `Set-Cookie` headers are inaccessible to client-side code — for example, when tokens are stored in `HttpOnly` cookies behind a reverse proxy. It lets you extract cookie values and make them available to the SDK before it processes the response. ```javascript import { AuthProvider } from '@descope/react-sdk'; const transformResponse = async (res) => { // res.cookies contains parsed Set-Cookie headers if (res.cookies.DS) { console.log('Session cookie present'); } return res; }; const AppRoot = () => { return ( ); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__', hooks: { transformResponse: async (res) => { // res.cookies contains parsed Set-Cookie headers if (res.cookies.DS) { console.log('Session cookie present'); } return res; }, }, }); ``` ## Using Multiple Hooks Together All three hooks can be combined on a single SDK instance: ```javascript import { AuthProvider } from '@descope/react-sdk'; const AppRoot = () => { return ( { config.headers = { ...config.headers, 'X-Request-Start': Date.now().toString() }; return config; }, afterRequest: async (req, res) => { console.log(`[Descope] ${req.method} ${req.path} → ${res.status}`); }, transformResponse: async (res) => res, }} > ); }; ``` ```javascript import createSdk from '@descope/web-js-sdk'; const sdk = createSdk({ projectId: '__ProjectID__', hooks: { beforeRequest: (config) => { config.headers = { ...config.headers, 'X-Request-Start': Date.now().toString() }; return config; }, afterRequest: async (req, res) => { console.log(`[Descope] ${req.method} ${req.path} → ${res.status}`); }, transformResponse: async (res) => res, }, }); ``` ## Passing Multiple Functions per Hook `beforeRequest` and `afterRequest` each accept either a single function or an array of functions. When an array is provided, functions run in order — for `beforeRequest`, each function's output is passed as input to the next. ```javascript hooks={{ beforeRequest: [ (config) => { config.headers = { ...config.headers, 'X-Tenant': 'acme' }; return config; }, (config) => { config.queryParams = { ...config.queryParams, version: '2' }; return config; }, ], afterRequest: [ (req, res) => console.log('hook 1:', res.status), (req, res) => console.log('hook 2:', res.status), ], }} ``` When `afterRequest` is an array, each function runs independently. If one function throws, the others still execute — errors are logged but do not propagate. # Google Drive (/authorization/rebac/examples/google-docs) Learn how to implement ReBAC for a collaborative document platform like Google Drive with hierarchical folder structures and granular file permissions. # Google Drive Example This example demonstrates how to implement Relationship-Based Access Control (ReBAC) for a collaborative document platform similar to Google Drive. The schema models hierarchical folder structures where files and folders can have different permission levels, and permissions inherit from parent folders. ## Schema ```yaml model AuthZ 1.0 type user type Group relation member: user type File relation owner: user | Group#member relation writer: user | Group#member relation commenter: user | Group#member relation reader: user | Group#member relation parent: Folder permission can_delete_file: owner permission can_access_historical_revisions: writer | parent.writer | can_delete_file permission can_modify_content: writer | parent.writer | can_delete_file permission can_modify_metadata: writer | parent.writer | can_delete_file permission can_add_comment: commenter | parent.commenter | can_modify_content permission can_read: reader | parent.reader | can_modify_content permission can_read_metadata: reader | parent.reader | can_modify_metadata type Folder relation owner: user | Group#member relation writer: user | Group#member relation commenter: user | Group#member relation reader: user | Group#member relation parent: Folder permission can_delete_folder: owner permission can_share_files_from_folder: writer | parent.writer | can_delete_folder permission can_remove_files_from_folder: writer | parent.writer | can_delete_folder permission can_add_files_to_folder: writer | parent.writer | can_delete_folder permission can_modify_metadata: writer | parent.writer | can_delete_folder permission can_read_items: reader | parent.reader | can_add_files_to_folder permission can_read_metadata: reader | parent.reader | can_modify_metadata ``` ## Schema Components ### Types - **`user`**: Individual users in the system - **`Group`**: Groups of users that can be assigned permissions collectively (e.g., "Engineering Team", "Marketing Department") - **`File`**: Individual files (e.g., documents, spreadsheets, presentations) - **`Folder`**: Collections of files and subfolders that form a hierarchical structure ### File Relations Files support four relationship types: - **`owner`**: Full control over the file, including deletion - **`writer`**: Can edit the file content and metadata - **`commenter`**: Can add comments but cannot edit content - **`reader`**: Can only view the file - **`parent`**: Links the file to its parent folder ### Folder Relations Folders support the same four relationship types as files: - **`owner`**: Full control over the folder, including deletion - **`writer`**: Can add, remove, and modify files in the folder - **`commenter`**: Can comment on files in the folder - **`reader`**: Can view files in the folder - **`parent`**: Links the folder to its parent folder (enables nested folder structures) ### Permission Hierarchy The schema implements a hierarchical permission model with inheritance: 1. **File Permissions**: - `can_delete_file`: Only owners can delete files - `can_access_historical_revisions`: Writers, parent folder writers, or owners can view version history - `can_modify_content`: Writers or parent folder writers can edit file content - `can_modify_metadata`: Writers or parent folder writers can change file properties - `can_add_comment`: Commenters, parent folder commenters, or those who can modify content - `can_read`: Readers, parent folder readers, or those who can modify content - `can_read_metadata`: Readers or parent folder readers can view file properties 2. **Folder Permissions**: - `can_delete_folder`: Only owners can delete folders - `can_share_files_from_folder`: Writers, parent folder writers, or owners can share files - `can_remove_files_from_folder`: Writers, parent folder writers, or owners can remove files - `can_add_files_to_folder`: Writers, parent folder writers, or owners can add files - `can_modify_metadata`: Writers or parent folder writers can change folder properties - `can_read_items`: Readers, parent folder readers, or those who can add files - `can_read_metadata`: Readers or parent folder readers can view folder properties ## Use Cases ### Personal Document Management **Scenario**: A user organizes personal documents in a folder hierarchy (Work, Personal, Projects). - **User (Owner)**: Has full control over all folders and files - **Shared with Family (Reader)**: Family members can view files in the "Personal" folder but cannot edit - **Collaborator (Writer)**: A colleague can edit files in the "Work" folder **Example Relations**: ```javascript // User owns the "Work" folder { resource: "work-folder", resourceType: "Folder", relation: "owner", target: "alice", targetType: "user" } // Document belongs to Work folder { resource: "project-plan.docx", resourceType: "File", relation: "parent", target: "work-folder", targetType: "Folder" } // Colleague is writer of Work folder (inherits to all files) { resource: "work-folder", resourceType: "Folder", relation: "writer", target: "bob", targetType: "user" } // Family member is reader of Personal folder { resource: "personal-folder", resourceType: "Folder", relation: "reader", target: "family-group", targetType: "Group" } ``` ### Team Collaboration **Scenario**: An engineering team collaborates on project documentation with nested folder structures. - **Team Lead (Owner)**: Owns the main project folder and all subfolders - **Engineering Team (Group - Writer)**: All team members can edit files in project folders - **Product Manager (Commenter)**: Can comment on files but cannot edit - **Stakeholders (Reader)**: Can view project files but cannot modify **Example Relations**: ```javascript // Team lead owns "Project Alpha" folder { resource: "project-alpha", resourceType: "Folder", relation: "owner", target: "team-lead", targetType: "user" } // Engineering team is writer of project folder { resource: "project-alpha", resourceType: "Folder", relation: "writer", target: "engineering-team", targetType: "Group" } // Design document in project folder { resource: "design-doc.md", resourceType: "File", relation: "parent", target: "project-alpha", targetType: "Folder" } // Product manager is commenter (can comment but not edit) { resource: "design-doc.md", resourceType: "File", relation: "commenter", target: "pm-123", targetType: "user" } ``` ### Enterprise Document Sharing **Scenario**: A company manages documents across departments with strict access controls. - **Department Head (Owner)**: Owns department folders - **Department Members (Group - Writer)**: Can manage files in their department - **Cross-Department Collaborators (Writer)**: Specific users can edit files across departments - **External Partners (Reader)**: Limited read-only access to specific folders **Example Relations**: ```javascript // Marketing department folder { resource: "marketing-dept", resourceType: "Folder", relation: "owner", target: "marketing-head", targetType: "user" } // Marketing team members can write { resource: "marketing-dept", resourceType: "Folder", relation: "writer", target: "marketing-team", targetType: "Group" } // Sales folder nested under Marketing { resource: "sales-materials", resourceType: "Folder", relation: "parent", target: "marketing-dept", targetType: "Folder" } // External partner has read-only access { resource: "sales-materials", resourceType: "Folder", relation: "reader", target: "partner-company", targetType: "Group" } ``` ## How It Works ### Permission Inheritance The schema uses hierarchical permission inheritance: 1. **Folder → File**: Permissions from the parent folder flow down to files - If a user is a `writer` of a folder, they can `can_modify_content` on all files in that folder - If a user is a `reader` of a folder, they can `can_read` all files in that folder 2. **Parent Folder → Child Folder**: Permissions cascade through nested folders - If a user is a `writer` of a parent folder, they can `can_add_files_to_folder` in child folders - Readers of parent folders can `can_read_items` in child folders 3. **Direct File Relations**: Files can have direct relations that complement folder permissions - A file can have its own `commenter` who can comment even if not a commenter of the parent folder - A file can have its own `owner` who has full control regardless of folder permissions ### Access Control Flow When checking if a user can perform an action on a file or folder: 1. **Check Direct Relations**: First, check if the user has a direct relation to the file/folder 2. **Check Parent Folder**: If no direct relation, check if the user has a relation to the parent folder 3. **Check Permissions**: Evaluate permission expressions that may combine multiple relations 4. **Inherit from Parent**: Parent folder permissions apply to all files and subfolders ### Example: Checking Access To check if user `alice` can read file `project-plan.docx`, you use the Descope SDK to check the `can_read` permission. The permission is defined as `can_read: reader | parent.reader | can_modify_content`, which means alice can read if she's a direct reader, a reader of the parent folder, or has permission to modify content. ```javascript // Check if alice can read the project-plan.docx file const relations = await descopeClient.management.fga.check([ { resource: 'project-plan.docx', resourceType: 'File', relation: 'can_read', target: 'alice', targetType: 'user', }, ]); // relations[0] will have an 'allowed' property indicating if access is granted if (relations[0].allowed) { console.log('Alice can read the file'); // Proceed with reading the file } else { console.log('Access denied'); } ``` ```python # Check if alice can read the project-plan.docx file relations = descope_client.mgmt.fga.check( [ { "resource": "project-plan.docx", "resourceType": "File", "relation": "can_read", "target": "alice", "targetType": "user", } ] ) # Check if access is granted if relations[0]["allowed"]: print("Alice can read the file") # Proceed with reading the file else: print("Access denied") ``` ```go // Check if alice can read the project-plan.docx file ctx := context.Background() relations, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "project-plan.docx", ResourceType: "File", Relation: "can_read", Target: "alice", TargetType: "user", }, }) if err != nil { // Handle error log.Fatal(err) } // Check if access is granted if relations[0].Allowed { fmt.Println("Alice can read the file") // Proceed with reading the file } else { fmt.Println("Access denied") } ``` ### Complete Implementation Example Here's a complete example showing how to set up relations and check access for files and folders: ```javascript // 1. Create folder and file relations await descopeClient.management.fga.createRelations([ // Alice owns the Work folder { resource: 'work-folder', resourceType: 'Folder', relation: 'owner', target: 'alice', targetType: 'user', }, // Project document belongs to Work folder { resource: 'project-plan.docx', resourceType: 'File', relation: 'parent', target: 'work-folder', targetType: 'Folder', }, // Bob is a writer of the Work folder (inherits to all files) { resource: 'work-folder', resourceType: 'Folder', relation: 'writer', target: 'bob', targetType: 'user', }, // Charlie is a direct reader of the file { resource: 'project-plan.docx', resourceType: 'File', relation: 'reader', target: 'charlie', targetType: 'user', }, ]); // 2. Check if alice can modify content (she can because she's owner of parent folder) const aliceAccess = await descopeClient.management.fga.check([ { resource: 'project-plan.docx', resourceType: 'File', relation: 'can_modify_content', target: 'alice', targetType: 'user', }, ]); console.log('Alice can modify:', aliceAccess[0].allowed); // true // 3. Check if bob can modify content (he can because he's writer of parent folder) const bobAccess = await descopeClient.management.fga.check([ { resource: 'project-plan.docx', resourceType: 'File', relation: 'can_modify_content', target: 'bob', targetType: 'user', }, ]); console.log('Bob can modify:', bobAccess[0].allowed); // true // 4. Check if charlie can read (he can because he's a direct reader) const charlieAccess = await descopeClient.management.fga.check([ { resource: 'project-plan.docx', resourceType: 'File', relation: 'can_read', target: 'charlie', targetType: 'user', }, ]); console.log('Charlie can read:', charlieAccess[0].allowed); // true // 5. Check if charlie can modify (he cannot - only reader) const charlieModifyAccess = await descopeClient.management.fga.check([ { resource: 'project-plan.docx', resourceType: 'File', relation: 'can_modify_content', target: 'charlie', targetType: 'user', }, ]); console.log('Charlie can modify:', charlieModifyAccess[0].allowed); // false ``` ```python # 1. Create folder and file relations descope_client.mgmt.fga.create_relations( [ # Alice owns the Work folder { "resource": "work-folder", "resourceType": "Folder", "relation": "owner", "target": "alice", "targetType": "user", }, # Project document belongs to Work folder { "resource": "project-plan.docx", "resourceType": "File", "relation": "parent", "target": "work-folder", "targetType": "Folder", }, # Bob is a writer of the Work folder (inherits to all files) { "resource": "work-folder", "resourceType": "Folder", "relation": "writer", "target": "bob", "targetType": "user", }, # Charlie is a direct reader of the file { "resource": "project-plan.docx", "resourceType": "File", "relation": "reader", "target": "charlie", "targetType": "user", }, ] ) # 2. Check if alice can modify content (she can because she's owner of parent folder) alice_access = descope_client.mgmt.fga.check( [ { "resource": "project-plan.docx", "resourceType": "File", "relation": "can_modify_content", "target": "alice", "targetType": "user", } ] ) print("Alice can modify:", alice_access[0]["allowed"]) # True # 3. Check if bob can modify content (he can because he's writer of parent folder) bob_access = descope_client.mgmt.fga.check( [ { "resource": "project-plan.docx", "resourceType": "File", "relation": "can_modify_content", "target": "bob", "targetType": "user", } ] ) print("Bob can modify:", bob_access[0]["allowed"]) # True # 4. Check if charlie can read (he can because he's a direct reader) charlie_access = descope_client.mgmt.fga.check( [ { "resource": "project-plan.docx", "resourceType": "File", "relation": "can_read", "target": "charlie", "targetType": "user", } ] ) print("Charlie can read:", charlie_access[0]["allowed"]) # True # 5. Check if charlie can modify (he cannot - only reader) charlie_modify_access = descope_client.mgmt.fga.check( [ { "resource": "project-plan.docx", "resourceType": "File", "relation": "can_modify_content", "target": "charlie", "targetType": "user", } ] ) print("Charlie can modify:", charlie_modify_access[0]["allowed"]) # False ``` ```go ctx := context.Background() // 1. Create folder and file relations err := descopeClient.Management.FGA().CreateRelations(ctx, []*descope.FGARelation{ // Alice owns the Work folder { Resource: "work-folder", ResourceType: "Folder", Relation: "owner", Target: "alice", TargetType: "user", }, // Project document belongs to Work folder { Resource: "project-plan.docx", ResourceType: "File", Relation: "parent", Target: "work-folder", TargetType: "Folder", }, // Bob is a writer of the Work folder (inherits to all files) { Resource: "work-folder", ResourceType: "Folder", Relation: "writer", Target: "bob", TargetType: "user", }, // Charlie is a direct reader of the file { Resource: "project-plan.docx", ResourceType: "File", Relation: "reader", Target: "charlie", TargetType: "user", }, }) // 2. Check if alice can modify content (she can because she's owner of parent folder) aliceAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "project-plan.docx", ResourceType: "File", Relation: "can_modify_content", Target: "alice", TargetType: "user", }, }) fmt.Println("Alice can modify:", aliceAccess[0].Allowed) // true // 3. Check if bob can modify content (he can because he's writer of parent folder) bobAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "project-plan.docx", ResourceType: "File", Relation: "can_modify_content", Target: "bob", TargetType: "user", }, }) fmt.Println("Bob can modify:", bobAccess[0].Allowed) // true // 4. Check if charlie can read (he can because he's a direct reader) charlieAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "project-plan.docx", ResourceType: "File", Relation: "can_read", Target: "charlie", TargetType: "user", }, }) fmt.Println("Charlie can read:", charlieAccess[0].Allowed) // true // 5. Check if charlie can modify (he cannot - only reader) charlieModifyAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "project-plan.docx", ResourceType: "File", Relation: "can_modify_content", Target: "charlie", TargetType: "user", }, }) fmt.Println("Charlie can modify:", charlieModifyAccess[0].Allowed) // false ``` ## Implementation Considerations - **File Organization**: When a new file is created, it must be linked to a folder via the `parent` relation - **Permission Propagation**: Changes to folder permissions automatically affect all files and subfolders - **Nested Folders**: Folders can have parent folders, creating deep hierarchies - **User Groups**: Use groups for team-based permissions (e.g., all "Engineering Team" members) - **Sharing Workflow**: Implement sharing by creating relations between users/groups and files/folders This schema provides a robust foundation for managing document access in collaborative platforms, from personal file management to enterprise document sharing systems. # IoT Device Management (/authorization/rebac/examples/iot-rebac) Learn how to implement ReBAC for IoT device management with hierarchical access control for device groups and individual devices. # IoT Device Management Example This example demonstrates how to implement Relationship-Based Access Control (ReBAC) for an Internet of Things (IoT) device management system. The schema models hierarchical access control where devices are organized into groups, and users have different permission levels based on their relationships with both device groups and individual devices. ## Schema ```yaml model AuthZ 1.0 type user type user_group relation member: user type device_group relation owner: user | user_group#member relation operator: user | user_group#member relation guest: user | user_group#member permission can_add_device: owner permission can_add_operator: owner permission can_add_guest: owner type device relation parent: device_group relation owner: user | user_group#member relation operator: user | user_group#member relation guest: user | user_group#member permission can_change_code: owner | parent.owner permission can_view: can_change_code | operator | parent.operator permission can_open: can_view | guest | parent.guest permission can_add_operator: owner | parent.owner permission can_add_guest: can_add_operator | operator | parent.operator ``` ## Schema Components ### Types - **`user`**: Individual users in the system - **`user_group`**: Groups of users that can be assigned permissions collectively - **`device_group`**: Collections of IoT devices (e.g., "Smart Home - Living Room", "Office Building - Floor 3") - **`device`**: Individual IoT devices (e.g., smart locks, thermostats, cameras) ### Device Group Relations Device groups support three relationship types: - **`owner`**: Full administrative control over the device group - **`operator`**: Can manage devices within the group but cannot modify group settings - **`guest`**: Limited access for temporary or restricted users ### Device Relations Individual devices inherit from their parent device group but can also have direct relationships: - **`parent`**: Links the device to its device group - **`owner`**: Direct owner of the device (can override group permissions) - **`operator`**: Can operate the device - **`guest`**: Limited access to the device ### Permission Hierarchy The schema implements a hierarchical permission model: 1. **Device Group Permissions**: - `can_add_device`: Only owners can add new devices to the group - `can_add_operator`: Only owners can grant operator access - `can_add_guest`: Only owners can grant guest access 2. **Device Permissions** (with inheritance): - `can_change_code`: Device owners or device group owners can change access codes - `can_view`: Owners, operators, or those with view access from the parent group - `can_open`: Anyone with view access, guests, or guests from the parent group - `can_add_operator`: Device owners or device group owners can add operators - `can_add_guest`: Operators or owners can add guests ## Use Cases ### Smart Home Management **Scenario**: A family manages their smart home with multiple device groups (Living Room, Bedroom, Garage) and various devices (smart locks, thermostats, cameras). - **Family Members (Owners)**: Have full control over all device groups and can add devices, change codes, and manage access - **House Sitter (Operator)**: Assigned as operator to the "Living Room" device group, can view and operate devices but cannot change codes or add new devices - **Guest (Guest)**: Temporary access to open the front door lock but cannot view other devices or change settings **Example Relations**: ```javascript // Family member owns the "Living Room" device group { resource: "living-room", resourceType: "device_group", relation: "owner", target: "alice", targetType: "user" } // House sitter is operator of the device group { resource: "living-room", resourceType: "device_group", relation: "operator", target: "sitter-123", targetType: "user" } // Front door lock inherits from parent group { resource: "front-door-lock", resourceType: "device", relation: "parent", target: "living-room", targetType: "device_group" } // Guest has temporary access to front door { resource: "front-door-lock", resourceType: "device", relation: "guest", target: "guest-456", targetType: "user" } ``` ### Commercial Building Management **Scenario**: An office building with multiple floors, each floor is a device group containing various IoT devices (access control, HVAC, lighting). - **Building Manager (Owner)**: Full control over all floors and devices - **Floor Manager (Operator)**: Can manage devices on their specific floor but cannot modify floor-level settings - **Maintenance Staff (Operator)**: Can operate devices across multiple floors for maintenance purposes - **Temporary Contractor (Guest)**: Limited access to specific devices for a project **Example Relations**: ```javascript // Building manager owns "Floor 3" device group { resource: "floor-3", resourceType: "device_group", relation: "owner", target: "manager-789", targetType: "user" } // Floor manager is operator of their floor { resource: "floor-3", resourceType: "device_group", relation: "operator", target: "floor-mgr-3", targetType: "user" } // HVAC system on floor 3 { resource: "hvac-floor-3", resourceType: "device", relation: "parent", target: "floor-3", targetType: "device_group" } // Maintenance staff can operate HVAC (inherits from parent operator) // Contractor has guest access to specific access control device { resource: "access-door-3a", resourceType: "device", relation: "guest", target: "contractor-xyz", targetType: "user" } ``` ### Multi-Tenant IoT Platform **Scenario**: An IoT platform serving multiple organizations, where each organization has device groups and devices. - **Organization Admin (Owner)**: Full control over their organization's device groups - **Department Head (Operator)**: Manages devices within their department's device group - **Team Member (Operator)**: Can operate devices in their team's device group - **External Partner (Guest)**: Temporary, limited access to specific devices for collaboration **Example Relations**: ```javascript // Organization owns their device group { resource: "org-acme-devices", resourceType: "device_group", relation: "owner", target: "org-acme", targetType: "user_group" } // Department is a device group within organization { resource: "dept-engineering", resourceType: "device_group", relation: "parent", target: "org-acme-devices", targetType: "device_group" } // Department head is operator of their department { resource: "dept-engineering", resourceType: "device_group", relation: "operator", target: "dept-head-eng", targetType: "user" } // Test device in engineering department { resource: "test-device-001", resourceType: "device", relation: "parent", target: "dept-engineering", targetType: "device_group" } ``` ## How It Works ### Permission Inheritance The schema uses hierarchical permission inheritance: 1. **Device Group → Device**: Permissions from the device group flow down to devices through the `parent` relation - If a user is an `owner` of a device group, they can `can_change_code` on all devices in that group - If a user is an `operator` of a device group, they can `can_view` all devices in that group 2. **Direct Device Relations**: Devices can have direct relations that override or complement group permissions - A device can have its own `owner` who has full control regardless of group permissions - A device can have its own `guest` who has access even if not a guest of the parent group ### Access Control Flow When checking if a user can perform an action on a device: 1. **Check Direct Relations**: First, check if the user has a direct relation to the device 2. **Check Parent Group**: If no direct relation, check if the user has a relation to the parent device group 3. **Check Permissions**: Evaluate permission expressions that may combine multiple relations 4. **Inherit from Group**: Group-level permissions apply to all devices in the group ### Example: Checking Access To check if user `alice` can open device `front-door-lock`, you use the Descope SDK to check the `can_open` permission. The permission is defined as `can_open: can_view | guest | parent.guest`, which means alice can open if she has `can_view` permission, is a direct guest of the device, or is a guest of the parent device group. ```javascript // Check if alice can open the front-door-lock device const relations = await descopeClient.management.fga.check([ { resource: 'front-door-lock', resourceType: 'device', relation: 'can_open', target: 'alice', targetType: 'user', }, ]); // relations[0] will have an 'allowed' property indicating if access is granted if (relations[0].allowed) { console.log('Alice can open the front door'); // Proceed with opening the device } else { console.log('Access denied'); } ``` ```python # Check if alice can open the front-door-lock device relations = descope_client.mgmt.fga.check( [ { "resource": "front-door-lock", "relationDefinition": "can_open", "namespace": "device", "target": "alice", } ] ) # Check if the relation exists (access is granted) if relations[0]["hasRelation"]: print("Alice can open the front door") # Proceed with opening the device else: print("Access denied") ``` ```go // Check if alice can open the front-door-lock device ctx := context.Background() relations, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "front-door-lock", ResourceType: "device", Relation: "can_open", Target: "alice", TargetType: "user", }, }) if err != nil { // Handle error log.Fatal(err) } // Check if access is granted if relations[0].Allowed { fmt.Println("Alice can open the front door") // Proceed with opening the device } else { fmt.Println("Access denied") } ``` ### Complete Implementation Example Here's a complete example showing how to set up relations and check access for an IoT device: ```javascript // 1. Create device group and device relations await descopeClient.management.fga.createRelations([ // Alice owns the living room device group { resource: 'living-room', resourceType: 'device_group', relation: 'owner', target: 'alice', targetType: 'user', }, // Front door lock belongs to living room group { resource: 'front-door-lock', resourceType: 'device', relation: 'parent', target: 'living-room', targetType: 'device_group', }, // Bob is a guest of the device { resource: 'front-door-lock', resourceType: 'device', relation: 'guest', target: 'bob', targetType: 'user', }, ]); // 2. Check if alice can open (she can because she's owner of parent group) const aliceAccess = await descopeClient.management.fga.check([ { resource: 'front-door-lock', resourceType: 'device', relation: 'can_open', target: 'alice', targetType: 'user', }, ]); console.log('Alice can open:', aliceAccess[0].allowed); // true // 3. Check if bob can open (he can because he's a direct guest) const bobAccess = await descopeClient.management.fga.check([ { resource: 'front-door-lock', resourceType: 'device', relation: 'can_open', target: 'bob', targetType: 'user', }, ]); console.log('Bob can open:', bobAccess[0].allowed); // true // 4. Check if charlie can open (he cannot - no relation) const charlieAccess = await descopeClient.management.fga.check([ { resource: 'front-door-lock', resourceType: 'device', relation: 'can_open', target: 'charlie', targetType: 'user', }, ]); console.log('Charlie can open:', charlieAccess[0].allowed); // false ``` ```python # 1. Create device group and device relations descope_client.mgmt.fga.create_relations( [ # Alice owns the living room device group { "resource": "living-room", "relationDefinition": "owner", "namespace": "device_group", "target": "alice", }, # Front door lock belongs to living room group { "resource": "front-door-lock", "relationDefinition": "parent", "namespace": "device", "target": "living-room", }, # Bob is a guest of the device { "resource": "front-door-lock", "relationDefinition": "guest", "namespace": "device", "target": "bob", }, ] ) # 2. Check if alice can open (she can because she's owner of parent group) alice_access = descope_client.mgmt.fga.check( [ { "resource": "front-door-lock", "relationDefinition": "can_open", "namespace": "device", "target": "alice", } ] ) print("Alice can open:", alice_access[0]["hasRelation"]) # True # 3. Check if bob can open (he can because he's a direct guest) bob_access = descope_client.mgmt.fga.check( [ { "resource": "front-door-lock", "relationDefinition": "can_open", "namespace": "device", "target": "bob", } ] ) print("Bob can open:", bob_access[0]["hasRelation"]) # True # 4. Check if charlie can open (he cannot - no relation) charlie_access = descope_client.mgmt.fga.check( [ { "resource": "front-door-lock", "relationDefinition": "can_open", "namespace": "device", "target": "charlie", } ] ) print("Charlie can open:", charlie_access[0]["hasRelation"]) # False ``` ```go ctx := context.Background() // 1. Create device group and device relations err := descopeClient.Management.FGA().CreateRelations(ctx, []*descope.FGARelation{ // Alice owns the living room device group { Resource: "living-room", ResourceType: "device_group", Relation: "owner", Target: "alice", TargetType: "user", }, // Front door lock belongs to living room group { Resource: "front-door-lock", ResourceType: "device", Relation: "parent", Target: "living-room", TargetType: "device_group", }, // Bob is a guest of the device { Resource: "front-door-lock", ResourceType: "device", Relation: "guest", Target: "bob", TargetType: "user", }, }) // 2. Check if alice can open (she can because she's owner of parent group) aliceAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "front-door-lock", ResourceType: "device", Relation: "can_open", Target: "alice", TargetType: "user", }, }) fmt.Println("Alice can open:", aliceAccess[0].Allowed) // true // 3. Check if bob can open (he can because he's a direct guest) bobAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "front-door-lock", ResourceType: "device", Relation: "can_open", Target: "bob", TargetType: "user", }, }) fmt.Println("Bob can open:", bobAccess[0].Allowed) // true // 4. Check if charlie can open (he cannot - no relation) charlieAccess, err := descopeClient.Management.FGA().Check(ctx, []*descope.FGARelation{ { Resource: "front-door-lock", ResourceType: "device", Relation: "can_open", Target: "charlie", TargetType: "user", }, }) fmt.Println("Charlie can open:", charlieAccess[0].Allowed) // false ``` ## Implementation Considerations - **Device Registration**: When a new device is added, it must be linked to a device group via the `parent` relation - **Permission Propagation**: Changes to device group permissions automatically affect all devices in the group - **User Groups**: Use user groups for role-based assignments (e.g., all "Maintenance Staff" users) This schema provides a robust foundation for managing IoT device access in scenarios ranging from smart homes to enterprise building management systems. # B2B RBAC Example (/authorization/role-based-access-control/examples/b2b-rbac) Discover how Descope's Roles and Permissions features support fine-grained access controls and streamline authentication development. # B2B RBAC Example Descope provides a structured Role-Based Access Control system, allowing for granular access control. Permissions are the foundation of this system, describing specific actions and functionality a user can take. Roles are collections of permissions that can be assigned to a user. For example, there could exist an “Admin” role with permissions to create, update, and delete other users. Creating, updating, assigning, grouping, and deleting roles and permissions is done through Descope's UI, SDKs, or APIs, with specific instructions on how to do so in our [Authorization Page](/authorization/role-based-access-control). ## Use Case Example Now, to better understand how to implement RBAC with Descope, let's dive into a use case example. Imagine running a SaaS company, providing a software-as-a-service platform to other businesses. This platform is designed to streamline e-commerce operations and amplify brand engagement. The platform has thee main features including: 1. **Digital Storefront**: Assists businesses in crafting an online presence, cataloging products, and processing sales. 2. **Customer Relations Manager (CRM)**: Designed to track customer interactions, purchases, and feedback. 3. **Brand Amplifier**: Aids in crafting, scheduling, and distributing promotional content across various digital channels. Recognizing the importance of granular access control, you integrate Descope into your solution. Without Role-Based Access Control (RBAC), employees across client businesses would have a free run of the platform, potentially complicating operations, especially in businesses where tasks are department-centric. ### Digital Storefront For the Digital Storefront module, permissions are [created](/authorization/role-based-access-control#creating-roles-and-permissions) such as: - `read-product-details` - `edit-product-pricing` - `process-online-orders` To manage these permissions, the role “Digital Store Supervisor” is [created](/authorization/role-based-access-control#creating-roles-and-permissions). ### CRM Transitioning to the CRM module, we see permissions like: - `view-customer-history` - `annotate-customer-feedback` - `generate-sales-report` This brings forth the role of “Customer Insights Analyst.” ### Brand Amplifier Lastly, the Brand Amplifier module encapsulates permissions like: - `draft-promotional-content` - `schedule-brand-campaign` - `analyze-engagement-metrics` Consequently, a role named “Brand Strategist” emerges. ## Assigning Roles to Users Now, consider a scenario: A business onboarded to your platform designates Dylan to oversee their e-commerce wing. Dylan is [provided](/authorization/role-based-access-control#configuring-users-roles) the Digital Store Supervisor and Customer Insights Analyst roles, granting him a spectrum of permissions from the two modules. However, since marketing is not his forte, he isn't assigned the Brand Strategist role, ensuring he remains focused on his domain. On the backend, when Dylan logs into your platform, Descope seamlessly authenticates and authorizes him, embedding the required permissions within the Access Token. Your software, by interpreting this token, ascertains which modules and features Dylan can access. ## Delegating Role Creation And Permission Assignment Using Descope's [Role Management Widget](/widgets/admins#role-management-widget), tenant administrators can create new tenant-level roles and assign them the relevant permissions. As part of this functionality, there is a need to manage the available permissions for each tenant, to allow granularity and avoid collisions between tenants. Meaning, some tenants require one set of permissions while others require a different set. And we want to be able to manage those different sets by creating delegation roles. This following example is about delegating permission assignments to new roles in "schools": 1. Create the role ![Role Delegation Create Role](/assets/role-delegation-create-role.webp) ![Role Delegation Role Set](/assets/role-delegation-role-set.webp) 2. Add the role to the tenant admin ![Role Delegation Role Add To Admin](/assets/role-delegation-assign-role-to-admin.webp) 3. The tenant admin can create a role with the delegated permissions ![Role Delegation Create Permissions](/assets/role-delegation-select-permissions.webp) ## Conclusion Thanks to Descope's efficient RBAC, your platform gains robustness without the need for internalizing authorization mechanics. As roles within client businesses evolve, permissions can be effortlessly realigned. Moreover, to cater to growing clientele and diversifying roles, Descope's [SDKs](/authorization/role-based-access-control/with-sdks) and [API](/api/management/permissions#overview) offers the potential of crafting a self-service portal. This in-platform feature could empower businesses to sculpt and manage their RBAC, further refining operational efficiency. # Checkboxes (/flows/screens/inputs/checkboxes) The article will cover how to implement checkboxes within your Descope flows. # Checkboxes Creating checkboxes within Descope flows is greatly important when you may display a terms and conditions checkbox or an "opt-in" checkbox for marketing messages. Descope allows you to create these checkboxes and track the answer on the user's details using custom attributes. This guide will cover how to implement a checkbox within your flow. ## Create the Custom Attribute The first step to configure checkboxes within your flow, you must create a boolean custom attribute. To do this, navigate to the [custom attributes](https://app.descope.com/users/attributes) screen within the Descope console. Click the `+ Create Attribute` button at the top right, and configure the attribute as you wish. For this example, we will create an email marketing opt-in checkbox. ![Create a custom attribute to use as a checkbox within your Descope flow](/assets/checkbox-custom-attribute.webp) ## Utilize Checkbox within Descope Flow ### Add a Checkbox to the Flow Now that you have created the custom attribute, navigate to the [flows](https://app.descope.com/flows) screen within the Descope console, select your desired flow, then select the screen within the flow you want to add the checkbox to. Once on the screen you'd like to add the checkbox to, scroll down on the left and find your custom attribute, then drag the attribute where you'd like it on the flow screen. Once you've added it to your flow screen, you can change the text of the checkbox by editing the label as shown below. ![Adding a checkbox within your Descope flow](/assets/checkbox-flow.webp) Additional thing to be noted, there is also a generic boolean input component on the screen builder that you could use to build into your screen. This component is similar to the one created above for a custom attribute. You can change its configuration including the label, its type (checkbox or a switch button), direction, size, fill container, mandatory, and whether it's selected by default. This component also allows defining the context key in which the value will be stored. If you turn on Selected by Default and also mark the input as mandatory, the checkbox or switch will already be checked when the screen loads. This means it passes the mandatory requirement automatically, without the user needing to click it. ![Adding generic boolean component within your Descope flow](/assets/checkbox-generic.webp) ### Update the User within the Flow After you've added your checkbox to the flow, you will need to add a `Update User/Properties` action to the flow by clicking the blue `+` sign at the top left and searching for the action. You can then configure the action to store the data captured from the user during the flow per the below configuration. Once the action is configured connect it to the flow after the screen with the checkbox. ![Configure update user action to update the user's checkbox selection within your Descope flow](/assets/checkbox-update-user-action.webp) ### User Details Post-Flow Once the user has ran through the flow, if they select the checkbox, it will be stored on the user as the flag being selected. In the user table, custom claims (if configured), and user details returned from the API or SDK it will be shown as true. ![User details after being updated using a checkbox within Descope Flows](/assets/checkbox-user-details.webp) # Date (/flows/screens/inputs/date-component) # Date This article will show how to use date type custom attribute and how to utilize the selection in a flow. ## Screen Builder Create a new screen inside a flow, and search for "Date": ![date example](/assets/date-example.webp) ## Options Overview * **Context Key**: Eventually, answers will be populated as a list inside a context key under `form.` * **Label**: The label will be presented to the customer right above the component. * **Placeholder**: A text that will be displayed inside the input component as a placeholder. Example: Date of Birth * **Format**: The available date format of calendar. The options are: `MM/DD/YYYY`, `DD/MM/YYYY`, `YYYY/MM/DD` * **Show Calendar**: A toggle to hide the calendar dropdown. * **Label Style**: Dropdown of Static or Floating Label. * **Direction**: Text Direction (left to right/right to left). * **Size**: The size of the text value inside the component. * **Fill container**: Stretch to fit the container. * **Mandatory (Behavior Tab)**: Determines if the user must select a value to continue to the next step. ## Usage The date input component allows users to either type in a date manually or select one using the calendar icon. This component is ideal for capturing values like a user's date of birth. Once a date is selected and a context key is set, Descopers can reference the user's input later in the flow using that key. The context key can be found under the Component Info setting of the component. ![context key in date field](/assets/date-component-info.webp) Using the calendar for the component is optional. ![date field in Descope screen](/assets/date-example-screen-1.webp) ![date component in Descope screen](/assets/date-example-screen.webp) # Month / Day (/flows/screens/inputs/date-monthday-component) This article will show how to use the month/day component and how to utilize the selection in a flow. # Month / Day Use this component when you need to collect a month and day from the user, such as a birthday, without asking for the year. ## Screen Builder Create a new screen (or edit an existing screen) inside a flow and search for "Month / Day" input component: ![Month / Day component](/assets/monthday-component.webp) ## Options Overview * **Label**: The label will be presented to the customer right above the component. * **Placeholder**: A text that will be displayed inside the input component as a placeholder. Example: Birthday * **Direction**: Text Direction (left to right/right to left). * **Format**: The format of the value. The options are: `MM/DD`, `DD/MM` * **Label Style**: You can select from these options: Static Label or Floating Label. * **Size**: The size of the text value inside the component. * **Show Picker**: A toggle to show/hide the picker. * **Fill container**: Stretch to fit the container. * **Context Key**: The answer will be populated as a value inside a context key under `form.` * **Mandatory (Behavior Tab)**: Determines if the user must select a value to continue to the next step. ## Usage The **month/day** component gives users two dropdowns: one for the month, one for the day. When a user selects a month, the day options change to match how many days that month has, so they can't submit an invalid date like February 30th. This component works well for birthday-based promotions or loyalty programs, where customers may hesitate to share a full date of birth. Set a context key on the component, and reference the user's input later in the flow using that key. The picker is optional. Users can also type a value directly. # Inputs (/flows/screens/inputs) Learn how to customize and utilize inputs within Descope screen. # Inputs Inputs are form components that can be pre-built fields like email or given name, or custom input fields for user attributes. This section covers the available configuration options for input components. When a field is marked mandatory, there is a generic "Missing Value" error message that can be translated within the Localization section of the Descope Console. ## Design ### Label The label is the text displayed above the input field. You can customize this text or leave it empty to hide the label. ### Placeholder The placeholder is the hint text displayed inside the input field. You can customize this text or leave it empty to have no placeholder. The placeholder disappears when the user starts typing. ### Allow Copying Value Enable the **Allow copying value** setting to display a copy icon next to the input field. Users can click this icon to copy the input value to their clipboard. This setting is commonly used with using Descope with [Native Flows](/mobile-sdk/native-vs-browser-flows#touch-interactions), where the mobile SDKs typically disable text selection. ### Obfuscated Text Input Enable the **Obfuscated** setting under **Behavior** to mask sensitive non-password information and protect it from "over-the-shoulder" viewing. You can use this component to collect SSN, Tax ID Numbers (EIN, TIN), Account Numbers, or any other Personal Identifiable Information (PII). ![Input design settings](/assets/obfuscated-text-input.webp) ## Behavior ### Validation Regex You can configure a regex pattern to validate the input data format. This is useful for ensuring specific formats, lengths, or character restrictions. For example, to allow only letters (no numbers or special characters), use the pattern `^[A-Za-z]+$`. This will reject any input that contains numbers or special characters. ### Pattern Validation Error Message You can configure a custom validation error message to display when the regex pattern validation fails. This message will appear instead of the default validation error. When this message is configured, you can translate the message within the Localization section of the Descope Console. ### Minimum Length The minimum number of characters required in the input field. ### Minimum Length Validation Error Message You can configure a custom validation error message to display when the minimum length requirement is not met. This message will appear instead of the default validation error. ### Maximum Length The maximum number of characters allowed in the input field. ### Mandatory When enabled, this field becomes required and must be filled out before the user can proceed to the next step in the flow. An asterisk will appear next to the field label to show it's required. ![Input behavior settings](/assets/input-behavior-settings.webp) The placeholder text for mandatory fields can be customized to meet the needs of each application. ![Mandatory input placeholder customization](/assets/mandatory-input-placeholder.webp) ## Errors On Blur vs On Submission ### On Submission By default, input validation uses **On Submission** mode, which means error messages only appear when the user attempts to submit the form. All validation errors will be displayed under their respective fields after submission. Errors disappear immediately when the user enters valid input, regardless of whether you use **On Blur** or **On Submission** mode. ![Errors on submission](/assets/input-error-presentation-onsubmit.webp) ### On Blur **On Blur** mode displays error messages immediately when a user clicks out of an input field if the input is invalid. This provides instant feedback and allows users to fix errors immediately. To enable **On Blur** mode, pass the `validateOnBlur` prop as `true` in your Descope Component. ```javascript import { Descope } from '@descope/react-sdk' const App = () => { return ( {...} console.log('Logged in!')} onError={(e) => console.log('Could not logged in')} validateOnBlur={true} /> ) } ``` ![Errors on blur](/assets/input-error-presentation-onblur.webp) # Login ID (/flows/screens/inputs/login-ids) This article will show how to use the email, phone and email/phone components in a Descope flow. # Login ID Login IDs are unique identifiers used for authentication. A user can have one or more login IDs associated to them. They can be email addresses, phone numbers, and/or custom identifiers (i.e. usernames). Proper management of these identifiers is essential for securing user accounts and facilitating smooth login processes. Since a user can have multiple login IDs, the values a user has for email, phone, etc. can differ from the login ID(s) they have. The purpose of the login ID is used specifically to identify the user with one of the authentication methods supported in Descope. Login IDs can also be used to search for users, instead of Descope User IDs, via our [SDKs](/management/user-management/sdks#load-existing-user-details) or [APIs](/api/management/users/search-users). This article will cover the design and behavior configurations of Descope Login ID fields within flow screens. Generally, these fields are added by dragging the respective `Email`, `Phone`, `Email or Phone` components under inputs from the left panel into a screen of your Descope flow. ## Email For adding email as Login ID field, you can drag the `email` component under the input field. This field has extensive design configurations that can be divided into categories: Content, Direction, and Style. Also the behavior tab contains validation related fields that can be used to configure this login field. ![email login id example](/assets/loginid-email-input.webp) ## Phone In your Descope Flow, phone numbers are accepted as inputs and will be properly formatted. For adding email as Login ID field, you can drag the `phone` component under the input field. Typically country code is included with Auto-Detection, but this can be turned off if desired. You can style this component as required by formatting or changing the Placeholder value or Label of the field. More details on the phone number component can be found [here](/flows/screens/inputs/phone-numbers). ![phone login id example](/assets/loginid-phone-input.webp) ## Email or Phone Descope supports a `Email or Phone` component that can accept both email or phone as login ID. This component will auto-detect if the entered value is an email or a phone and will change its preview to the relevant login ID type. The designer tab contains all the Content and Styling relevant to both an `email` or a `phone` field. The behavior tab has all the validation checks applicable for both of these fields. ![email or phone login id example](/assets/loginid-email-or-phone-input.webp) ## Custom Login ID If the Login ID does not fit standard formats like email or phone number, utilize the Custom Login ID Input Field. This is typically used to support usernames or external IDs that must be uniquely associated with a user. ![custom login id example](/assets/custom-loginid-input.webp) # Multi-Select (/flows/screens/inputs/multiselect-component) This article will show how to use the multi select component and how to utilize the selection in a flow. # Multi-Select This article will show how to use the multi-select component and what options a Descoper can change to customize the user's choice and experience. ## Screen Builder Create a new screen inside a flow, and search for "Multi Select" you will find the regular multi-select component and the dynamic multi-select. The regular multi-select input allows the user to choose one or more values from a predetermined list, while the dynamic multi-select allows the user to input their own values. ![multi select component in Descope screen](/assets/multi-select-component.webp) ## Options Overview * **Context Key**: Input results will be populated as a list inside a context key under `form.` * **Label**: The label that will be displayed right above the component. * **Placeholder**: The text that will be displayed inside the input component as a placeholder. * **Maximum Values**: The maximum number of values the component can accept. It can be left empty to not set a maximum values limit. * **Direction**: Text Direction (left to right or right to left). * **Fill container**: Toggle on whether the input stretches to fit the container. * **Mandatory**: Under behavior, determines if the user must select a value to continue to the next step. * **Size**: The size of the text value inside the component. ### Regular Multi-select options * **Values**: Available values from which to choose. Each value can be a static value, a context key, or dynamic value. If the dynamic value is a list of values (ex: `{{user.loginIds}}`), they will each show up as a separate value in the list of available values. * **Default Values**: Default values that will be submitted if the user doesn't choose any. They can also be empty values. * **Component**: `Dropdown` or `Labels` display for the input. ![dynamic values select component](/assets/dynamic-values-select-example.webp) ## Usage Once using the multi-select component and setting a context key, Descopers can use the user's selection in the flow as follows: ![multi select example](/assets/multi-select-example.webp) ![multi select component in Descope screen](/assets/multi-select-example-screen.webp) # One Time Code (/flows/screens/inputs/one-time-code) This article will show how to use one time code component and how to utilize the selection in a flow. # One Time Code This article will show how to use the one time code input component and how a Descoper can change the behavior to customize the user experience. ## One Time Code Input The **One Time Code** input is a standard input field that allows the user to enter a code. This is typically used for [OTP](/auth-methods/otp), [TOTP](/auth-methods/auth-apps), and [PIN codes](/auth-methods/passwords#pin-codes-as-passwords) as a user's password. ![One time code input](/assets/one-time-code.webp) ### Enabling the Auto-submit code You can enable **Auto-submit code** from the **Behavior** tab, which will automatically trigger the next action once the user enters the final digit of their code. This removes the need for a separate `Continue` or `Submit` button and creates a smoother login experience. ![One time code input behavior](/assets/one-time-code-behavior.webp) ### Using a Single-Box Field Instead If the segmented layout doesn't fit your design, put a [Password input](/flows/screens/inputs/passwords) on the verification screen instead. The OTP and TOTP verify actions accept the code from either input. # Password (/flows/screens/inputs/passwords) Learn how to customize and utilize the password component within Descope screen. # Password This article will show how to use the password components and what options a Descoper can change to customize their users' experience. ## Password Input The `login password` input is a standard input field that allows the user to enter a password. The `new password` input allows the user to enter a new password, and has options to confirm the password and preview the password policy. ### Using It for OTP or TOTP Codes Place a `login password` input on an OTP or TOTP verification screen to collect the code in a single field instead of the segmented [One Time Code input](/flows/screens/inputs/one-time-code). The verify action reads whichever of the two inputs is populated. ### Using It to Set a Password The `Sign Up / Password` and `Update Password` actions both read the password from either the `login password` or the `new password` input. Reach for `login password` when the confirmation field and policy previewer that come with `new password` don't suit the screen. When a screen has both inputs, `new password` wins. The input you pick only changes the screen. Your project's [password policy](/auth-methods/passwords/settings) and password history check apply to the stored password either way, so a password that fails the policy will fail the action even when no previewer is shown. ### Password Matching Error Message You can add a custom error message on the confirm password field of the `new password` input by turning on the toggle for `Password confirmation`. When you have this configured, the message will be displayed when both the passwords fields do not match. ![Configuring password validation error message](/assets/password-validation-field.webp) If the `Policy previewer` is turned on, the validation message will only be displayed after the new password satisfies the policy. If policy is not satisfied, the policy validation message is displayed first. ![Demo of custom password validation message within Descope flows](/assets/password-validation-message.webp) # Phone Numbers (/flows/screens/inputs/phone-numbers) Learn how to customize and utilize the phone number component within Descope screen. # Phone Numbers This article will cover the design and behavior configurations of phone numbers within screens, as well as the formatting standards of phone numbers. ## Using the Phone Number Input Component In your Descope Flow, phone numbers accepted as inputs will be properly formatted according to area and country code. You can toggle off **Number format** if you would not like to automatically format the phone number. Also, you can control which specific phone numbers are allowed to sign up, based on their respective country codes under **Allowed Countries**: ![Allowed countries](/assets/phone-number-allowed-countries.webp) ### Hiding Country Code Input When configuring your phone number component within your Descope Flow, you can hide the country code input and force the country code to a specific country. To do this, you can go to the screen within your flow, which has your phone number input field, select the phone field, and then switch the component to an input box. You can then select the chosen countries and change the country code to the one you want to enforce. ![Descope Screen configuration to force specific country phone number input](/assets/phone-number-force-country.webp) ## Formatting Phone Numbers If you're using our Management SDK or handling user management functions with our APIs, you will need to make sure that the phone numbers are formatted properly before being added to user identities in Descope. ### E.164 Standard Descope uses the E.164 standard when handling phone numbers. E.164 numbers are formatted as: [+][country code][subscriber number including area code] and can have a maximum of fifteen digits:
E.164 Formatted Phone Number Country Code Country Subscriber Number
+14151234567 1 US 4151234567
+4402012341234 44 UK 020 1234 1234
+551155256325 55 BR 1155256325
### Regex Expression for E.164 According to the official E.164 format, the number can be up to fifteen digits in length starting with a ‘+'. You can also exclude 0 as the first character since there are no country codes that start with 0. Here is a sample regular expression: ``` /^\+[1-9]\d{10,14}$/ ``` If you input an invalid number with `()` or `-` symbols, forget to add a `+` for the country code, or anything else like that, this Regex expression should return **false**. Otherwise if the phone number is valid, it should return **true**. You can use this when validating the phone number, before adding it to a user with Descope. If you're using Stripe to add phone number information from customers, please note that if your customers complete their transactions with Apple or Google Pay, the phone numbers will [not adhere to the E.164 format](https://stripe.com/docs/payments/checkout/phone-numbers#after-session). You will need to verify and modify the phone numbers to account for this manually, before adding them to a Descope User Identity. If you have any other questions about Descope or our phone number formatting, feel reach to reach out to [us](/support)! # Single-Select (/flows/screens/inputs/singleselect-component) This article will show how to use the single select component and how to utilize the selection in a flow. # Single-Select This article will show how to use the single-select component and what options a Descoper can change to customize the user's choice and experience. ## Screen Builder Create a new screen inside a flow, and search for "Single Select": ![single select component in Descope screen](/assets/single-select-component.webp) ## Options Overview * **Context Key**: Input results will be populated as a list inside a context key under `form.` * **Label**: The label that will be displayed right above the component. * **Placeholder**: The text that will be displayed inside the input component as a placeholder. * **Values**: Available values from which to choose. Each value can be a static value, a context key, or dynamic value. If the dynamic value is a list of values (ex: `{{user.loginIds}}`), they will each show up as a separate value in the list of available values. * **Default Value**: Default value that will be submitted if the user doesn't choose one. It can also be an empty value. * **Direction**: Text Direction (left to right or right to left). * **Component**: `Labels`, `Dropdown`, or `Radio Group` display for the input. * **Size**: The size of the text value inside the component. * **Fill container**: Toggle on whether the input stretches to fit the container. * **Require Match (Behavior Tab)**: When active, an unmatched input triggers an error. When inactive, the field ignores mismatches and returns to its previous state. * **Mandatory (Behavior Tab)**: Determines if the user must select a value to continue to the next step. ![dynamic values select component](/assets/dynamic-values-select-example.webp) ## Usage Once using the single-select component and setting a context key, Descopers can use the user's selection in the flow as follows: ![single select example](/assets/single-select-example.webp) ![single select component in Descope screen](/assets/single-select-example-screen.webp) # Switch Tenant (/flows/screens/inputs/tenantselect-component) Learn how to use the Switch Tenant component and set it up in your flow. # Switch Tenant The Switch Tenant component allows users to select one of their associated tenants during a flow. Once selected, tenant details can be used in conditions, logic branches, or displayed on screen. This guide explains how to use the component, access selected tenant data, and dynamically update tenant information within a flow. ## The Component Tenants will automatically be sorted in alphabetical order. Create a new screen inside a flow, and search for "Switch Tenant": ![Switch tenant component in Descope screen](/assets/switch-tenant-component.webp) This component allows a user to choose from the list of tenants they are associated with. Once selected, the tenant's details are available for use in the flow. ## Implementing Tenant Selection In Descope Flows ### Flow Overview The example below shows a flow where a user selects a tenant. The flow then evaluates the selected tenant’s name using a condition. - If the tenant name is "test", the flow ends. - Otherwise, the tenant's SSO configuration is shown. ![Descope flow using tenant name through switch tenant component](/assets/switch-tenant-flow.webp) ### A Closer Look At The Condition Here is a closer look at the condition that uses the tenant's name: ![A condition based on tenant details in Descope](/assets/switch-tenant-condition.webp) You can also use the tenant's domain(s) and any custom attributes you define. For creating custom attributes, you can read further [here](/management/tenant-management). ## Using The Selected Tenant Form Value After the user selects a tenant in the flow, it can be accessed from the form context as `{{form.userSelectedTenant}}`. ## JWT After Tenant Selection After the tenant has been selected, the JWT will have the `dct` (descope current tenant) claim marking which tenant is the actively selected tenant. If there's just one tenant associated with the user, the tenant does not have to be selected because the `dct` is automatically set to that one tenant. ```json { "amr": [ "oauth" ], "dct": "T2JEfswIPhl9I5YipLhSLNa6VfVV", "drn": "DS", "exp": 1708620768, "iat": 1708620588, "iss": "xxxxx", "rexp": "2024-03-21T16:49:48Z", "sub": "xxxxx", "tenants": { "xxx": {}, "T2JEfswIPhl9I5YipLhSLNa6VfVV": {}, "xxx": {}, } } ``` # Textarea (/flows/screens/inputs/textarea) This article will show how to use the textarea component in a Descope flow. # Textarea The **Textarea** component is a multi-line input designed for collecting longer, free-form responses than a standard single-line input. It is ideal for fields like comments, descriptions, mailing addresses, or anywhere users may need to enter multiple lines or larger amounts of text. Descopers can reference the user's input later in the flow using the textarea component's context key, which can be found under the Component Info setting of the component. ## Screen Builder Create a new screen inside a flow, and search for "Textarea": ![textarea example](/assets/textarea-component.webp) ## Options Overview **Design Tab:** * **Label**: The label will be presented to the customer right above the component. * **Placeholder**: A text that will be displayed inside the input component as a placeholder. * **Direction**: Text Direction (left to right / right to left). * **Size**: The size of the text value inside the component. Select from several default sizes. * **Fill container**: Stretch to fit the container. * **Context Key**: The value you enter here determines where the textarea's response is stored in the flow's context, under `form.`. You can reference this context key in later steps or components in the flow to use the value entered in this textarea. **Behavior Tab:** * **Validation Regex**: Configure a regex pattern to validate the input data format. For example, use `^[A-Za-z]+$` to allow only letters. * **Pattern Validation Error Message**: Set a custom error message to display when regex validation fails. This message can be translated within the Localization section of the Descope Console. * **Minimum Length**: Set the minimum number of characters required. * **Minimum Length Validation Error Message**: Set a custom message for when the minimum length requirement isn’t met. This can also be translated. * **Maximum Length**: Set the maximum number of characters allowed. * **Mandatory**: Determines if the user must input a value to continue to the next step. An asterisk will indicate the field is required. # Upload Document (/flows/screens/inputs/uploaddocument-component) This article will show how to use the upload document component to use the document in original format and in bytes. # Upload Document This article will show how to use the upload document component and how the uploaded document can be used in a flow using context keys. ## Screen Builder Create a new screen inside a flow, and search for "Upload document": ![upload document component in Descope screen](/assets/upload-document-add.webp) ## Options Overview * _Title_: The title will be presented to the customer at the top of the component. * _Description_: Text that will be displayed inside the component for further direction to the customer. * _Text_: Text that will be displayed in the button to upload the document. * _Color_: The theme that the button will use, can be changed on the Styles page. * _Direction_: Text Direction (left to right/right to left). * _Size_: The size of the whole component. * _Fill container_: Stretch to fit the container. * _Mandatory_: Determines if the user must upload a document to continue to the next step. ## Usage After adding a Upload Document component to a screen, Descopers can use the document uploaded in the flow as follows: ![using HTTP connector with document](/assets/upload-document-payload.webp) Descopers can reference the document using the `document` context key. This can be used to send the document to an endpoint in a generic HTTP connector POST request. # Multi-Tenant MCP Server (/mcp/examples/b2b-mcp-server) Build one tenant-agnostic MCP server whose per-tenant toolset is driven by the token's scopes, so adding a tenant needs no redeploy. # Multi-Tenant MCP Server For a high level overview of how MCP servers work with Descope, see the [MCP Server docs](/agentic-identity-hub/core-components/mcp-servers). The Python + [FastMCP](https://gofastmcp.com/integrations/descope) reference implementation is in the Descope AI repo: [examples/multi-tenant-mcp](https://github.com/descope/ai/tree/main/examples/multi-tenant-mcp). The [Calendar MCP Server](/mcp/examples/multi-tenant-calendar) example gates tools with SSO and scopes for users who belong to a single tenant. This example shows how to run **one MCP server** for many tenants. Each tenant gets a different toolset, but the server itself stays tenant-agnostic: you assign **one scope per tool**, use Descope [access policies](/agentic-identity-hub/policies) to decide which scopes each tenant grants at consent, and let the SDK expose only the tools whose scope is in the token. You don't maintain a tenant-to-tools map in code; onboarding a new tenant is a policy change in Descope, not a redeploy. ## How It Works The access token carries `dct` (active tenant) and the scopes that tenant's policy granted. The server never hardcodes tenants; it keys off scopes. 1. **Sign-in:** One consent flow, one MCP URL. The user picks a tenant (or gets one automatically if they only belong to one). The token is issued with `dct` and that tenant's scopes. 2. **Toolset:** Every tool is registered with a scope. The SDK filters `tools/list` and rejects calls when the scope is missing. 3. **Tenant data:** Tools that read or write tenant-specific data pull the tenant id from `dct`. Scopes control *what* the user can do; `dct` controls *where*. 4. **Multiple tenants:** A user can belong to several tenants but is signed into one at a time. Changing tenants means getting a new token; see [Step 5](#step-5-users-in-more-than-one-tenant). ### Example Flow Diagram ## Step 1: Configure the MCP Server 1. Create an MCP Server in Descope: - Go to [MCP Servers](https://app.descope.com/agentic-hub/mcp-servers) and create a server. - Enable **Dynamic Client Registration (DCR)** (or [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)) so clients can register automatically. - Copy the **Well-Known / discovery URL**; you'll pass it to the server as `DESCOPE_CONFIG_URL`. 2. **Enable the `dct` claim** in the server's [JWT template authorization claims](/management/token/jwt-templates#authorization-claims-configuration). 3. **Define one scope per tool** (for example `mcp:inventory.read`, `mcp:metrics.read`, `mcp:db.write`). Use [access policies](/agentic-identity-hub/policies) to grant scopes per tenant. A scope the policy doesn't allow won't appear in the token. New tenant = new policy, not new server code. ## Step 2: Add Tenant Selection to the Consent Flow Use **one MCP URL and one flow**, with no per-tenant connection URLs. Configure this in the [User Consent Flow](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) on your server's Dynamic Registration Template. It runs during `/authorize`. After authentication, add the [Tenant Select component](/flows/screens/inputs/tenantselect-component). It lists the user's tenants; the selection sets `dct` on the issued token. The tenant's access policy adds that tenant's scopes in the same pass. Skip the screen when the user has only one tenant. ![Example consent flow with a tenant-selection screen](/assets/mcp-tenant-selector-flow.webp) The screenshot is one possible layout. Swap in your own flow once you've built it. ## Step 3: Read the Active Tenant from the Token Tools that operate on tenant data read `dct` from the verified access token. Bearer middleware validates the token before your handler runs, so this is just a claim lookup, with no extra round trip. ```python def get_current_tenant() -> tuple[str | None, str | None]: """Read the active tenant from the verified token's `dct` claim. Returns (current_tenant, error_message). """ access = get_access_token() if not access: return None, "Authentication required: no bearer token on this request." dct = (access.claims or {}).get("dct") return (str(dct) if dct else None), None ``` ```ts function getCurrentTenant(token: string): string | null { const payload = token.split(".")[1]; if (!payload) return null; const claims = JSON.parse(Buffer.from(payload, "base64url").toString()); return claims.dct != null ? String(claims.dct) : null; } ``` ## Step 4: Drive the Toolset from Scopes Register a single tool catalog. Attach a scope to each tool; the SDK lists and executes only what the token allows. Both FastMCP and the Express SDK will automatically return a 401 error if the token lacks the required scope. ```python from fastmcp import FastMCP from fastmcp.server.auth import require_scopes mcp = FastMCP("multi-tenant") @mcp.tool(auth=require_scopes("mcp:inventory.read")) async def inventory_snapshot() -> str: """Inventory for the active tenant.""" tenant, err = get_current_tenant() if err: return err return f"[{tenant}] inventory snapshot OK." @mcp.tool(auth=require_scopes("mcp:metrics.read")) async def metrics_ping() -> str: """Metrics for the active tenant.""" tenant, err = get_current_tenant() if err: return err return f"[{tenant}] pong." ``` ```ts import "dotenv/config"; import express from "express"; import { descopeMcpAuthRouter, defineTool, DescopeMcpProvider } from "@descope/mcp-express"; const app = express(); app.use(express.json()); const provider = new DescopeMcpProvider({ serverUrl: process.env.SERVER_URL, descopeMcpServerWellKnownUrl: process.env.DESCOPE_MCP_SERVER_WELL_KNOWN_URL, }); const inventorySnapshot = defineTool({ name: "inventory_snapshot", description: "Inventory for the active tenant.", scopes: ["mcp:inventory.read"], handler: async (extra) => { const tenant = getCurrentTenant(extra.authInfo.token); return { content: [{ type: "text", text: `[${tenant}] inventory snapshot OK.` }] }; }, }); const metricsPing = defineTool({ name: "metrics_ping", description: "Metrics for the active tenant.", scopes: ["mcp:metrics.read"], handler: async (extra) => { const tenant = getCurrentTenant(extra.authInfo.token); return { content: [{ type: "text", text: `[${tenant}] pong.` }] }; }, }); app.use( descopeMcpAuthRouter((server) => { inventorySnapshot(server); metricsPing(server); }, provider), ); app.listen(3000, () => console.log("MCP endpoint: POST http://localhost:3000/mcp")); ``` ### Scope enforcement vs. `tools/list` **Enforcement** is the scope check at call time. Policies decide which scopes land in the token at consent; the SDK rejects any tool whose scope is missing. That holds even if the client cached an old `tools/list`. **Visibility** is what the client shows in `tools/list`. Filtering the list to the token's scopes keeps clients like Claude from offering tools the user can't run. FastMCP's `AuthMiddleware` does this from `require_scopes`. On Express, the shared `StreamableHTTPServerTransport` returns one global list; rely on the call-time scope check, and optionally filter the list handler yourself if you want the UI to match. Because scopes are tenant-specific via policy, filtering by scope gives you per-tenant toolsets without a tenant registry in the server. ## Step 5: Users in More Than One Tenant If a user belongs to one tenant, you're done after Step 4. This section is for users who need to move between tenants. They're signed into whichever tenant they picked at consent (`dct`). To work in another tenant they need a fresh token with a new `dct` and that tenant's scopes. Both values are signed into the token, so **the server can't flip the active tenant**; the client has to re-authenticate. With scope-filtered `tools/list`, the client usually won't call tools it can't use, so switching is something the user starts (reconnect / re-run OAuth), not something a failed tool call triggers. They pick the other tenant in consent and return with an updated token and tool list. The `401 WWW-Authenticate` challenge ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) tells clients to run OAuth. That works well for the initial sign-in and for scope step-up. It won't give you silent in-session tenant switching on its own; if the client only sees allowed tools, there's no failed call to prompt a switch. ### Optional: `switch_tenant` tool A `switch_tenant` tool gives agents and users a named way to ask for another tenant and see what's available. It doesn't replace the token (responses come back over `200`), but it can list choices and tell the user to reconnect. Include the `tenants` claim in the JWT template to read membership from the token: ```python @mcp.tool async def switch_tenant(tenant_id: str) -> str: """Name a target tenant and explain that switching requires re-auth.""" access = get_access_token() claims = access.claims or {} current = claims.get("dct") available = list((claims.get("tenants") or {}).keys()) if tenant_id == current: return f"Already signed into {tenant_id!r}." if tenant_id not in available: return f"{tenant_id!r} is not one of your tenants: {available}." return ( f"To switch from {current!r} to {tenant_id!r}, reconnect and select {tenant_id!r} " f"in the consent flow. Your client will re-run sign-in and come back with the new tenant." ) ``` ```ts import { z } from "zod"; function getTokenClaims(token: string): Record { const payload = token.split(".")[1]; if (!payload) return {}; return JSON.parse(Buffer.from(payload, "base64url").toString()); } const switchTenant = defineTool({ name: "switch_tenant", description: "Name a target tenant and explain that switching requires re-auth.", input: { tenantId: z.string().describe("Target tenant id") }, handler: async (args, extra) => { const claims = getTokenClaims(extra.authInfo.token); const current = claims.dct; const available = Object.keys(claims.tenants ?? {}); if (args.tenantId === current) { return { content: [{ type: "text", text: `Already signed into '${args.tenantId}'.` }] }; } if (!available.includes(args.tenantId)) { return { content: [{ type: "text", text: `'${args.tenantId}' is not one of your tenants: ${JSON.stringify(available)}`, }], }; } return { content: [{ type: "text", text: `To switch from '${current}' to '${args.tenantId}', reconnect and select '${args.tenantId}' in the consent flow. Your client will re-run sign-in and come back with the new tenant.`, }], }; }, }); ``` # Server Examples (/mcp/examples) Step-by-step examples for building secure MCP servers with Descope. # MCP Examples Learn how to build secure MCP servers with Descope through these step-by-step examples: ## Calendar MCP Server [Calendar Management MCP Server (w/ SSO)](/mcp/examples/multi-tenant-calendar) Build a secure calendar MCP server that: - Restricts access to only registered MCP clients (Claude Desktop and ChatGPT) - Requires SSO authentication via Okta or Azure AD for multiple tenants - Grants write access only to users with specific permissions - Allows read-only access to all other authenticated users ## Multi-Tenant MCP Server [Multi-Tenant MCP Server](/mcp/examples/b2b-mcp-server) Run **one MCP server** for users across different tenants: - One MCP URL and consent flow: users pick their tenant at sign-in via the [tenant selector](/flows/screens/inputs/tenantselect-component) - **One scope per tool**; [access policies](/agentic-identity-hub/policies) decide which tools each tenant's users get at consent - The server stays tenant-agnostic: no tenant-to-tools map in code, and onboarding a new organization is a policy change, not a redeploy - Tools read the active tenant from the token's `dct` claim; switching organizations means re-authenticating for a new token ## Additional Resources For more example MCP servers, please refer to our GitHub [repository](https://github.com/descope/ai/tree/main/examples). # Calendar MCP Server (w/ SSO) (/mcp/examples/multi-tenant-calendar) Learn how to build a secure calendar MCP server with SSO authentication, permission-based scopes, and multi-tenant support. # Calendar MCP Server with SSO and Tool-Level Scopes For a high level overview of how MCP servers work with Descope, please refer to the [MCP Server docs](/agentic-identity-hub/core-components/mcp-servers). This example demonstrates how to build a secure calendar MCP server that: - Restricts access to only registered MCP clients (Claude Desktop and ChatGPT) - Requires SSO authentication via Okta or Azure AD for "Tenant A" - Grants write access only to users with the `scheduler` permission in "Tenant A" - Allows read-only access to all other authenticated users ## Step 1: Configure the MCP Server 1. Create an MCP Server in Descope: - Navigate to [MCP Servers](https://app.descope.com/agentic-hub/mcp-servers) and create a new MCP Server - Enable **Dynamic Client Registration (DCR)** to allow Claude and ChatGPT clients to register automatically - Create a [Client Registration Flow](/agentic-identity-hub/core-components/mcp-servers/settings#client-registration-flow), configure **Approved Redirect URLs** to restrict registration: ``` claude://oauth-callback/* https://chat.openai.com/oauth/callback/* ``` This ensures only Claude Desktop and ChatGPT can register as OAuth clients. 2. **Define MCP Tool Scopes**: - `mcp:calendar.write` - Permission to create, update, or delete calendar events - `mcp:calendar.readonly` - Permission to read calendar events and query availability 3. **Configure the Consent Flow**: - Create a Descope Flow that handles [user consent](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) - Set this as the **Flow Hosting URL** in your Inbound App settings - The flow will display which scopes the MCP client is requesting ## Step 2: Set Up SSO for Tenant A 1. **Configure SSO Providers**: - For **Tenant A**: [Set up Okta or Azure AD as the SSO provider](/management/tenant-management/sso) 2. **Map SSO Groups to Descope Permissions**: - In your SSO provider (Okta or Azure AD), create a group called "Schedulers" - Configure Descope to [map this group to a `scheduler` permission](/auth-methods/sso/sso-setup-suite#attribute-mapping-user-and-group) - Users in the "Schedulers" group will automatically receive the `scheduler` permission in their JWT claims ## Step 3: Create Access Control Policies Create a [policy](/agentic-identity-hub/policies) that enforces the following rules: **Policy: Scheduler Write Access** - **Conditions**: - `user.tenantIds` contains `tenant-a` - `user.permissions` contains `scheduler` - **Allowed Scopes**: `mcp:calendar.write`, `mcp:calendar.readonly` ![Scheduler write access policy](/assets/scheduler-write-access-policy.webp) **Policy: Read-Only Access for All Others** - **Conditions**: - N/A - **Allowed Scopes**: `mcp:calendar.readonly` only ![Read-only access policy](/assets/read-only-access-policy.webp) ## User Flow Once you've configured everything, here's what will happen when a user connects their MCP client to your MCP server: 1. **Client Registration**: - Claude Desktop or ChatGPT will self-register with Descope either via [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) or via [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) (depending on the client implementation) - Descope validates the redirect URI against approved patterns 2. **User Authentication**: - The MCP client redirects the user to Descope's authorization endpoint - Descope detects the user's tenant (A or B), based on the [user's email domain](/auth-methods/sso#option-1-sso-domains), and redirects to the appropriate SSO provider - User authenticates via their SSO provider - SSO provider returns user and group attributes, that are mapped accordingly based on your [tenant configuration](/management/tenant-management/sso) to Descope [roles](/authorization/role-based-access-control) - In the consent flow, the user should also [connect](/agentic-identity-hub/core-components/connections/storing-connections) to Google using [Connections](/agentic-identity-hub/core-components/connections) so that Descope can store the necessary Google OAuth token for future MCP tool usage 3. **Consent and Token Issuance**: - User is presented with a consent screen showing requested scopes - User grants consent to use the MCP server - Descope evaluates access control policies: - If user has `scheduler` permission in Tenant A or B → token includes `mcp:calendar.write` and `mcp:calendar.readonly` - Otherwise → token includes only `mcp:calendar.readonly` - Access token is issued with appropriate scopes 4. **Tool Execution**: - MCP client makes a request to execute `create_calendar_event` tool, sending the Descope access token - MCP server validates the access token, checks the `aud` claim, and verifies the `mcp:calendar.write` scope - If scope is missing → MCP server returns `403 Forbidden - Insufficient Scope` - If scope is present → MCP server exchanges the Descope access token with the Google Calendar [Connection](/agentic-identity-hub/core-components/connections) to retrieve the Google access token with calendar write permissions - MCP server uses the Google access token to make an API request to Google Calendar API to schedule the meeting - Google Calendar API returns the created event details - MCP server returns the result to the MCP client ## Complete Flow Diagram Here is a mermaid diagram of how this is supposed to work, from start to finish: # Skyflow Integration (/mcp/integrations/skyflow) Learn how to integrate Skyflow with Descope MCP servers to securely exchange tokens and access PII data based on user roles. # Skyflow Integration with Descope Skyflow is a data privacy vault that enables secure storage and governed access to sensitive data such as PII (Personally Identifiable Information) and payment data. This guide demonstrates how to integrate a Descope-protected MCP server with [Skyflow Security Token Service (STS)](https://docs.skyflow.com/docs/governance/token-exchange/exchange#manage-your-sts-configurations) to securely exchange Descope access tokens for Skyflow access tokens. The resulting Skyflow token enforces [role-based data access](https://docs.skyflow.com/docs/governance/roles/conditional-data-access/overview) and redaction policies defined directly in Skyflow. The key concept is mapping [Descope roles](/authorization/role-based-access-control) to Skyflow roles using conditions, allowing identity-driven data governance without application-side access logic. ## How It Works The Skyflow integration with Descope uses OAuth 2.0 Token Exchange (RFC 8693): 1. **User Authentication:** Users authenticate to your MCP server using Descope and receive a Descope access token. 2. **Token Exchange:** The MCP server exchanges the Descope token for a Skyflow token using Skyflow STS. 3. **Role Mapping via Conditions:** Skyflow evaluates conditions against claims in the Descope token (for example, roles) and assigns Skyflow roles dynamically. 4. **Policy-Enforced Data Access:** The Skyflow token is used to query the vault. Field-level access, masking, redaction, and RLS are enforced automatically by Skyflow. The token exchange occurs server-side during authentication. Skyflow tokens are never exposed to the client or the LLM. ## Prerequisites Before starting, ensure you have: 1. [A Descope-protected MCP server](/agentic-identity-hub/core-components/mcp-servers) configured in Descope 2. **A Skyflow account** with: - A vault created - A service account created - API access to Skyflow Management APIs ### Getting Started with an Example To quickly get started with a working example, clone the [Descope AI examples repository](https://github.com/descope/ai/tree/main/examples/integrations/skyflow-descope-mcp-server) which includes a complete MCP server implementation that demonstrates: - Token exchange between Descope and Skyflow STS - Using the Skyflow [Get Records API](https://docs.skyflow.com/api/data/records/get-records) to retrieve PII data - Role-based data access and redaction The example includes a basic tool that allows you to request PII data from Skyflow vaults using the exchanged STS token, demonstrating the complete integration pattern. ### Environment Variables ```bash DESCOPE_PROJECT_ID SKYFLOW_VAULT_URL_IDENTIFIER SKYFLOW_VAULT_ID SKYFLOW_SERVICE_ACCOUNT_ID SKYFLOW_STS_URL=https://api.skyflowapis.com/v1/auth/sts/token ``` ## Setup Overview (Order Matters) 1. Set up your **MCP server** in Descope 2. Create a **Skyflow role** with data policies 3. Associate the role with a **Skyflow service account** 4. Create an **STS configuration** in Skyflow 5. Assign the Skyflow role using a **conditional role mapping** 6. Implement token exchange in your MCP server ## Step 1: Set Up Your MCP Server in Descope Your MCP server must be protected by Descope's [MCP server authentication](/agentic-identity-hub/core-components/mcp-servers). This will ensure that only authenticated users can access your MCP server and that every client will possess an OAuth compliant access token that can be exchanged for a Skyflow token. ## Step 2: Create a Skyflow Role with Policies In the Skyflow dashboard: 1. Navigate to **Vault → Access** 2. Click **+ Add Role** 3. Name the role (example: `marketing`) 4. Add a description ![Skyflow role](/assets/skyflow-role.webp) ### Example Policies These policies can also include **Row-Level Security (RLS)** conditions if needed. The following policies demonstrate field-level access and redaction: ```text ALLOW READ ON persons.date_of_birth WITH REDACTION = MASKED ALLOW READ ON persons.ssn WITH REDACTION = REDACTED ALLOW READ ON persons.name, persons.email_address, persons.state, persons.skyflow_id WITH REDACTION = PLAIN_TEXT ``` ![Skyflow policies](/assets/skyflow-policies.webp) ## Step 3: Associate the Role with a Service Account Under Service Accounts, you'll need to associate the newly created role with a pre-defined service account in Skyflow. If you don't already have a service account, create one from the main **Access** page by opening the **Service Accounts** tab and clicking **Add Service Account** button. ![Add service account](/assets/add-service-account.webp) This service account represents your MCP server when accessing Skyflow. ![Skyflow service account](/assets/skyflow-service-account.webp) ## Step 4: Create an STS Configuration in Skyflow Create an STS configuration so Skyflow can validate Descope tokens and extract claims. ```bash curl -X POST https://manage.skyflowapis.com/v1/sts/config \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "issuer": "__BaseURL__/v1/apps/customized/__ProjectID__", "name": "sts_config_descope", "description": "STS config for Descope", "publicKeyJWKURI": "__BaseURL__/__ProjectID__/.well-known/jwks.json", "contextClaims": [ "roles" ], "serviceAccountIDs": [ "" ], "accountID": "" }' ``` ### Notes on `contextClaims` * `"roles"` must match how roles appear in your Descope JWT * Roles may be nested under tenants depending on your Descope authorization model * Adjust `contextClaims` accordingly if using tenant-scoped roles ## Step 5: Assign the Role Using a Condition Now assign the Skyflow role to the service account **with a condition**. ```bash curl -X POST https://manage.skyflowapis.com/v1/roles/assign \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "ID": "", "members": [ { "ID": "", "type": "SERVICE_ACCOUNT", "status": "ACTIVE" } ], "condition": "\'Viewer\' in request.context.roles" }' ``` ### How This Works * `Viewer` is a Descope role included in the Descope token * During STS token exchange, Descope roles are placed into `request.context.roles` * Skyflow evaluates the condition * If true, the `marketing` role is applied dynamically This is how Descope roles map to Skyflow [roles](https://docs.skyflow.com/docs/governance/roles/overview). ## Step 6: Implement Token Exchange in the MCP Server During token validation, exchange the Descope token for a Skyflow token. ```ts curl --location 'https://manage.skyflowapis.com/v1/auth/sts/token' \ --header 'Content-Type: application/json' \ --data '{ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "", "subject_token_type": "urn:ietf:params:oauth:token-type:jwt", "service_account_id": "" }' ``` The returned Skyflow token: * Includes role-based permissions * Automatically enforces masking, redaction, and RLS * Is scoped to the service account and mapped roles ## Using the Skyflow Token in MCP Tools Once the token is exchanged and stored in the MCP authentication context, you can use it to make authenticated requests to Skyflow APIs. The Skyflow token is available in your tool's context and can be retrieved from `context.authInfo.extra.skyflowToken`. ### Example: Retrieving Records from Skyflow Here's a complete example of an MCP tool that uses the Skyflow token to retrieve records using the [Skyflow Get Records API](https://docs.skyflow.com/api/data/records/get-records): ```typescript server.tool( "get_skyflow_records", "Get records from a Skyflow vault table", { tableName: z.string().describe("Name of the table"), skyflow_ids: z.array(z.string()).optional().describe("Specific record IDs to retrieve"), redaction: z.enum(["DEFAULT", "MASKED", "PLAIN_TEXT", "REDACTED"]).optional(), limit: z.string().optional().describe("Number of records to return (max 25)"), }, async (args, context) => { // Retrieve the Skyflow token from authentication context const skyflowToken = context?.authInfo?.extra?.skyflowToken; if (!skyflowToken) { throw new Error("Skyflow token not available"); } // Build the Skyflow API URL const vaultUrl = `https://${SKYFLOW_VAULT_URL_IDENTIFIER}.vault.skyflowapis.com`; const url = new URL(`${vaultUrl}/v1/vaults/${SKYFLOW_VAULT_ID}/${args.tableName}`); // Add query parameters if (args.skyflow_ids?.length) { args.skyflow_ids.forEach(id => url.searchParams.append("skyflow_ids", id)); } if (args.redaction) { url.searchParams.append("redaction", args.redaction); } if (args.limit) { url.searchParams.append("limit", args.limit); } // Make authenticated request to Skyflow const response = await fetch(url.toString(), { method: "GET", headers: { Authorization: `Bearer ${skyflowToken}`, "Content-Type": "application/json", }, }); if (!response.ok) { throw new Error(`Skyflow API error: ${response.status} ${response.statusText}`); } const data = await response.json(); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } ); ``` ### Automatic Security Enforcement When you use the Skyflow token in API requests, Skyflow automatically enforces security policies based on the user's role: * **Field-level visibility**: Only fields permitted by the role are returned * **Redaction rules**: Sensitive data is automatically masked or redacted based on role permissions * **Table access restrictions**: Users can access only the tables that their assigned role permits. * **Row-level security (RLS)**: If RLS policies are configured, only matching rows are returned The redaction level you request in the API call (`DEFAULT`, `MASKED`, `PLAIN_TEXT`, `REDACTED`) will be honored up to the maximum level allowed by the user's role. For example, if a role only allows `MASKED` data, requesting `PLAIN_TEXT` will still return masked values. For complete API reference and additional query parameters, see the [Skyflow Get Records API documentation](https://docs.skyflow.com/api/data/records/get-records). # Express MCP SDK (/mcp/sdks/express) Add Descope authentication to your Express MCP server with drop-in middleware, per-tool scopes, and the required OAuth metadata endpoints. # Express MCP SDK The [Descope MCP Express SDK](https://github.com/descope/mcp-express) (`@descope/mcp-express`) provides drop-in Express middleware and helpers to add secure auth to your MCP server. It ships an authenticated `/mcp` endpoint, serves the required OAuth metadata endpoints, and lets you register tools with per-tool scopes. By default it runs as a **resource server**, in line with the current MCP authorization spec: Descope stays the [authorization server](/mcp#how-it-fits-together), and the SDK validates the tokens Descope issues. ## Prerequisites - A Descope project with an [MCP Server](https://app.descope.com/agentic-hub/mcp-servers) created in the Console; you will need its **Discovery URL** - An Express app - Node.js 18+ ## Installation ```bash npm install @descope/mcp-express ``` ## Quick Start ### Create your `.env` ```bash SERVER_URL=http://localhost:3000 # Recommended: MCP Server Discovery URL DESCOPE_MCP_SERVER_WELL_KNOWN_URL=__BaseURL__/v1/apps/agentic///.well-known/openid-configuration ``` With `DESCOPE_MCP_SERVER_WELL_KNOWN_URL` set, the SDK derives the issuer, project ID, and API base URL automatically. Existing setups can instead provide `DESCOPE_PROJECT_ID` (and optionally `DESCOPE_BASE_URL`), or set `DESCOPE_MCP_SERVER_ISSUER` directly. ### Wire up a minimal server ```ts import "dotenv/config"; import express from "express"; import { descopeMcpAuthRouter, defineTool, DescopeMcpProvider } from "@descope/mcp-express"; import { z } from "zod"; const app = express(); // Required: so /mcp can read JSON bodies app.use(express.json()); // Optional: explicit provider config (env vars work out of the box) const provider = new DescopeMcpProvider({ serverUrl: process.env.SERVER_URL, descopeMcpServerWellKnownUrl: process.env.DESCOPE_MCP_SERVER_WELL_KNOWN_URL, }); // Define an authenticated tool (requires 'openid') const hello = defineTool({ name: "hello", description: "Say hello to the authenticated user", input: { name: z.string().describe("Name to greet").optional(), }, scopes: ["openid"], handler: async (args, extra) => { const result = { message: `Hello ${args.name || "there"}!`, authenticatedUser: extra.authInfo.clientId, }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }, }); // Wire the MCP router and register your tools app.use( descopeMcpAuthRouter((server) => { hello(server); }, provider), ); app.listen(3000, () => { console.log("MCP endpoint: POST http://localhost:3000/mcp"); }); ``` - `/mcp` requires a valid Bearer token, and requests must send `Content-Type: application/json`. - The metadata endpoints are always on. The `/mcp` handler is wired only when you pass a tool-registration function to `descopeMcpAuthRouter`. - Self-provided issuer or discovery URLs must use one of two path shapes: `/v1/apps/agentic/__ProjectID__//...` (MCP Server) or `/v1/apps/__ProjectID__` (Inbound App). ## Creating Authenticated Tools There are two APIs with identical capabilities: both support Zod input, an optional output schema, annotations, and scopes. `defineTool` is a thin wrapper over `registerAuthenticatedTool` with a single-object config and cleaner TypeScript inference for `(args, extra)`; `registerAuthenticatedTool` mirrors the underlying MCP `registerTool` shape with explicit overloads. Pick the style you prefer. ```ts import { defineTool } from "@descope/mcp-express"; import { z } from "zod"; const getUser = defineTool({ name: "get_user", description: "Get user information", input: { userId: z.string().describe("The user ID to fetch") }, scopes: ["profile", "email"], handler: async (args, extra) => { return { content: [{ type: "text", text: JSON.stringify({ userId: args.userId, scopes: extra.authInfo.scopes }, null, 2) }], }; }, }); ``` ```ts import { registerAuthenticatedTool } from "@descope/mcp-express"; import { z } from "zod"; // With input const getUser = registerAuthenticatedTool( "get_user", { description: "Get user information", inputSchema: { userId: z.string().describe("The user ID to fetch") }, }, async (args, extra) => { return { content: [{ type: "text", text: JSON.stringify({ userId: args.userId }, null, 2) }] }; }, ["profile", "email"], ); // Without input const whoami = registerAuthenticatedTool( "whoami", { description: "Return authenticated identity info" }, async (extra) => { const result = { clientId: extra.authInfo.clientId, scopes: extra.authInfo.scopes || [], }; return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }, ["openid"], ); ``` ## Features An MCP 2025-06-18 compliant **resource server**, always enabled: - OAuth 2.0 Protected Resource Metadata (RFC 9728) - OAuth 2.0 Authorization Server Metadata (RFC 8414) - `/mcp` endpoint with bearer token authentication - Resource Indicator support (RFC 8707) All OAuth schemas use Zod for runtime validation. Token and revocation endpoints are provided by Descope. ## Verify Token Options Require scopes globally, or pin the expected resource indicator and audience: ```ts import { DescopeMcpProvider } from "@descope/mcp-express"; const provider = new DescopeMcpProvider({ verifyTokenOptions: { requiredScopes: ["get-schema", "run-query"], // resourceIndicator: "your-resource", // optional // audience: "your-audience", // optional (single value supported currently) }, }); ``` ## Migrating an Existing MCP Server Already have a plain MCP server using `server.registerTool`? 1. **Put your MCP behind Express**: add `app.use(express.json())` and wire `descopeMcpAuthRouter((server) => { /* register tools */ }, provider)`. The router exposes the metadata endpoints and wires `POST /mcp` with bearer auth. 2. **Wrap each existing tool**: convert `server.registerTool(...)` calls to `defineTool` or `registerAuthenticatedTool`, adding the `scopes` each tool requires. 3. **Remove custom wiring**: you no longer manage `StreamableHTTPServerTransport` or your own `/.well-known/*` endpoints; the router handles them. 4. **Update handler signatures**: with input: `(args, extra) => CallToolResult`; without input: `(extra) => CallToolResult`. 5. **Optional: call external APIs on the user's behalf**: use `extra.getOutboundToken(appId, scopes?)` to fetch a vaulted third-party token from [Connections](/agentic-identity-hub/core-components/connections). If you can't use the router, the lower-level pieces exist (`descopeMcpBearerAuth` and `createMcpServerHandler` on `POST /mcp`), but the router is the simplest and safest path. ## Legacy Authorization Server Mode By default the SDK runs as a resource server only. That is the recommended path, aligned with the MCP 2025-06-18 spec. Legacy Authorization Server mode exposes additional endpoints (`/authorize`, `/register`) for backwards compatibility and testing. Consider the added surface area before enabling it. It requires `DESCOPE_PROJECT_ID`, `SERVER_URL`, and a `DESCOPE_MANAGEMENT_KEY`. Enable it through `authorizationServerOptions`, and configure `/register` through `dynamicClientRegistrationOptions`: ```ts import { DescopeMcpProvider } from "@descope/mcp-express"; const provider = new DescopeMcpProvider({ projectId: process.env.DESCOPE_PROJECT_ID, serverUrl: process.env.SERVER_URL, authorizationServerOptions: { isDisabled: false, // enable Authorization Server mode enableAuthorizeEndpoint: true, // expose /authorize enableDynamicClientRegistration: true, // optionally expose /register }, // Only needed if you enable dynamic client registration dynamicClientRegistrationOptions: { authPageUrl: `__BaseURL__/login/${process.env.DESCOPE_PROJECT_ID}?flow=consent`, permissionScopes: [ { name: "get-schema", description: "Allow getting the SQL schema" }, { name: "run-query", description: "Allow executing a SQL query", required: false }, ], clientType: "public", // or "confidential"; Descope then issues a client_secret at registration }, }); ``` With `clientType: "confidential"`, the `/register` response includes a `client_secret` (per RFC 7591). It is only returned at registration time, so store it securely. ## Related - [MCP Express SDK on GitHub](https://github.com/descope/mcp-express): source and full reference - [Python MCP SDK](/mcp/sdks/python): the same resource-server role for Python and FastMCP - [MCP Servers](/agentic-identity-hub/core-components/mcp-servers): configure the server object, scopes, and registration methods - [MCP overview](/mcp): how the authorization model fits together # MCP SDKs (/mcp/sdks) Server-side SDKs that add Descope authentication and authorization to your MCP server, in Express and Python. # MCP SDKs Descope ships server-side SDKs that protect your MCP server: they validate Descope-issued tokens (signature, `aud`, `scope`), expose the required OAuth metadata endpoints, and let you require scopes per tool. } title="Express SDK" href="/mcp/sdks/express" description="Drop-in Express middleware and helpers. Ship an authenticated /mcp endpoint and register tools with per-tool scopes." /> } title="Python SDK" href="/mcp/sdks/python" description="Token validation with scope and audience enforcement, Connection token retrieval, and FastMCP integration." /> Both SDKs implement the resource-server side of the [MCP authorization model](/mcp#how-it-fits-together): Descope stays the authorization server, and your MCP server validates what it issues. For configuring the server object itself, see [MCP Servers](/agentic-identity-hub/core-components/mcp-servers). ## Framework Integrations If you build on a framework with a built-in Descope integration, you can wire up **basic auth with scopes** without our SDKs: - **[FastMCP](https://gofastmcp.com/integrations/descope)** (Python): `DescopeProvider` discovers your MCP server's OpenID configuration from its Well-Known URL, validates Descope-issued tokens, and supports DCR. - **[xmcp](https://xmcp.dev/docs/integrations/descope)** (TypeScript): the `@xmcp-dev/descope` plugin adds OAuth 2.1 auth middleware, with session data via `getSession()` and Connection token fetching. These integrations cover basic authentication and don't include everything the Descope MCP SDKs provide. Use them on their own if token validation and scopes are all you need, or in conjunction with the SDKs. For example, use FastMCP for the transport and auth wiring with the [Python SDK](/mcp/sdks/python) for Connection token retrieval and scope enforcement. # Python MCP SDK (/mcp/sdks/python) Learn how to use the Descope Python MCP SDK to integrate authentication, authorization, and connection token retrieval with your MCP servers. # Python MCP SDK The Descope Python MCP SDK (`descope-mcp`) provides a simple way to integrate Descope authentication and authorization with MCP (Model Context Protocol) servers built with Python. ## Overview The SDK provides: - **Token validation** with scope and audience enforcement - **[Connection token retrieval](/agentic-identity-hub/core-components/connections/fetching-connection-tokens)** using MCP server access tokens (default) or management keys - **Scope validation** following the MCP spec for insufficient scope errors - **Integration** with [FastMCP](https://gofastmcp.com/integrations/descope) and the official MCP SDK ### Compatibility - **Python**: 3.8+ - **MCP SDK**: 1.0.0+ - **Descope SDK**: 1.0.0+ - **FastMCP**: 2.0+ The SDK can also be imported alongside the official [Descope Python SDK](https://github.com/descope/python-sdk) without conflicts. ## Prerequisites Before using the SDK, you need to: 1. **Set up your Descope MCP Server** in the [Descope Console](https://app.descope.com/agentic-hub/mcp-servers) 2. **Get your `.well-known` URL** from the MCP server settings ### Setting Up Your MCP Server In Descope, create or configure your MCP server in **Agentic Identity Hub → MCP Servers → Settings**. The settings include: - **MCP Server URL**: If set, this becomes the `aud` claim in access tokens and should match the `mcp_server_url` you pass to this SDK - **Discovery endpoints**: Use the server's discovery settings to copy the MCP Server's **`.well-known` OpenID configuration URL** and pass it as `well_known_url` For detailed instructions, see [MCP Server Settings](/agentic-identity-hub/core-components/mcp-servers/settings). ## Installation To install the SDK, run the following command: ```bash pip install descope-mcp ``` Then to initialize the SDK, use the following code: ```python from descope_mcp import DescopeMCP # Initialize SDK with your MCP server configuration DescopeMCP( well_known_url="__BaseURL__/v1/apps/agentic///.well-known/openid-configuration", mcp_server_url="https://your-mcp-server.com" # Optional: for audience validation ) ``` ## Key Functions The SDK provides the following main functions for working with MCP servers: - **`validate_token()`** - Validates MCP server access tokens and returns token claims including user ID and scopes - **`validate_token_and_get_user_id()`** - Convenience function that validates a token and returns just the user ID - **`require_scopes()`** - Validates that a token contains required scopes, raising `InsufficientScopeError` if scopes are missing (MCP spec-compliant) - **`get_connection_token()`** - Retrieves OAuth tokens for third-party services stored in Descope Connections, with support for specific scopes or latest token retrieval Each function is detailed in the sections below. ## Token Validation Validate MCP server access tokens with signature verification, expiration checking, and audience validation: ```python from descope_mcp import validate_token, validate_token_and_get_user_id # Get full validation result result = validate_token(access_token) user_id = result.get("sub") or result.get("userId") scopes = result.get("scopes", []) # Or get user ID directly user_id = validate_token_and_get_user_id(access_token) ``` The validation checks: - Token signature against JWKs from the `.well-known` endpoint - Token expiration - Audience (`aud`) claim matches `mcp_server_url` (if provided) - Token issuer matches the configured Descope project ## Scope Validation The SDK provides scope validation that follows the MCP spec's [Runtime Insufficient Scope Errors](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors). ### Using require_scopes() ```python from descope_mcp import validate_token, require_scopes, InsufficientScopeError @mcp.tool() def my_tool(mcp_access_token: str) -> str: try: token_result = validate_token(mcp_access_token) require_scopes(token_result, ["read", "write"]) return "Success" except InsufficientScopeError as e: # Returns MCP spec-compliant error response return e.to_json() ``` ### InsufficientScopeError When `require_scopes()` detects missing scopes, it raises an `InsufficientScopeError` that follows the [MCP spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#runtime-insufficient-scope-errors): ```python try: require_scopes(token_result, ["calendar.read"]) except InsufficientScopeError as e: missing = e.missing_scopes # ["calendar.read"] combined = e.combined_scopes # ["read", "write", "calendar.read"] scope_param = e.scope_parameter # "read write calendar.read" # Get MCP spec-compliant JSON response error_json = e.to_json() ``` The error includes: - `error`: "insufficient_scope" - `scope`: Space-separated list of all scopes (existing + required) - uses recommended approach - `error_description`: Human-readable description - `missing_scopes`: List of missing scopes - `token_scopes`: List of scopes in the token - `required_scopes`: List of required scopes ## Connection Tokens Retrieve OAuth tokens for third-party services stored in Descope [Connections](/agentic-identity-hub/core-components/connections): ```python from descope_mcp import get_connection_token # Get token with specific scopes (uses access token by default) token = get_connection_token( user_id="user-123", app_id="google-calendar", scopes=["https://www.googleapis.com/auth/calendar.readonly"], access_token=mcp_access_token # Enables policy enforcement ) ``` ```python from descope_mcp import get_connection_token # Get latest token (any scopes) token = get_connection_token( user_id="user-123", app_id="google-calendar", access_token=mcp_access_token ) ``` The SDK uses MCP server access tokens by default for policy enforcement. Management keys can be used as a fallback for tenant-level tokens or when access tokens aren't available. When using `access_token`, Descope enforces your [access control policies](/agentic-identity-hub/policies) before returning connection tokens. This ensures only authorized clients can retrieve tokens for specific connections. ## Examples You can find examples of MCP servers using our SDK in our [AI repository](https://github.com/descope/ai/tree/main/examples) in GitHub. Here's a complete example of the SDK in action, showing token validation, scope checking, and connection token retrieval: ```python from mcp.server import FastMCP from descope_mcp import DescopeMCP, validate_token, require_scopes, get_connection_token, InsufficientScopeError import requests # Initialize SDK DescopeMCP( well_known_url="__BaseURL__/v1/apps/agentic///.well-known/openid-configuration", mcp_server_url="https://your-mcp-server.com" ) mcp = FastMCP("calendar-server") @mcp.tool() async def get_calendar_events( max_results: int = 5, mcp_access_token: str = None ) -> str: """ Get upcoming calendar events for the authenticated user. Requires 'calendar.read' scope. """ if not mcp_access_token: return {"error": "Authentication required"} try: # Validate token token_result = validate_token(mcp_access_token) # Check required scopes require_scopes(token_result, ["calendar.read"]) # Get user ID # Get Google Calendar connection token google_token = get_connection_token( user_id=token_result.get("sub") or token_result.get("userId"), app_id="google-calendar", scopes=["https://www.googleapis.com/auth/calendar.readonly"], access_token=mcp_access_token ) # Use token to call Google Calendar API from datetime import datetime now = datetime.utcnow().isoformat() + "Z" url = f"https://www.googleapis.com/calendar/v3/calendars/primary/events" response = requests.get( url, headers={"Authorization": f"Bearer {google_token}"}, params={ "timeMin": now, "maxResults": max_results, "orderBy": "startTime", "singleEvents": True } ) if response.status_code != 200: return {"error": f"Google API error: {response.status_code}"} return response.json() except InsufficientScopeError as e: # Return MCP spec-compliant error return e.to_json() except Exception as e: return {"error": str(e)} ``` # Golf.dev (/mcp/gateways/golf-dev) Use Descope as the identity and authorization layer for MCP servers, with Golf.dev Gateway enforcing access policies at runtime. # Descope + Golf.dev Descope manages identity, authentication, and authorization for your MCP environment, with our [Agentic Identity Hub](/agentic-identity-hub). It defines who can access which servers, tools, and resources. [Golf.dev](https://golf.dev) is an MCP firewall and gateway platform that acts as a runtime enforcement layer for those Descope policies. It sits in front of MCP servers and applies Descope-issued identity, roles, and scopes to every request. This setup is useful when you need to: - Enforce RBAC and tool-level permissions - Secure MCP servers you do not control (e.g. SaaS tools) - Apply consistent identity and access rules across all agent traffic ![Descope + Golf.dev](/assets/descope-golf-dev.webp) ## How They Work Together **Descope** defines identity and access control: authentication (OAuth 2.1, SSO, MFA), authorization (RBAC, scopes, roles), and centralized policy management. **Golf.dev Gateway** enforces those policies at runtime. It's an MCP-aware proxy that validates Descope tokens, reads roles and scopes, and applies access policies before requests reach MCP servers. Because Golf.dev understands MCP semantics (methods, tools, resources), it enforces Descope policies at the protocol level, not just HTTP. This lets you: - Enforce least-privilege access to tools - Control agent access to third-party MCP servers - Apply consistent identity rules across all systems - Audit every request with a single identity model ## How the Integration Works 1. An agent or MCP client will authenticate with Descope and receive an access token. 2. Requests are sent through the Golf.dev [Gateway](https://docs.golf.dev/gateway/overview/golf-gateway). 3. Golf.dev validates the token using Descope's issuer and JWKs endpoint. 4. Roles and scopes from the token are mapped to Gateway policies. 5. Requests are allowed or denied before reaching the connected MCP server. ## Required Configuration Values When configuring Descope as an identity provider in Golf.dev Gateway, use the following values: ### Required - **Project ID** - Your Descope Project ID - **Issuer URL** - `__BaseURL__/v1/apps/customized/__ProjectID__` - **API Identifiers** - Comma-separated list of valid audience identifiers that Golf.dev will accept from your Descope token ### Optional - **JWKs Endpoint** - Auto-generated from your Project ID as `__BaseURL__/__ProjectID__/.well-known/jwks.json`, but can be overridden if needed - **UserInfo Endpoint** - Optional endpoint if you want Golf.dev to query user information from the OAuth endpoint - **Request Timeout** - Default is 10 seconds. Valid range is 1-60 seconds - **Management Key** - Required for Golf.dev to retrieve user information, including role information for role synchronization - **Tenant ID** - Required if you want to get tenant role information for tenant-scoped role synchronization ![Golf.dev Configuration](/assets/golf-dev-configuration.webp) ### Service Account (M2M) for In-House Servers If you have MCP servers that you've built in-house and are static clients (that don't dynamically register), you can use a Service Account (Machine-to-Machine) configuration: - **Client ID** - Service account client ID from Descope - **Client Secret** - Service account client secret from Descope - **Scopes** - OAuth scopes required for the service account This allows Golf.dev Gateway to authenticate to your in-house MCP servers using the service account credentials. ![Golf.dev Service Account Configuration](/assets/golf-dev-service-account-configuration.webp) ## Role Synchronization When role sync is enabled: - Descope roles are mapped to Golf.dev Gateway [groups](https://docs.golf.dev/gateway/guides/security/setup-server-rbac#how-it-works) - Access is evaluated using the assoiated [Descope role](/authorization/role-based-access-control#project-level-roles) - [Tenant-scoped roles](/authorization/role-based-access-control#tenant-level-roles) are supported ![Golf.dev Role Sync](/assets/golf-dev-role-sync.webp) An example of role sync is a user with the role `finance-analyst` can call the `getInvoices` tool but not the `writeLedger` tool. If you're building your own MCP server, you can configure the authorization server for it with [MCP Servers](/agentic-identity-hub/core-components/mcp-servers). Once you've connected your MCP server to Descope and want to set up RBAC for your MCP server, you can do the following in Golf.dev: - [Set up Server RBAC](https://docs.golf.dev/gateway/guides/security/setup-server-rbac) - [Set up Capability RBAC](https://docs.golf.dev/gateway/guides/security/setup-capability-rbac) # Gateways (/mcp/gateways) Learn how to model MCP servers, connections, and policies in Descope when building an MCP gateway for multiple customers or tenants. # MCP Gateways When you build an **MCP gateway** (a single entrypoint that fronts many MCP servers and/or tenants), Descope gives you a few core building blocks: - **[Resources](/resources)** define *what* each server protects: **API** (scopes ↔ RBAC) or **MCP Server** (scopes ↔ Connections), plus audience, flows, and DCR/CIMD in the Agentic Identity Hub. - **Connections per tenant** define *how* each tenant MCP server is wired to downstream systems (credentials, endpoints, scope mapping). - **Policies** define *who* can call which MCP server, for which tenants, and with what capabilities (scopes). With these three building blocks, you can design a flexible, multi-tenant MCP gateway on top of Descope that: - Scales to many tenants and MCP servers. - Keeps credentials and configuration isolated per tenant. - Enforces clear, auditable policies per server and agent set. ## Where Descope Fits in the MCP Gateway When an agent invokes a tool, the request typically will go through the gateway. If the agent is not allowed to access that MCP server or scope, the gateway does not issue the connection or resource token and the agent has no access to that tool. If the policy allows, the gateway resolves the tenant-specific MCP server and its Connections. The two scenarios below show how this works for different gateway architectures. ### Scenario 1: Internal MCP Servers + Third-Party MCP Servers In this scenario you are building an MCP gateway that connects **two types** of MCP servers: - **MCP servers you build internally** (e.g. your own MCP server). - **Third-party MCP servers you do not build** (e.g. [Notion MCP](https://developers.notion.com/guides/mcp/mcp), [Linear MCP](https://linear.app/docs/mcp), or other remote community MCP servers). **Descope Connections** manage the tokens for the third-party MCP servers. You can connect as many third-party MCP servers as you need using Connections, and scope them at the **tenant level** so each customer has their own credentials and scope mapping. **MCP Server A** (internal) in the diagram below represents an MCP server that you build yourself. It shows the different ways you can use Descope within that server: - **Resource tokens** (e.g. for a [Descope-protected Resource](/resources)) - **Connection tokens** (e.g. for Google Calendar or other OAuth-backed APIs) and **API key-based resources** The gateway enforces **policy** when the agent requests a connection or resource token; if the agent is not permitted for that MCP server or scope, the request is denied and the agent has no access to that tool. The diagram below illustrates this mix of internal and third-party MCP servers: |tool call request| Policy Policy -->|allowed| STS1 Policy -->|denied| Denied STS1 -->|MCP Server A access token + tool request| ServerA STS1 -->|Connection token + tool request| ServerB STS2 -->|Connection token| ExternalService1 STS2 -->|API A access token| ExternalService2 STS2-->|Connection token| ExternalService3 classDef descope fill:#4F46E5,stroke:#3730A3,color:#fff class Policy,STS1,STS2,ExternalService2 descope `} /> ### Scenario 2: One MCP Server Per Customer If you're spinning up a different MCP server for every customer, you can use the [MCP Server Management](/agentic-identity-hub/core-components/mcp-servers/management) API to create and manage them. In this scenario you **spin up a different MCP server for every customer**. Each of these MCP servers is modeled as a **separate MCP Server (resource)** in Descope, with its own well-known, flows, scopes, and policies. The gateway still enforces policy when the agent requests a connection or resource token; only permitted agents get access to a given customer’s MCP server. Within each MCP server you build for a customer, you use **token exchange** to either: - Manage **Connections** (e.g. OAuth or API key-based tools), or - Issue **API access tokens** (e.g. Descope resource tokens for your own APIs). The diagram below shows two such internal MCP servers (Server A and Server B), each connecting to its own set of backends: - Server A → API A (Descope OAuth Resource) and API B (API key) - Server B → API C (Descope OAuth Resource) and API D (API key) No third-party MCP servers are involved; each server is your own, per-customer deployment. |tool call request| Policy Policy -->|allowed| STS1 Policy -->|denied| Denied STS1 -->|access token + tool request| ServerA STS1 -->|connection token + tool request| ServerB STS2_A --> |API A access token| API_A STS2_A --> |Connection token| API_B STS2_B --> |API C access token| API_C STS2_B --> |Connection token| API_D classDef descope2 fill:#4F46E5,stroke:#3730A3,color:#fff class Policy,STS1,STS2_A,STS2_B,API_A,API_C descope2 `} /> In both scenarios, policies are evaluated when the gateway issues a connection or resource token. If the agent is not permitted for that MCP server or scope, the request is denied and the agent cannot use that tool. ## MCP Servers per Tenant In a gateway, you often have **multiple logical MCP servers** behind a single public endpoint. For example: - One MCP server per customer or tenant. - One MCP server per product domain (e.g. “billing MCP”, “analytics MCP”). In Descope, you model each of these as its own **[MCP Server](/agentic-identity-hub/core-components/mcp-servers)**, with: - **Well-known MCP server metadata** - each server can expose its own `.well-known` configuration (endpoints, capabilities, etc.). - **[Client registration templates](/agentic-identity-hub/core-components/mcp-servers/settings#flows)** - define the default client registration behavior and UX: - **Flows** - which Descope Flow runs when a client registers (e.g. consent, MFA, attribute collection). - **Session management** - how long sessions last, idle timeouts, refresh behavior. - **Branding** - logo, colors, and consent screen text specific to that MCP server or tenant. - **[DCR / CIMD settings](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-client-registration)** - dynamic client registration or client-initiated metadata discovery configuration per server. - **[Audience and scopes (resource definitions)](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes)** - what each MCP server exposes: - **Audience** - the resource identifier your agents target (e.g. `https://mcp.example.com/tenant-a`). - **Scopes** - fine-grained permissions (e.g. `mcp:read`, `mcp:write:invoices`, `mcp:run:tools`). You can create **one MCP server per tenant**, even if they are all backed by the same runtime code. This lets you: - Give each customer their own **session configuration** (e.g. stricter or more relaxed timeouts). - Run different **Flows** for different customers (e.g. extra consent, extra attributes, custom legal text). - Customize **branding** per customer (logos, product name, colors). ## Connections per Tenant Descope [Connections](/agentic-identity-hub/core-components/connections) are not just global integrations - you can also scope them to a **specific tenant**. This is powerful for MCP gateways because: - Each tenant may need its **own credentials** for downstream systems: - OAuth client IDs/secrets. - API keys. - Each tenant may need **different** [Connection scope mappings](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes). For an MCP gateway, you can: - Create **Connections per tenant** that hold: - Tenant-specific OAuth credentials for the tenant's MCP server. - Tenant-specific API keys or secrets for the tools that MCP server exposes. - Any custom headers, base URLs, or configuration needed to talk to that tenant's environment. ![Connections per tenant](/assets/connections-per-tenant.webp) - Use those Connections in your **Flows** so that: - When a request targets a given tenant's MCP server, Descope automatically uses that tenant's Connection. - Scope/permission mapping happens per tenant (for example, `mcp:read` at the gateway can map to different underlying scopes per tenant MCP server). ![Mapping per MCP server](/assets/outbound-mapping-per-mcp-server.webp) This lets you run a single gateway codebase while isolating credentials, endpoints, and scope mappings per tenant. ## Policies Per MCP Server (Tenant-Specific) Descope [Policies](/agentic-identity-hub/policies) let you define **who can talk to what**, and **under which rules**: - Which **agents/clients** are allowed to access a given MCP server. - Which **scopes** they can request and under what conditions. - Any additional **constraints** (e.g. IP allowlists, tenant constraints, environment). In an MCP gateway, you can: - Create **[multiple policies](/agentic-identity-hub/policies)**, each tied to: - A specific **MCP server** (for example, “Tenant A MCP server”). - A specific **set of agents** or client types (for example, “internal tools”, “customer LLM gateway”, “partner agents”). - Express different rules per combination, such as: - “Agents in group X can only access `mcp:read` on Tenant A's MCP server.” - “Agents in group Y can run write tools (`mcp:write:*`) only on a staging MCP server, not production.” - “External partner agents can access only specific MCP servers with a limited scope set.” ![Policies per MCP server](/assets/policies-per-mcp-server.webp) # Portkey (/mcp/gateways/portkey) Use Descope as the identity provider for Portkey's MCP Gateway with External OAuth, and forward validated claims to downstream MCP servers. # Descope + Portkey [Portkey](https://portkey.ai) is an MCP Gateway that sits in front of one or more MCP servers, authenticates incoming requests, and forwards identity downstream. With Portkey's [Bring Your Own Auth](https://portkey.ai/docs/product/mcp-gateway/authentication/bring-your-own-auth) support, you can plug Descope in as the identity provider so that Portkey validates Descope tokens at the gateway before any MCP traffic reaches your servers. Descope handles identity, authentication, and authorization through the [Agentic Identity Hub](/agentic-identity-hub). Portkey enforces policy at the gateway layer by validating those tokens, then forwards authenticated user identity to downstream MCP servers. ![agentic identity hub list of clients](/assets/agentic-identity-hub-list-of-clients.webp) This setup is useful when you want to: - Use Descope as the source of truth for user identity across MCP servers - Model the Portkey gateway as an [MCP Server](/agentic-identity-hub/core-components/mcp-servers) in Descope to get full visibility into user and tenant consent, granted permissions, and enable [CIMD/DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods) with minimal configuration - Validate tokens centrally at the gateway rather than in every individual MCP server - Forward user claims (roles, tenant, email, custom attributes) downstream for authorization and audit logging - Avoid provisioning separate Portkey accounts per user ## How It Works 1. The agent or MCP client authenticates with Descope and receives an access token (JWT). 2. The agent sends MCP requests to Portkey with the Descope token in the `Authorization` header. 3. Portkey validates the JWT against Descope's JWKs endpoint, checks the issuer, and verifies required claims. 4. Portkey extracts user claims from the token and forwards them to the downstream MCP server via [Identity Forwarding](https://portkey.ai/docs/product/mcp-gateway/authentication/identity-forwarding). 5. The MCP server uses the forwarded identity for authorization, audit logging, or personalization. Descope (as the IdP) stays the source of truth, and Portkey verifies every access token with Descope's public keys. ## Configuring Descope as Portkey's Identity Provider Before configuring Portkey, [create an MCP Server](/agentic-identity-hub/core-components/mcp-servers) in Descope to represent your Portkey gateway. This is what lets you control [CIMD and DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods) for the gateway, define the scopes users consent to, and see every connected agent in the [Agentic Identity](/agentic-identity-hub/core-components/agents) view. Once the MCP Server exists, grab its **Issuer URL** from the [Usage Samples](/agentic-identity-hub/core-components/mcp-servers#usage-samples) section of the MCP Server configuration in the Descope Console, and use it as the `iss` value in Portkey. Then, in Portkey, configure your MCP server's `jwt_validation` block to point at Descope: ```json { "jwt_validation": { "jwksUri": "__BaseURL__/{project-id}/.well-known/jwks.json", "algorithms": ["RS256"], "requiredClaims": ["sub", "email"], "claimValues": { "iss": { "values": "", "matchType": "exact" } } } } ``` Replace `{project-id}` with your Descope **Project ID** (from [Project Settings](/management/project-settings) in the Descope Console), and replace `` with the Issuer URL shown for your gateway's MCP Server. ### Required Configuration Values | Field | Value | | --- | --- | | `jwksUri` | `__BaseURL__/{project-id}/.well-known/jwks.json` | | `algorithms` | `["RS256"]` | | `iss` (in `claimValues`) | Issuer URL from your MCP Server's Settings page (format: `__BaseURL__/v1/apps/agentic/{project-id}/{mcp-server-id}`) | ### Enabling Dynamic Registration Methods Because the Portkey gateway is modeled as an MCP Server in Descope, enabling [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) or [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) is a per-server toggle in the MCP Server's [registration settings](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-client-registration); no extra infrastructure required. Any compliant MCP client can then register against the gateway automatically. ## Client Configuration Once Portkey is configured, agents authenticate with Descope and include the Descope-issued JWT on requests to Portkey: ```json { "mcpServers": { "linear": { "url": "https://mcp.portkey.ai/linear/mcp", "headers": { "Authorization": "Bearer " } } } } ``` No Portkey API key is needed on the client side. Portkey authorizes the request entirely based on the Descope token. ## Using Custom Claims One of the practical advantages of pairing Descope with Portkey is control over what ends up in the JWT. Using Descope as your identity provider, you can configure your Descope JWT claims to drive both Portkey's validation logic and downstream MCP server authorization. One example could be to enforce a claim that the user has a subscription tier, and that tier is used to authorize tool access to the downstream MCP server. See [Custom Claims Security Best Practices](/security-best-practices/custom-claims) when choosing which claims to include in your JWT. You can typically do this either via [Custom Claims](/flows/actions/custom-claims) or [JWT Templates](/management/token/jwt-templates). ### Enforcing Claims in Portkey Portkey rejects any token missing a required claim or with an `iss` / `aud` mismatch. Once the claims are on the token, enforce them in Portkey's `jwt_validation` block using `requiredClaims` and `claimValues`: ```json { "jwt_validation": { "jwksUri": "__BaseURL__/{project-id}/.well-known/jwks.json", "algorithms": ["RS256"], "requiredClaims": ["sub", "email", "groups", "tenant_id"], "claimValues": { "iss": { "values": "__BaseURL__/v1/apps/agentic/{project-id}/{mcp-server-id}", "matchType": "exact" }, "aud": { "values": ["https://{your-portkey-gateway-host}/mcp"], "matchType": "contains" }, "subscription": { "values": "free", "matchType": "exact" } } } } ``` ## Forwarding Identity to Downstream MCP Servers After Portkey validates the Descope JWT, you can forward user identity to the downstream MCP server using Portkey's [Identity Forwarding](https://portkey.ai/docs/product/mcp-gateway/authentication/identity-forwarding). This lets your MCP server handle per-user authorization or audit logging without implementing OAuth itself. Pair `jwt_validation` with `user_identity_forwarding` and specify which claims to pass through: ```json { "jwt_validation": { "jwksUri": "__BaseURL__/{project-id}/.well-known/jwks.json", "algorithms": ["RS256"], "requiredClaims": ["sub", "email", "groups", "tenant_id"], "claimValues": { "iss": { "values": "__BaseURL__/v1/apps/agentic/{project-id}/{mcp-server-id}", "matchType": "exact" } } }, "user_identity_forwarding": { "method": "claims_header", "include_claims": ["sub", "email", "groups", "tenant_id"] } } ``` Portkey extracts the listed claims from the validated Descope JWT and injects them into the downstream request as a JSON header, signed JWT, or bearer token passthrough. The claim contents are entirely up to you. Anything you add to the Descope JWT via [Custom Claims](/flows/actions/custom-claims) or [JWT Templates](/management/token/jwt-templates) (Descope [roles](/authorization/role-based-access-control), tenant metadata, attributes pulled from your existing IdP, etc) arrives at the MCP server as trusted identity context, without the MCP server needing to implement OAuth itself. # Adaptive MFA (/mfa-and-step-up/mfa/adaptive-mfa) Learn how to implement adaptive MFA within your Descope flows. This guide has examples of trusted device, IP reputation, and impossible traveler adaptive MFA. # Adaptive MFA Descope allows you to support adaptive MFA within your authentication flow. Adaptive MFA is a security mechanism that dynamically adjusts the authentication requirements based on various risk factors during a login attempt. Unlike traditional MFA, which consistently applies the same authentication steps (like a password and a secondary factor such as an OTP code), adaptive MFA assesses contextual information and only applies the secondary factor when certain conditions are met. This guide covers how to implement adaptive MFA within your Descope flows. Check out our prebuilt adaptive MFA flows within our [Flow Library](/flows/intro-to-flows/flow-library) for more examples. ## Implementation Adaptive MFA can be based on many different criteria, such as trusted device, impossible traveler, bad IP reputation, risk calculations, etc. This adaptability ensures that the MFA system can respond to a wide range of security threats, making it a robust and effective solution. Below are some examples of dynamic MFA with each of these items, as well as a combined version including multiple MFA triggers. These examples start with OTP via email and then progress to MFA with OTP via SMS. However, you could utilize other authentication methods for both the primary and secondary factors. ![Basic start to an adaptive MFA flow within Descope](/assets/baseline-for-adaptive-mfa.webp) ### Trusted device For trusted device functionality to work, you need to have a [custom domain](/how-to-deploy-to-production/custom-domain) configured for your Descope project. A trusted device in authentication allows users to bypass additional security checks (like MFA) on pre-approved devices. These devices are recognized by unique identifiers or behavioral patterns and marked as safe for future logins. Trusted devices improve security and convenience by adding an extra layer of assurance while reducing user friction. To implement trusted device within Descope flows for adaptive MFA, you will prompt the user whether they'd like to trust (remember) the device and then utilize the `Mark Device as trusted` action. ![Add a prompt screen and mark device as trusted action within a Descope flow to enable adaptive MFA for trusted device](/assets/adaptive-mfa-trust-device-promt.webp) Then, on subsequent logins, you can check if the device is trusted by adding a condition to check if the device is trusted, and trigger MFA if the device is not trusted. ![Add a condition to check if a device is trusted within a Descope flow to enable adaptive MFA for trusted device](/assets/adaptive-mfa-trust-device-condition.webp) End users can see their own trusted devices through the [User Profile Widget](/widgets/users#trusted-devices). As an admin, you currently cannot see a user's trusted devices. ### Impossible Traveler The impossible traveler scenario in authentication occurs when a user attempts to log in from two geographically distant locations in a short time, making it physically impossible for the person to travel between them. Descope allows you to check for this anomaly and handle user authentication differently: block or trigger MFA to verify the user. To implement impossible traveler within Descope flows, you will utilize a condition to check for the impossible traveler scenario and then trigger MFA in the event of an impossible traveler scenario. ![Add a condition to check for impossible traveler within a Descope flow to enable adaptive MFA](/assets/adaptive-mfa-impossible-traveler-condition.webp) ![Completed impossible traveler Descope flow to enable adaptive MFA](/assets/adaptive-mfa-impossible-traveler.webp) ### IP reputation IP reputation is calculated based on known malicious activity, frequency of suspicious requests, and involvement in botnets or attacks. If an IP has a poor reputation, Descope allows you to configure adaptive MFA to trigger Multi-Factor Authentication (MFA) to add an extra layer of security before allowing access from that IP. This use case depends on an IP reputation connector such as [Abuse IPDB](/connectors/connector-configuration-guides/fraud/abuseipdb). Once you have configured an IP risk calculation connector such as [Abuse IPDB](/connectors/connector-configuration-guides/fraud/abuseipdb), add the action to the flow by clicking the blue `+` in the top left corner, navigate to connectors, then search for the connector action and add it to the flow in the correct location. ![Completed risky IP reputation Descope flow to enable adaptive MFA](/assets/adaptive-mfa-risky-ip.webp) The condition for the use case of checking IP reputation within this flow would look like the below. ![Add a condition to check for risky IP reputation within a Descope flow to enable adaptive MFA](/assets/adaptive-mfa-risky-ip-condition.webp) ### Combined Use case You may want a combined use case that utilizes multiple tiers of risk analysis within your Descope flow. Below, you can see a combined flow that implements the detailed adaptive MFA use cases above (Trusted Device, Impossible Traveler, and IP Reputation) into a single flow. ![An example of an adaptive MFA flow within Descope which implements IP Reputation, trusted device, and impossible traveler](/assets/adaptive-mfa-combined-flow.webp) ### Other Risk Calculations and Connectors For further adaptive MFA configurations, you can utilize additional risk-based conditions in your flow. You can use built-in Descope `riskInfo` functions, and/or risk details collected via Connector within your adaptive MFA conditions. Check out our [Fingerprinting guide](/fingerprinting) for more details. # Homegrown Auth with MFA (/mfa-and-step-up/mfa/homegrown-auth-mfa) Add Descope-powered MFA to an existing homegrown authentication system while keeping your own JWTs for session management. # Homegrown Auth with MFA If you already have your own login system, with your own password checks and your own sessions, you don't need to rebuild it to add MFA. Your backend keeps validating the primary factor and issuing its own JWTs. Descope only handles the second factor, then hands control back to you. If you'd rather have Descope fully manage sessions (and get Descope JWTs back directly), see [MFA with Backend SDKs](/mfa-and-step-up/mfa/mfa-with-sdks/backend-sdk) or [MFA with Client SDKs](/mfa-and-step-up/mfa/mfa-with-sdks/client-sdk) instead. This guide is for teams that want to keep their existing auth system as the source of truth. ## How It Works This pattern uses Descope as an OAuth provider for the second factor only: 1. The user submits credentials to **your** backend as they always have. 2. Your backend validates the primary factor however it already does (password, existing session, etc.). 3. Your backend redirects the user to a Descope-hosted flow to complete MFA, using the Descope SDK's OAuth helpers. 4. The user completes the second factor of your choice (passkeys, TOTP, OTP, or magic link; see [Second-Factor Options](#second-factor-options) below). 5. Descope redirects back to your callback with an authorization code. 6. Your backend exchanges the code for a Descope session and validates it to confirm MFA succeeded. 7. Your backend mints **its own** JWT and returns it to the user. Descope's session token only confirms MFA; your app doesn't use it going forward. ## Prerequisites - A Descope project. - A custom OAuth provider named `Descope` configured in **Authentication Methods → OAuth**, pointing its authorization and token URLs at your own project's OIDC endpoints. This is what gives `oauth.start('Descope', redirectUrl)` somewhere to redirect to. See [Configuring a Custom Provider](/auth-methods/oauth/providers/custom-providers#configuring-a-custom-provider) and [OIDC Endpoints](/identity-federation/applications/oidc-apps/oidc-endpoints). - A Descope flow behind that provider that runs the MFA challenge. This is where you drop in one of the [flow templates](#second-factor-options) below. - The [Descope Node SDK](https://github.com/descope/node-sdk) (or any backend SDK) installed in your app. ```sh title="Terminal" npm i --save @descope/node-sdk jose ``` ```ini title=".env" JWT_SECRET=your-own-jwt-signing-secret DESCOPE_PROJECT_ID=your-project-id DESCOPE_REDIRECT_URL=http://localhost:3000/api/auth/callback ``` ## Code Walkthrough This example is based on the [homegrown-auth-server](https://github.com/descope-sample-apps/homegrown-auth-server) sample app (Node/Express + TypeScript). The same pattern applies with any backend SDK. ### 1. Initialize the SDK and set up the app ```typescript title="src/config/descope.ts" import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: process.env.DESCOPE_PROJECT_ID || '', }); export default descopeClient; ``` The rest of this walkthrough lives in your Express entry point. Set up the pieces every later step builds on: ```typescript title="api/index.ts" import express, { Request, Response, NextFunction } from 'express'; import { SignJWT, jwtVerify } from 'jose'; import descopeClient from '../src/config/descope'; const app = express(); app.use(express.json()); // Extend Express's Request type so authenticated routes can read req.user interface AuthenticatedRequest extends Request { user?: any; } ``` ### 2. Validate the primary credential Keep your existing login logic exactly as it is. If the primary factor checks out, kick off the redirect to Descope for MFA instead of logging the user in directly. ```typescript title="api/index.ts" app.post('/api/auth/login', async (req: Request, res: Response) => { const { email, password } = req.body; if (isValidPassword(email, password)) { const redirectUrl = await getOidcRedirectUrl(email); res.json({ redirectUrl }); } else { res.status(401).json({ message: 'Invalid credentials' }); } }); ``` ### 3. Redirect to Descope for the second factor `oauth.start` builds the authorization URL for the custom `Descope` provider you configured. Its full signature is `(provider, redirectUrl, loginOptions, token, loginHint)`. Pass the identity you verified in the previous step as `loginHint` so it ties the MFA step to the same user and pre-fills it in the hosted flow. ```typescript title="api/index.ts" const getOidcRedirectUrl = async (userEmail: string) => { const redirectUrl = process.env.DESCOPE_REDIRECT_URL || ''; const response = await descopeClient.oauth.start( 'Descope', redirectUrl, undefined, undefined, userEmail, ); if (!response.ok || !response.data?.url) { throw new Error(`OAuth start failed: ${response.error?.errorMessage}`); } return response.data.url; }; ``` ### 4. Handle the callback and exchange the code Once the user completes MFA, Descope redirects back with a `code`. Exchange it for a session. ```typescript title="api/index.ts" app.get('/api/auth/callback', async (req: Request, res: Response) => { const { code, error } = req.query; if (error || !code || typeof code !== 'string') { return res.status(400).json({ message: `Authentication failed: ${error ?? 'missing code'}` }); } const tokenResponse = await descopeClient.oauth.exchange(code); if (!tokenResponse.ok) { return res.status(401).json({ message: 'Failed to exchange authorization code' }); } const sessionToken = tokenResponse.data?.refreshJwt; // continue to session validation below... }); ``` ### 5. Validate the session, then mint your own JWT `validateSession` throws if the session isn't valid, so wrap it in a try/catch rather than checking for a falsy return. On success it returns the decoded token; `token.sub` is the Descope user ID, which is what you sign into your own JWT. ```typescript title="api/index.ts" try { const { token } = await descopeClient.validateSession(sessionToken); const secret = new TextEncoder().encode(process.env.JWT_SECRET || ''); const userToken = await new SignJWT({ sub: token.sub }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('1h') .sign(secret); // Return userToken to the client however your app normally establishes a session // (cookie, redirect with token, etc.) } catch { return res.status(401).json({ message: 'Invalid session' }); } ``` Need more than the user ID in your own JWT, such as email? Read it from the `user` object on the `oauth.exchange` response in the previous step, or add it as a [custom claim](/flows/actions/custom-claims) in the Descope flow. ### 6. Protect routes with your own JWT From here on, your app doesn't need Descope at all. Verify your own JWT like you always have. ```typescript title="api/index.ts" const authenticateToken = async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ message: 'No token provided' }); try { const secret = new TextEncoder().encode(process.env.JWT_SECRET || ''); const { payload } = await jwtVerify(token, secret); req.user = payload; next(); } catch { res.status(401).json({ message: 'Invalid token' }); } }; app.get('/api/protected', authenticateToken, (req: AuthenticatedRequest, res: Response) => { res.json({ message: 'This is a protected route', user: req.user }); }); ``` ## Second-Factor Options Swap in any of these flow templates as the flow behind your `Descope` custom provider: | Second factor | Flow template | | --- | --- | | Magic Link | [Magic Link Sign-Up-Or-In](https://app.descope.com/flows?template=magiclink-template-sign-up-or-in) | | One-Time Password (OTP) | [Email or Phone Sign-Up-Or-In](https://app.descope.com/flows?template=email-or-phone-template-sign-up-or-in) | | Passkeys | [Biometrics + OTP Sign-In](https://app.descope.com/flows?template=biometrics-otp-template-sign-in) | | TOTP (Authenticator App) | [TOTP](https://app.descope.com/flows?template=totp) | Browse the full [Flow Library](https://app.descope.com/flows) for more options, or see [Using the Flow Library](/flows/intro-to-flows/flow-library) for how to preview and customize a template. ## Don't Ask for the Login ID Twice Your backend already collected the user's email or phone in the primary login step. Don't make users type it again once they land in the Descope-hosted flow. `login_hint` pre-fills the `form.externalId` context key in the flow. It does not populate `form.email` or `form.phone`, the fields the templates above use, and it does not skip the identifier-collection screen on its own. See [Dynamic Values](/flows/dynamic-keys#form) for the full list of `form` context keys. The templates above start with a "Collect Email or Phone" screen that writes into `form.email` or `form.phone`. To skip it when a `login_hint` is present, edit your copy of the template one of two ways: 1. **Skip it conditionally.** Add a [Scriptlet](/flows/actions/scriptlets) at the start of the flow that copies `form.externalId` into `form.email` (or `form.phone`, matching your identifier format). Then add a [Condition](/flows/conditions) that skips straight to the send-magic-link, send-OTP, or verify-TOTP step whenever that value is already set, and falls through to the "Collect Email or Phone" screen only when it's empty. Use this option if the flow might also run without a `login_hint`, such as when you test it directly in the console. 2. **Remove the screen entirely.** If this flow only runs through your OAuth redirect, where `login_hint` is always set, delete the "Collect Email or Phone" screen from your copy of the template and feed the scriptlet's output directly into the send or verify action. ## Try It Yourself Find the full working example on GitHub: ```sh title="Terminal" git clone https://github.com/descope-sample-apps/homegrown-auth-server.git cd homegrown-auth-server npm install npm run dev ``` For a narrated walkthrough of this same sample app, see [Adding MFA to Homegrown Auth With Descope](https://www.descope.com/blog/post/mfa-homegrown) on the Descope blog. ## Related - [MFA with Backend SDKs](/mfa-and-step-up/mfa/mfa-with-sdks/backend-sdk): for teams that want Descope to fully manage sessions instead - [Adaptive MFA](/mfa-and-step-up/mfa/adaptive-mfa): trigger MFA conditionally based on risk - [Step-up Authentication](/mfa-and-step-up/step-up): re-verify identity for sensitive actions - [Flow Library](/flows/intro-to-flows/flow-library): browse and search all available flow templates # Overview (/mfa-and-step-up/mfa) Add layered security to your app utilizing Multi-factor Authentication (MFA). # Multi-factor Authentication (MFA) Descope provides the ability to add layered security to your application by implementing Multi-factor Authentication (MFA). MFA is an authentication method that requires the user to provide two or more separate pieces of evidence to verify their identity. For example, the first factor can be an OTP sent to the user’s phone or email, and the second factor can be biometric authentication. Using more than one factor greatly reduces the chance of attackers compromising a user’s account. With Descope, you can implement MFA within your application using either [Flows](/mfa-and-step-up/mfa#with-flows), [Client SDKs](/mfa-and-step-up/mfa/mfa-with-sdks/client-sdk), or [Backend SDKs](/mfa-and-step-up/mfa/mfa-with-sdks/backend-sdk). Irrespective of the implementation method, once MFA has been successfully completed, the returned JWT will include `mfa` within the `amr` claim of the JWT. Already have your own login system and want Descope to handle the second factor? See [Homegrown Auth with MFA](/mfa-and-step-up/mfa/homegrown-auth-mfa) for a pattern that keeps your existing auth and JWTs in place. ```bash { "amr": [ "oauth", "sms", "mfa" ], "drn": "DS", "exp": xxx, "iat": xxx, "iss": "xxxxxx", "rexp": "2024-08-08T14:24:58Z", "sub": "xxxxxx" } ``` ## With Flows In Descope, MFA is implemented as a sequence of two or more authentication methods that provide different pieces of evidence. As the developer, you have the flexibility to determine how you want to implement MFA in your flow. For example, you could create an MFA flow that: - Starts with a password, followed by TOTP verification - Uses social login, then requires a one-time password sent via SMS - Begins with passkeys, followed by a magic link via email - Combines any other authentication methods that provide different pieces of evidence MFA requires multiple different pieces of evidence. You cannot use the same channel twice. For example: - ❌ Magic link to email + OTP to the same email - ❌ SMS OTP + SMS magic link to the same phone number The key is that users must successfully complete multiple authentication steps using different pieces of evidence before gaining access. This approach gives you the flexibility to choose the authentication methods that best balance security and user experience for your specific use case. Check out the MFA section of our [flow library](/flows/intro-to-flows/flow-library) for more examples of flows that implement MFA. ![MFA Flow Library](/assets/mfa-flow-library.webp) # Clients (/agentic-identity-hub/core-components/clients) Learn how to view, create, and manage OAuth clients for MCP servers and autonomous agents in the Agentic Identity Hub. # Clients The [Clients](https://app.descope.com/agentic-hub/clients) page is where you register, view, and manage the OAuth clients in your project. From here you can: - **Create clients manually** for autonomous agents, pre-registered MCP clients, or any case where you want to configure credentials and grant types ahead of time - **View all clients** registered in your project, including those created automatically via [DCR or CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods) when MCP clients like Claude or Cursor connect to your MCP servers - **Manage clients**: update tags, configure grant types, and delete clients that are no longer in use Clients fall into two categories: The Clients view in the Agentic Identity Hub provides an overview of all OAuth clients in your project. Clients are the OAuth applications that connect to your MCP servers, and they fall into two categories: - **Dynamically registered clients**: Clients registered automatically through Dynamic Client Registration (DCR) or Client ID Metadata Documents (CIMD) when an MCP client like Claude, Cursor, or VS Code connects to one of your MCP servers. These get access to the MCP server they registered with automatically, with no policy required. - **Manually registered clients**: Clients you pre-register yourself, typically for autonomous agents using `client_credentials` or for cases where you have disabled DCR/CIMD on your MCP server. These need a [policy](/policies) before they can reach a Resource. All CIMD registrations that present the same metadata URL resolve to the **same client** with one client ID. Every Claude user, for example, presents Claude's metadata URL, so they all appear under a single Claude client, but each user's consent creates its own [agentic identity](/agentic-identity-hub/core-components/agents) under that client. ## Viewing Clients ![Clients](/assets/clients-list.webp) Navigate to the [Clients](https://app.descope.com/agentic-hub/clients) section in the Descope Console to see an overview of all your OAuth clients and their details. ## Creating a Client ![Creating a client](/assets/creating-a-client.webp) To create a new client manually, click the `+ Client` button in the top right of the Clients page. The creation form is divided into several sections. Creating the client does not by itself grant it access to anything. To let a manually created client reach a [Resource](/resources), for example an MCP server, you must also add a [policy](/policies) whose subject is this client and whose target is that Resource, with the scopes it may receive. The Console shows a reminder in the bottom-right corner after you create the client. This applies only to **manually created** clients. Clients registered through [DCR or CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods) automatically get access to the MCP server Resource they registered with; no policy required. ### Client Details The Client Details section captures the basic identifying information for the client. #### Name The client **name** is a required, human-readable identifier that helps you distinguish between clients in your project. #### Description The **description** is an optional field where you can provide additional context about the client, such as its purpose, owner, or environment. #### Logo You can optionally paste a logo for the client. When a client is registered dynamically through a well-known MCP client such as Claude, Cursor, or VS Code, Descope will pre-fill the logo for you automatically. ### Tags ![Tags](/assets/tags.webp) **Tags** are optional labels you can assign to a client for organization and categorization. You can add as many tags as you want. Press **Enter** after typing each tag name to add it to the client. Tags help you group and filter clients based on custom criteria such as environment, team, or use case. ### Grant Types ![Grant types](/assets/grant-types.webp) The **Grant Types** section controls which OAuth grant types the client is allowed to use. Enable a grant type with the toggle on the left; use **Manage** on the right to configure grant-specific settings. | Grant Type | Description | |------------|-------------| | **Authorization Code** | Interactive flows where a user authenticates and consents to the client acting on their behalf. | | **Client Credentials** | Machine-to-machine authentication where the client authenticates as itself, with no user involved. | | **JWT Bearer** | The client presents a signed JWT from a trusted issuer to obtain a Descope access token (`urn:ietf:params:oauth:grant-type:jwt-bearer`). **Not enabled by default.** | | **CIBA** | Client-Initiated Backchannel Authentication for asynchronous, decoupled user authorization. **Not enabled by default.** | #### Authorization Code Used when an MCP client or application redirects a user through Descope for login and consent (for example Claude, Cursor, or VS Code connecting to your MCP server). Click **Manage** to configure **approved redirect URLs**: the callback URIs Descope may return users to after authentication. Restrict this list to the URLs your client actually uses, so that authorization codes cannot be sent to unexpected destinations. ![authorization code settings](/assets/authorization-code-settings.webp) For clients registered through [CIMD or DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods), redirect URLs from the registration payload are **added automatically**. #### Client Credentials Used when an autonomous agent or backend service authenticates with its client ID and secret and receives a token with no end-user session. Enabling this grant type on a client automatically creates a corresponding agentic identity in the [Agentic Identity](/agentic-identity-hub/core-components/agents) view; no separate setup required. Click **Manage** to set **Allowed Tenants**. By default a client can be used across your entire Descope project; add specific tenants to the list when you want to restrict the client to particular tenants only (for example a dedicated M2M agent per customer tenant). ![client credentials settings](/assets/client-credentials-settings.webp) #### JWT Bearer Used when the client exchanges an external OIDC JWT for a Descope-issued token at the [token endpoint](/api/third-party-apps/token-endpoint). See [External token management](/identity-federation/inbound-apps/using-inbound-apps#external-token-management) for examples of how to use this grant type. Click **Manage** to register one or more **trusted issuers** for this client. Descope accepts JWTs from any issuer you add. For each issuer you configure: | Field | Required | Description | |-------|----------|-------------| | **Issuer URL** | Yes | Expected `iss` claim on incoming JWTs. | | **JWKs URL** | No | URL for the signing keys used to validate the JWT. If omitted, Descope derives it from the issuer URL. | | **Sign algorithm** | No | Acceptable algorithms (for example `RS256` or `ES256`). If omitted, the algorithm is taken from the JWT header. | | **User Information Endpoint URL** | No | OIDC UserInfo URL called after JWT validation to load profile attributes not present in the token. | | **User Information LoginID Field Name** | No | Field name in the UserInfo response that maps to the Descope login ID for the user (for example `sub` or `email`). | You can add multiple issuers so a single client accepts JWTs from more than one external identity provider. ![jwt bearer settings](/assets/jwt-bearer-settings.webp) To learn more about how to use the JWT Bearer grant type, and how to use the Agentic Identity Hub with Workloads, see our doc on [Using Descope with Workloads](/agentic-identity-hub/core-components/clients/workloads). #### CIBA CIBA currently only supports email as a delivery method. [Client-Initiated Backchannel Authentication](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html), or CIBA, lets the client start authentication without redirecting the user's browser on the initiating device. Instead, Descope notifies the user out of band to complete approval on another device. Click **Manage** to configure: - **Delivery method:** Email is currently the supported channel. Choose a [custom email connector](/connectors/connector-configuration-guides/messaging) and [template](/management/messaging-templates) for the approval message. - **Custom link expiration:** How long the approval link remains valid (default **3 minutes**). ![CIBA settings](/assets/ciba-settings.webp) Once you've enabled CIBA, you can configure the authentication experience users see when they open the link under [Flows → CIBA](#flows). Read more details on the CIBA model in [Creating Inbound Apps → CIBA](/identity-federation/inbound-apps/creating-inbound-apps#client-initiated-backchannel-authentication-ciba). #### Token Exchange Once a client holds a Descope token (obtained through any grant type above), it can exchange that token at the [token endpoint](/api/third-party-apps/token-endpoint) for a token scoped to a specific [Resource](/resources), using the RFC 8693 token-exchange grant (`urn:ietf:params:oauth:grant-type:token-exchange`). This is how an agent turns the token it signed in with into an access token for your API or MCP server, or into an [ID-JAG](/agentic-identity-hub/enterprise-managed-authorization) to reach a third-party server. Which exchanges a client may perform is controlled by [Policies](/policies), keyed on the client as the **subject** and the target **Resource** and its scopes. A client can only exchange for a Resource an active policy allows, and never for broader scopes than the policy grants. See the [Inbound Apps token endpoint](/identity-federation/inbound-apps/authorization-server#token-exchange) for the request format. #### Choosing Grant Types Enable only the grant types the client will actually use. Unused grant types expand the attack surface without adding value. - **Authorization Code**: Use this for any client that redirects users through a consent flow. If you are pre-registering a client instead of relying on [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) or [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr), create the client here, copy the generated client ID, and configure it on the MCP client side. - **Client Credentials**: Use this for autonomous agents and background services that authenticate as themselves with no user involved. Disable any other grant types on these clients. - **JWT Bearer**: Use this when the client will authenticate by presenting an external OIDC token, for example a workload running on AWS or GCP. See [Workload Identity](/agentic-identity-hub/core-components/clients/workloads) for setup details. - **CIBA**: Use this when the client needs to initiate authentication without a browser redirect on the requesting device, routing approval to the user out of band. - **Token Exchange**: Authorized through [Policies](/policies) (subject = the client, target = the Resource) rather than enabled here. Use it when a client trades its Descope token for one scoped to a downstream Resource, or for an [ID-JAG](/agentic-identity-hub/enterprise-managed-authorization) to a third-party server. ### Flows The **Flows** section controls the experience users see when you run through OAuth grant types against this client. #### User Consent Under **Flows**, you can select the [Descope Flow](/flows) the client will run during the authorization code flow. This is where you configure the authentication and consent experience for end users. ![Flows](/assets/flows-under-user-consent.webp) You can also check the **Skip consent screen** box to bypass the consent screen entirely for this client. This is useful for trusted first-party clients where displaying a consent prompt would add friction without adding meaningful security. #### CIBA Flow This section will only be visible if you have enabled CIBA for this client. If you enable CIBA, you can select the [Descope Flow](/flows) the client will run during the CIBA flow. You will be able to either select your own flow, or generate a pre-built flow that contains the required actions for the CIBA grant type. ![CIBA flow](/assets/ciba-flow-create.webp) ### Session Management The **Session Management** section controls how tokens are issued and how long they remain valid for this client. By default, this is set to **According to system settings**, which uses the [project-level session settings](/management/project-settings#session-management). You can switch to **Custom** to override these defaults on a per-client basis. ![Session management](/assets/session-management-under-mcp-server-settings.webp) #### Token Format When using custom session management, you can choose between two token formats: - **User JWT**: A JWT representing a user session. Relevant for authorization code and CIBA flows. - **Access Key JWT**: A JWT representing a machine identity. Relevant for `client_credentials` flows. Depending on which grant types you have enabled, one or both of these formats may apply to the client. #### Token Expiration You can configure the following expiration settings: | Setting | Description | |---------|-------------| | **Refresh token timeout** | How long a refresh token remains valid before the client must re-authenticate. | | **Session token timeout** | How long a user session token remains valid. | | **Access key session token timeout** | How long an access key session token remains valid for machine clients (Only applies to `client_credentials` grant type). | ## Managing Clients ### Filtering Clients You can filter the client list to find specific clients based on various criteria. Filter operators behave the same way as in the [Agentic Identity](/agentic-identity-hub/core-components/agents#filtering-agentic-identity) view, supporting operators such as **Contains**, **Equals**, **In**, **Matches**, and timestamp comparisons across columns like Name, Client ID, and Tags. ### Selecting Clients Select one or more clients from the list to perform bulk operations such as managing tags or deleting clients. #### Managing Tags You can add or remove [tags](#tags) on individual clients or multiple clients at once. #### Deleting Clients Deleting a client is immediate and cannot be undone. Any agents or applications using this client ID will lose access and will need to be re-registered. Deleting a client removes the OAuth client and invalidates its client ID. Note that deleting a client is **not** the same as [revoking access](/agentic-identity-hub/core-components/agents#revoking-access) for an agentic identity. Revoking access invalidates previously granted user consent while leaving the client ID intact, whereas deleting the client removes the OAuth client entirely. # Workload Identity (/agentic-identity-hub/core-components/clients/workloads) Exchange cloud workload OIDC tokens from AWS or GCP for Descope access tokens using the JWT-Bearer grant type. # Workload Identity Cloud providers issue OIDC tokens to workloads running in their environments. AWS assigns them to EKS pods via IRSA; GCP assigns them to any compute resource attached to a service account. These tokens are cryptographically signed by the provider and carry claims about the workload: which service account it runs as, which project or cluster it belongs to, and what it is authorized to do. Descope can accept these tokens directly and issue a Descope access token in exchange, using the [JWT-Bearer grant type](/agentic-identity-hub/core-components/clients#jwt-bearer). Your agent never needs a long-lived secret. It authenticates as a workload, gets a scoped Descope token, and uses that token to reach your APIs and MCP servers. ## How It Works 1. Your workload runs in AWS or GCP with a cloud identity attached (an IAM role or a GCP service account). 2. It fetches an OIDC token from the cloud provider's metadata or token service. 3. It presents that token to Descope's token endpoint using the JWT-Bearer grant. 4. Descope validates the token against the configured trusted issuer, creates an agentic identity for the workload, and returns a Descope access token. 5. The workload uses the Descope token to call your protected resources. ## Prerequisites Before you can exchange a workload token, create a client in Descope with the **JWT-Bearer** grant type enabled and configure your cloud provider as a trusted issuer. 1. Go to [Clients](https://app.descope.com/agentic-hub/clients) and create a new client, or open an existing one. 2. Under **Grant Types**, enable **JWT Bearer**. 3. Click **Manage** next to JWT Bearer and add a trusted issuer. The issuer URL and JWKs URL differ by provider — see the sections below. 4. Save the client and copy the **Client ID**. You will pass this in the token request. ## AWS AWS EKS supports [IAM Roles for Service Accounts (IRSA)](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html), which projects a signed OIDC token into each pod at a known path. Descope can validate these tokens against your cluster's OIDC issuer. ### Trusted Issuer Configuration Each EKS cluster has its own OIDC issuer URL. Find yours with: ```bash aws eks describe-cluster \ --name YOUR_CLUSTER_NAME \ --query "cluster.identity.oidc.issuer" \ --output text ``` This returns a URL like `https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE1234`. In Descope, configure the trusted issuer with: | Field | Value | |-------|-------| | **Issuer URL** | Your cluster's OIDC issuer URL | | **JWKs URL** | `{issuer_url}/keys` (Descope derives this automatically if left blank) | ### IRSA Setup Annotate your Kubernetes service account with the IAM role ARN you want to associate with the workload: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: my-agent namespace: default annotations: eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/my-agent-role ``` EKS projects a signed OIDC token into the pod at `/var/run/secrets/eks.amazonaws.com/serviceaccount/token` when you mount the volume in your pod spec: ```yaml volumes: - name: aws-iam-token projected: sources: - serviceAccountToken: audience: YOUR_DESCOPE_CLIENT_ID expirationSeconds: 3600 path: token volumeMounts: - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount name: aws-iam-token ``` Set the `audience` field to your Descope client ID. Descope validates the `aud` claim on the incoming token. ### Exchanging the Token Read the projected token and exchange it at Descope's token endpoint: ```python import requests with open("/var/run/secrets/eks.amazonaws.com/serviceaccount/token") as f: workload_token = f.read().strip() response = requests.post( "__BaseURL__/oauth2/v1/token", data={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": workload_token, "client_id": "YOUR_DESCOPE_CLIENT_ID", "scope": "openid", }, ) descope_token = response.json()["access_token"] ``` Use `descope_token` to call your protected APIs and MCP servers. --- ## GCP GCP assigns an OIDC identity to any compute resource that runs with a service account attached: Cloud Run services, GKE pods, Compute Engine VMs, and Cloud Functions. The token is available from the instance metadata server. ### Trusted Issuer Configuration GCP tokens are issued by Google's OAuth infrastructure. Configure the trusted issuer in Descope with: | Field | Value | |-------|-------| | **Issuer URL** | `https://accounts.google.com` | | **JWKs URL** | `https://www.googleapis.com/oauth2/v3/certs` | ### Fetching the OIDC Token The token comes from the metadata server. Set the `audience` query parameter to your Descope client ID: ```bash curl -s \ "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=YOUR_DESCOPE_CLIENT_ID&format=full" \ -H "Metadata-Flavor: Google" ``` From Python: ```python import requests metadata_url = ( "http://metadata.google.internal/computeMetadata/v1/" "instance/service-accounts/default/identity" ) token_response = requests.get( metadata_url, params={"audience": "YOUR_DESCOPE_CLIENT_ID", "format": "full"}, headers={"Metadata-Flavor": "Google"}, ) workload_token = token_response.text ``` The metadata server is only reachable from inside a GCP compute environment. For local development, use a service account key file with the [Google Auth Library](https://google-auth.readthedocs.io/en/master/) to generate an equivalent OIDC token, or use a separate Descope client configured for development with `client_credentials`. ### Exchanging the Token ```python response = requests.post( "__BaseURL__/oauth2/v1/token", data={ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": workload_token, "client_id": "YOUR_DESCOPE_CLIENT_ID", "scope": "openid", }, ) descope_token = response.json()["access_token"] ``` ### GKE Workload Identity For GKE, use [Workload Identity Federation](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) to bind a Kubernetes service account to a GCP service account. Once the binding is in place, pods fetch tokens the same way as any other GCP compute resource, via the metadata server. --- ## Agentic Identity Each time a workload exchanges a token, Descope creates or updates an [agentic identity](/agentic-identity-hub/core-components/agents) record for it. The identity carries the workload's claims as context — project, cluster, service account, and namespace — so you can write [policies](/agentic-identity-hub/policies) that target specific workloads and see per-workload audit trails in your logs. For workloads sharing the same client ID but running in different environments (separate AWS accounts, separate GCP projects), each produces a distinct agentic identity. You can revoke one deployment's access without affecting others. # Creating a Connection (/agentic-identity-hub/core-components/connections/create-connections) Learn how to create a Connection in Descope to vault third-party OAuth tokens or API keys for MCP tools and backend services. # Creating a Connection You can create a Connection in Descope using three different methods: 1. **Manually in the Console**: Navigate to the [Connections](https://app.descope.com/agentic-hub/connections) section of the console and select `+ Connection`. You can then select a custom connection, or one of our preconfigured connections within the library. 2. **Programmatically via APIs/SDKs**: Use the [CRUD APIs](#managing-connections) to create connections programmatically through the Descope Management API or SDKs. 3. **From a DCR Preset Connection**: Create a new connection instance from a DCR preset connection. This is useful when building an MCP gateway, or when you want to create different tenant-specific connections for an OAuth provider used with another MCP server. ![Creating an connection in Descope](/assets/creating-a-connection.webp) There are two different types of Connections in Descope: [OAuth-based](#oauth-connections) and [API key-based](#api-key-connections) Connections. ## OAuth Connections An OAuth Connection lets Descope run a provider's OAuth flow and store the tokens it returns. You can build one from scratch as a custom connection, or start from a pre-configured template in the library. ### Register an OAuth App with the Provider Most setups use **Manual** connection settings, so you register an **OAuth application** with the provider yourself (Google, Microsoft, Salesforce, and so on) before you configure anything in Descope. Descope then acts as a confidential OAuth client for that provider: you create the app on their side and paste its **Client ID** and **Client Secret** into Descope. Two things need to match up on the provider's app: - **Scopes**: The app must allow the scopes you define on the Connection (see [Scopes](#scopes)). Enable the same scopes on the provider that you list in Descope. - **Authorized redirect URI**: Whitelist Descope's outbound OAuth callback URL on the provider app (see [Connection Settings](#connection-settings) below). Some providers support **Dynamic Client Registration** against Descope's callback URL, which lets you skip the manual app and use the DCR option in Connection Settings instead. Most do not, so you will usually register the app by hand. ### Connection Details The connection ID cannot be shared with any other connection or outbound app. These fields set the Connection's identity and, optionally, scope it to a tenant: - **Name (Required)**: Display name for the Connection. - **Connection ID (Required)**: Unique identifier. Set only at creation; not editable afterward. - **Description (Optional)**: Short summary of what the Connection is for. - **Associated Tenant (Optional)**: Dropdown to tie the Connection to a specific [tenant](/management/tenant-management). Use this when the Connection should be scoped to one organization (for example, tenant-specific credentials in a gateway). Leave unset for a project-wide Connection. ![Configuring an oauth connection's details in Descope](/assets/oauth-connection-details.webp) ### Account Information Account Information is where you connect Descope to the provider and list the OAuth scopes the Connection can request. #### Connection Settings Choose how Descope registers as an OAuth client with the provider: **Manual** Enter credentials from an OAuth app you created at the provider: - **Client ID (Required)**: The client ID from your OAuth application at the provider. - **Client Secret (Required)**: The client secret from the same OAuth application. ![Configuring an outbound app's connection settings in Descope](/assets/outbound-app-connection-settings.webp) Most OAuth providers require this callback URL in the app's authorized redirect URI list. Without it, the provider rejects the request when a user connects. When creating an OAuth app integration with your desired provider, the default callback URL to whitelist is: ``` __BaseURL__/v1/outbound/oauth/callback ``` ![Callback URL for app registration on a Connection](/assets/connection-oauth-callback-url.webp) If you set a **Callback Domain** or use a [custom domain](/how-to-deploy-to-production/custom-domain), the URL in the console may differ. Always use the value displayed here when registering the app at the provider. **Dynamic Client Registration** If you don't wish to create an OAuth app in the provider, and the provider supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591), you can provide a provider endpoint and Descope will register an OAuth app for you: - **Registration URL**: An `/mcp` URL or other **dynamic client registration endpoint** exposed by the provider. Most OAuth providers do not allow Dynamic Client Registration when Descope is the redirect URL. In practice you usually need **Manual** registration: create an approved app at the provider, then paste its Client ID and Client Secret into Descope. ![Configuring a connection with Dynamic Client Registration](/assets/dynamic-client-registration-connection.webp) #### Scopes This is the master list of OAuth scopes the Connection can request from the provider. Add every scope you might need, even if you do not use them all on every connect. Descope uses this list in a few places: - **Default at connect time**: If the MCP client, your app, or a [Connection flow action](/agentic-identity-hub/core-components/connections/storing-connections#method-2-descope-flows) does not pass specific scopes, Descope requests the scopes defined here. - **Override at connect time**: A client or flow action can request a subset (or custom set). Those override the defaults for that connect only. - **MCP Server mapping**: Scopes defined here appear in [MCP Server Scopes](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes) under **Connection scopes**, where you map each MCP server scope to the provider scopes this Connection should fetch. They do not appear in that mapping UI until you define them on the Connection. You must also enable the same scopes on the provider's OAuth app. Otherwise the provider will not grant them. For example, Google Contacts defaults to full access (`https://www.googleapis.com/auth/contacts`), but you can configure read-only access (`https://www.googleapis.com/auth/contacts.readonly`) instead: ![Configuring an outbound app's scopes in Descope](/assets/outbound-app-scopes.webp) You must also configure the same scopes in your OAuth provider's application settings (e.g., Google Cloud Console) for them to be available. ![Configuring additional scopes on a Google Oauth application to be used with outbound apps in Descope](/assets/outbound-google-contacts-example.webp) ### Additional Settings Additional Settings covers where users land after a successful connection, which OAuth endpoints Descope calls, and optional query parameters on the authorize request. When using a predefined connection from the connections library, the authorization and token endpoints are prepopulated. - **Redirect URL (Optional)**: The default redirect URL after a successful connection. This value will be overridden when using flows or specifying the redirect URL in the API/SDK call. - **Authorization Endpoint (Required)**: The endpoint to request authorization from the user. - **Token Endpoint (Required)**: The endpoint to exchange the authorization code for an access token. - **Authorize request params**: Use **+ Add request param** to attach extra query parameters to the provider's `/authorize` request when a user connects. Set fixed key/value pairs for provider-specific parameters such as `resource`, `audience`, or other OAuth query params this Connection always needs on the initial connect. ![Configuring an outbound app's additional settings in Descope](/assets/outbound-app-additional-settings.webp) Once created, you can read how you can [connect to your OAuth provider](/agentic-identity-hub/core-components/connections/storing-connections), and [fetch the OAuth tokens](/agentic-identity-hub/core-components/connections/fetching-connection-tokens) with our SDKs. ## API Key Connections This is ideal for integrating with external services that do not support OAuth, and use static tokens instead for authentication. In addition to storing OAuth tokens, Descope can also store static API keys with API Key-based Connections. A few templates use API keys, such as Jenkins and OpenAI, or you can create your own **Custom API key Connection**. ### Connection Details The connection ID cannot be shared with any other connection. These fields configure the connection: - **Logo (Optional)**: Upload a logo by clicking the edit button on the logo. The "Connect To" consent button in your flows uses this logo automatically. - **Connection Name (Required)**: The display name for the connection. You can edit it later. - **Description (Optional)**: An optional description of the connection. You can edit it later. - **Connection ID (Required)**: Set this only when you create the connection. It cannot be changed afterward. ![Configuring an api key connection's details in Descope](/assets/api-key-connection-details.webp) API key based connections don't have any additional settings besides the details above. Once created, you can [store API keys](/agentic-identity-hub/core-components/connections/storing-connections) via Flows, the connect API, or [Add Tenant Token in the Console](/agentic-identity-hub/core-components/connections/storing-connections#method-4-add-tenant-token-via-the-descope-console) for tenant-level keys, then [fetch them](/agentic-identity-hub/core-components/connections/fetching-connection-tokens) with our SDKs. ## Managing Connections You can create, update, delete, and load connections programmatically using a Descope SDK directly via the REST API. ### Create a Connection ```js // Create a connection const { id } = await descopeClient.management.outboundApplication.createApplication({ name: 'my new connection', description: 'my desc', // ...other fields (see [Connection Schema](#connection-schema) below) }); ``` ```python # Create a connection response = descope_client.mgmt.outbound_application.create_application( name='my new connection', description='my desc', # ...other fields (see [Connection Schema](#connection-schema) below) ) id = response['app']['id'] ``` ```java // Set up the outbound apps service OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Create a connection OutboundAppRequest request = new OutboundAppRequest(); request.setName("my new connection"); // ...other fields (see [Connection Schema](#connection-schema) below) OutboundAppCreateResponse response = outboundAppsService.createApplication(request); String id = response.getId(); ``` ```go // Create a connection appRequest := &descope.CreateOutboundAppRequest{ OutboundApp: descope.OutboundApp{ Name: "my new connection", Description: "my desc", // ...other fields (see Connection Schema below) }, ClientSecret: "your-client-secret", // optional } app, err := descopeClient.Management.OutboundApplication().CreateApplication(ctx, appRequest) if err != nil { // Handle error } id := app.ID ``` ```http POST /v1/mgmt/outbound/app/create Authorization: Bearer __ProjectID__: Content-Type: application/json { "name": "my new connection", "description": "my desc" // ...other fields (see [Connection Schema](#connection-schema) below) } ``` ### Update a Connection ```js // Update a connection (overrides all fields) await descopeClient.management.outboundApplication.updateApplication({ id: 'my-connection-id', name: 'my updated connection', // ...other fields (see [Connection Schema](#connection-schema)) }); ``` ```python # Update a connection (overrides all fields) descope_client.mgmt.outbound_application.update_application( id='my-connection-id', name='my updated connection', # ...other fields (see [Connection Schema](#connection-schema)) ) ``` ```java // Update a connection (overrides all fields) OutboundAppRequest request = new OutboundAppRequest(); request.setId("my-connection-id"); request.setName("my updated connection"); // ...other fields (see [Connection Schema](#connection-schema)) outboundAppsService.updateApplication(request); ``` ```go // Update a connection (overrides all fields) app := &descope.OutboundApp{ ID: "my-connection-id", Name: "my updated connection", // ...other fields (see Connection Schema) } clientSecret := "your-client-secret" // optional, can be nil app, err := descopeClient.Management.OutboundApplication().UpdateApplication(ctx, app, &clientSecret) if err != nil { // Handle error } ``` ```http POST /v1/mgmt/outbound/app/update Authorization: Bearer __ProjectID__: Content-Type: application/json { "app": { "id": "my-connection-id", "name": "my updated connection" // ...other fields (see [Connection Schema](#connection-schema)) } } ``` ### Delete a Connection ```js // Delete a connection by id await descopeClient.management.outboundApplication.deleteApplication('my-connection-id'); ``` ```python # Delete a connection by id descope_client.mgmt.outbound_application.delete_application( id='my-connection-id' ) ``` ```java // Delete a connection by id outboundAppsService.deleteApplication("my-connection-id"); ``` ```go // Delete a connection by id err := descopeClient.Management.OutboundApplication().DeleteApplication(ctx, "my-connection-id") if err != nil { // Handle error } ``` ```http POST /v1/mgmt/outbound/app/delete Authorization: Bearer __ProjectID__: Content-Type: application/json { "id": "my-connection-id" } ``` ### Load a Connection ```js // Load a connection by id const connection = await descopeClient.management.outboundApplication.loadApplication('my-connection-id'); ``` ```python # Load a connection by id connection = descope_client.mgmt.outbound_application.load_application( id='my-connection-id' ) ``` ```java // Load a connection by id OutboundApp connection = outboundAppsService.loadApplication("my-connection-id"); ``` ```go // Load a connection by id connection, err := descopeClient.Management.OutboundApplication().LoadApplication(ctx, "my-connection-id") if err != nil { // Handle error } ``` ```http GET /v1/mgmt/outbound/app/{id} Authorization: Bearer __ProjectID__: ``` ### Load All Connections ```js // Load all connections const connectionsRes = await descopeClient.management.outboundApplication.loadAllApplications(); connectionsRes.data.forEach((connection) => { // do something }); ``` ```python # Load all connections connections_res = descope_client.mgmt.outbound_application.load_all_applications() for connection in connections_res['apps']: # do something pass ``` ```java // Load all connections OutboundApp[] connections = outboundAppsService.loadAllApplications(); for (OutboundApp connection : connections) { // do something } ``` ```go // Load all connections connections, err := descopeClient.Management.OutboundApplication().LoadAllApplications(ctx) if err != nil { // Handle error } for _, connection := range connections { // do something } ``` ```http GET /v1/mgmt/outbound/apps Authorization: Bearer __ProjectID__: ``` ## Connection Schema When creating or updating a connection, you can configure the following fields: ### Connection Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `id` | string | Optional (create) | Unique identifier for the connection. Auto-generated if not provided during creation. | | `name` | string | Required | Display name for the connection. | | `description` | string | Optional | Human-readable description of the connection. | | `logo` | string | Optional | URL or path to the connection logo. | | `clientId` | string | Optional | OAuth client ID from the provider. Required for OAuth connections. | | `clientSecret` | string | Optional | OAuth client secret from the provider. Required for OAuth connections. | | `discoveryUrl` | string | Optional | OAuth discovery URL for automatic endpoint configuration. | | `authorizationUrl` | string | Optional | OAuth authorization endpoint URL. Required if `discoveryUrl` is not provided. | | `authorizationUrlParams` | array | Optional | Additional parameters to include in authorization requests. | | `tokenUrl` | string | Optional | OAuth token endpoint URL. Required if `discoveryUrl` is not provided. | | `tokenUrlParams` | array | Optional | Additional parameters to include in token exchange requests. | | `revocationUrl` | string | Optional | OAuth token revocation endpoint URL. | | `defaultScopes` | array of strings | Optional | Default OAuth scopes to request for all connections. | | `defaultRedirectUrl` | string | Optional | Default redirect URL after successful OAuth flow. | | `callbackDomain` | string | Optional | Domain to use for OAuth callbacks. Defaults to project domain. | | `pkce` | boolean | Optional | Enable PKCE (Proof Key for Code Exchange) for OAuth flows. | | `accessType` | string | Optional | OAuth access type (e.g., "offline" for refresh tokens). | | `prompt` | array of strings | Optional | OAuth prompt parameters (e.g., ["consent", "select_account"]). | | `appType` | string | Optional | Type of application. Will always be "custom" for manually created connections. | ### URL Parameter Structure The `authorizationUrlParams` and `tokenUrlParams` fields accept an array of URL parameter objects with the following structure: | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | Required | The parameter name. | | `value` | string | Required | The parameter value. | Here's an example of a complete connection object with all available fields: ```js const connection = { name: 'Google Contacts', description: 'Access Google Contacts API', logo: 'https://example.com/logo.png', clientId: 'your-client-id', clientSecret: 'your-client-secret', authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth', tokenUrl: 'https://oauth2.googleapis.com/token', defaultScopes: ['https://www.googleapis.com/auth/contacts.readonly'], defaultRedirectUrl: 'https://app.example.com/callback', callbackDomain: 'app.example.com', pkce: true, accessType: 'offline', prompt: ['consent'] }; ``` ```python connection = { 'name': 'Google Contacts', 'description': 'Access Google Contacts API', 'logo': 'https://example.com/logo.png', 'client_id': 'your-client-id', 'client_secret': 'your-client-secret', 'authorization_url': 'https://accounts.google.com/o/oauth2/v2/auth', 'token_url': 'https://oauth2.googleapis.com/token', 'default_scopes': ['https://www.googleapis.com/auth/contacts.readonly'], 'default_redirect_url': 'https://app.example.com/callback', 'callback_domain': 'app.example.com', 'pkce': True, 'access_type': 'offline', 'prompt': ['consent'] } ``` ```java OutboundAppRequest request = new OutboundAppRequest(); request.setName("Google Contacts"); request.setDescription("Access Google Contacts API"); request.setLogo("https://example.com/logo.png"); request.setClientId("your-client-id"); request.setClientSecret("your-client-secret"); request.setAuthorizationUrl("https://accounts.google.com/o/oauth2/v2/auth"); request.setTokenUrl("https://oauth2.googleapis.com/token"); request.setDefaultScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly")); request.setDefaultRedirectUrl("https://app.example.com/callback"); request.setCallbackDomain("app.example.com"); request.setPkce(true); request.setAccessType("offline"); request.setPrompt(Arrays.asList("consent")); ``` ```go app := &descope.OutboundApp{ Name: "Google Contacts", Description: "Access Google Contacts API", Logo: "https://example.com/logo.png", ClientID: "your-client-id", AuthorizationURL: "https://accounts.google.com/o/oauth2/v2/auth", TokenURL: "https://oauth2.googleapis.com/token", DefaultScopes: []string{"https://www.googleapis.com/auth/contacts.readonly"}, DefaultRedirectURL: "https://app.example.com/callback", CallbackDomain: "app.example.com", Pkce: true, AccessType: "offline", Prompt: []string{"consent"}, } appRequest := &descope.CreateOutboundAppRequest{ OutboundApp: *app, ClientSecret: "your-client-secret", } ``` ```json { "name": "Google Contacts", "description": "Access Google Contacts API", "logo": "https://example.com/logo.png", "clientId": "your-client-id", "clientSecret": "your-client-secret", "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth", "authorizationUrlParams": [ { "key": "hd", "value": "example.com" } ], "tokenUrl": "https://oauth2.googleapis.com/token", "tokenUrlParams": [], "defaultScopes": ["https://www.googleapis.com/auth/contacts.readonly"], "defaultRedirectUrl": "https://app.example.com/callback", "callbackDomain": "app.example.com", "pkce": true, "accessType": "offline", "prompt": ["consent"], "appType": "custom" } ``` For more information on how to store tokens and connect to your Connection, see the [Storing Connection Tokens](/agentic-identity-hub/core-components/connections/storing-connections) documentation. For more information on how to fetch these tokens see [this doc](/agentic-identity-hub/core-components/connections/fetching-connection-tokens). # Fetching Tokens (/agentic-identity-hub/core-components/connections/fetching-connection-tokens) Learn how to fetch connection tokens for users and tenants to access third-party APIs securely. # Fetching Connection Tokens Fetching a connection token is how your code pulls the actual OAuth token or API key out of the vault so it can call a third-party API. Most of the time this happens inside an [MCP server](/agentic-identity-hub/core-components/mcp-servers#token-validation--tool-execution), though your agent can also fetch tokens directly if that suits your architecture better. You fetch tokens with an SDK or the REST API. Before you pick a method, decide how you will authenticate the request. That choice controls whether your policies apply and which vaulted tokens you are allowed to read, so it is worth settling first. The next section covers the two options. ## Authentication for Token Fetching Every fetch request is authenticated as `Bearer __ProjectID__:`, where the credential is either an access token or a Management Key: - Access token: `Bearer __ProjectID__:` - Management Key: `Bearer __ProjectID__:` The one you use determines both which tokens you can read and whether [policies](/agentic-identity-hub/policies) are evaluated. ### Access Token Reach for an access token whenever an agent or MCP server is acting as a real identity, whether that is a user who granted consent or an autonomous agent that belongs to a tenant. Descope runs your [policies](/agentic-identity-hub/policies) on every fetch, so you can control which clients, agents, roles, and tenants are allowed to read which connection credentials. This is the right choice in most cases. Which access token you use depends on the agent pattern: | Agent pattern | Credential to use | What it can fetch | | ------------- | ----------------- | ----------------- | | Delegated user agent (user granted consent) | The user access token the agent or MCP client already holds | That user's connection tokens, whether user-level or user-level associated with a tenant | | MCP server tool | The MCP server access token from the incoming request | Whatever connection tokens policy allows for that subject. Usually you exchange the MCP access token for the specific token the tool needs | | Autonomous agent (`client_credentials`) | The agent's own access token | Tenant-level connection tokens only, and only for tenants the agent belongs to | For a delegated or user-backed agent, you almost always exchange the user access token, or the MCP server access token derived from that session, for a vaulted connection token rather than using a Management Key. Inside an MCP server, validate the incoming MCP access token first, then use it to fetch the connection token the tool needs. Your policies decide whether that fetch is allowed. An autonomous agent signed in with [client credentials has to be associated with specific tenants](/agentic-identity-hub/core-components/clients#client-credentials), and it can only fetch tenant-level tokens for those tenants. It cannot read user-level tokens, including user-level tokens associated with a tenant. To act on a specific user's vaulted OAuth token, you need that user's access token, or a Management Key. Both variants follow the same shape. The caller presents its own access token, Descope checks the policies for that identity, and returns only the connection tokens that identity is allowed to read. This is the on-behalf-of model: the agent acts as itself or as a consenting user, and it can never reach a credential your rules do not permit for that identity. How those identities are defined is covered in [Agents](/agentic-identity-hub/core-components/agents) and [Clients and workloads](/agentic-identity-hub/core-components/clients), the rules themselves live in [Policies](/agentic-identity-hub/policies), and the token exchange an MCP server performs to turn an inbound token into a connection token is detailed in [Downstream Credential Access](/agentic-identity-hub/auth-patterns/downstream-credential-access). ### Management Key A Management Key is for cases where a privileged backend needs to read connection tokens outside of any user or agent consent path. A common example is a support agent that has to fetch a user's or tenant admin's vaulted token so it can operate as that user or tenant, similar to impersonation. A Management Key bypasses policies and grants full administrative access, so use it deliberately: | | Access token | Management Key | | - | ------------ | -------------- | | Policies enforced | Yes | No, policies are bypassed | | Typical use | Delegated agents, MCP tools, and autonomous agents fetching tenant-level keys | Support or admin tooling that needs to fetch arbitrary user or tenant tokens | | Scope of access | Limited by the token subject and your policies | Full access to connection tokens across all users and tenants | Use an access token wherever you can, and turn to a Management Key only when you specifically need privileged access that skips policy checks. The difference from the access-token model is who chooses the user. There is no user session and no consent in the request. Your backend decides which user to act as, passes that user's ID to the fetch, and Descope returns that user's vaulted token without checking policies. The agent then operates as that user rather than on behalf of them. Because this bypasses the policy checks that normally protect vaulted credentials, keep it on trusted backends only. If the agent should instead act with a user present and consenting, use the access-token model above, fetching the token [directly from the agent](#directly-from-an-agent), and see [Auth patterns](/agentic-identity-hub/auth-patterns) for how these fit together across the Agentic Identity Hub. ## Fetching with the SDKs Which SDK you use depends on whether an MCP server sits in front of the connection. Inside an MCP server, use the MCP Auth SDK for your language; it validates the incoming request and hands your tool a connection token. When an agent talks to services directly with no MCP server in between, use the Agent Auth SDK. Both enforce your [policies](/agentic-identity-hub/policies) on the fetch. The [management SDK and REST methods](#token-fetching-methods) below still work and remain the right tool for privileged or backend fetches. ### Inside an MCP Server Protect the MCP server with the [MCP Auth SDKs](/mcp/sdks), which validate the incoming access token and expose a helper to fetch a connection token from inside a tool handler. Policies are enforced when you pass the request's access token. The example below fetches a user's Google contacts: ```python import requests from descope_mcp import get_connection_token def get_contacts(user_id, mcp_access_token): # Fetch the Google connection token; passing the MCP access token enforces policies token = get_connection_token( user_id=user_id, app_id="google-contacts", access_token=mcp_access_token, ) # Call the Google People API with the fresh, scoped token response = requests.get( "https://people.googleapis.com/v1/people/me/connections", headers={"Authorization": f"Bearer {token}"}, params={"personFields": "names,emailAddresses", "pageSize": 100}, ) response.raise_for_status() connections = response.json().get("connections", []) return { "contacts": [ { "name": c.get("names", [{}])[0].get("displayName", ""), "email": c.get("emailAddresses", [{}])[0].get("value", ""), } for c in connections ] } ``` ```ts import { defineTool } from '@descope/mcp-express'; import { z } from 'zod'; // The SDK validates the incoming token; extra.getOutboundToken fetches from Connections const getContacts = defineTool({ name: 'get_contacts', description: "Fetch the user's Google contacts", input: {}, handler: async (_args, extra) => { const token = await extra.getOutboundToken('google-contacts'); const res = await fetch( 'https://people.googleapis.com/v1/people/me/connections?personFields=names,emailAddresses&pageSize=100', { headers: { Authorization: `Bearer ${token}` } }, ); if (!res.ok) throw new Error(`Google API error: ${res.status}`); const { connections = [] } = await res.json(); const contacts = connections.map((c) => ({ name: c.names?.[0]?.displayName ?? '', email: c.emailAddresses?.[0]?.value ?? '', })); return { content: [{ type: 'text', text: JSON.stringify({ contacts }) }] }; }, }); ``` ### Directly from an Agent When an agent connects to services directly, with no MCP server in between, use the [Agent Auth SDK](/agentic-identity-hub/agent-auth-sdk) (Python and TypeScript). It signs the agent in to Descope and fetches the tokens its tools need. You bind it to the user's JWT from your app's login, call `connections.get_token`, and it returns a fresh, policy-checked token. If the user has not linked the account yet, it raises `ConnectionAuthorizationRequired`, which carries a connect URL to send them through. ```python from descope_agent_auth import AgentAuthClient, AccessTokenProvider from descope_agent_auth.errors import ConnectionAuthorizationRequired client = AgentAuthClient( project_id="__ProjectID__", credential=AccessTokenProvider(access_token=user_jwt), # the JWT from your app's login ) try: google = client.connections.get_token(connection="google-contacts", identifier=user_id) use(google.access_token) # a fresh, scoped Google token except ConnectionAuthorizationRequired as e: redirect_user_to(e.connect_url) # the user hasn't linked Google yet ``` ```ts import { AgentAuthClient, AccessTokenProvider, ConnectionAuthorizationRequired } from '@descope/agent-auth'; const client = new AgentAuthClient({ projectId: '__ProjectID__', credential: new AccessTokenProvider({ accessToken: userJwt }), // the JWT from your app's login }); try { const google = await client.connections.getToken({ connection: 'google-contacts', identifier: userId }); use(google.accessToken); // a fresh, scoped Google token } catch (e) { if (e instanceof ConnectionAuthorizationRequired) { redirectUserTo(e.connectUrl); // the user hasn't linked Google yet } else { throw e; } } ``` Direct access fits when your app already authenticates users and can hand the agent a user JWT, the agent runs in a backend you control, and you want the shortest path from a request to a provider call. Be aware that it hands the agent the provider credential itself, and those tokens and API keys are often long-lived. For most cases it is better to put a [Resource](/resources), commonly an MCP server, between the agent and the vault: the agent holds only a short-lived Descope token, and the exchange for the Connection token happens inside the Resource, so the provider credential never reaches the agent. See [Keep credentials off the agent](/resources#keep-credentials-off-the-agent). ### Key Points 1. **Prefer an access token**: Passing the validated MCP or user access token enforces [policies](/agentic-identity-hub/policies) on every fetch. A Management Key bypasses policies and should be reserved for privileged support-style access. 2. **Subject drives what you can fetch**: A delegated user token can fetch that user's Connection tokens. An autonomous (`client_credentials`) agent can only fetch tenant-level tokens for tenants it is associated with, not user-level tokens. 3. **Handle the unconnected case**: When the user has not linked the account, the Agent Auth SDK raises `ConnectionAuthorizationRequired` with a connect URL. Send the user through it, then retry. 4. **Do not cache tokens**: Fetch whenever a tool runs. Descope refreshes vaulted tokens, so a fresh fetch always returns a valid credential. For more detailed examples and AI agent implementations, see our [Examples Guide](/identity-federation/outbound-apps/examples). ## Token Fetching Methods For Python-based MCP servers, use the [Python MCP SDK](/mcp/sdks/python#connection-tokens) which provides a simplified interface for fetching connection tokens with automatic token validation and policy enforcement. These are all of the methods you can use to fetch connection tokens. The `tenantId` parameter on the user token endpoints is only for [user-level tokens associated with a tenant](/agentic-identity-hub/core-components/connections/multi-tenancy#2-user-level-tokens-associated-with-a-tenant). If you pass a `tenantId` but the user's token has no tenant association, the fetch returns `404 Token not found`. To fetch a plain user-level token, omit `tenantId` entirely. True tenant-level tokens, such as the ones you [add directly in the Console](/agentic-identity-hub/core-components/connections/storing-connections#method-4-add-tenant-token-via-the-descope-console), are shared by a whole tenant and are fetched from a [separate endpoint](#fetching-tenant-level-tokens), not these user token endpoints. ### Fetch Latest User Token This method is recommended when you don't know the exact scopes or want the most recent valid token for the user, regardless of scopes. ```js // Fetch latest user token const latestUserToken = await descopeClient.management.outboundApplication.fetchToken( 'my-app-id', 'user-id', 'tenant-id', // optional { forceRefresh: false, withRefreshToken: false } // optional ); ``` ```python # Fetch latest user token latest_user_token = descope_client.mgmt.outbound_application.fetch_token( app_id='my-app-id', user_id='user-id', tenant_id='tenant-id', # optional options={ 'forceRefresh': False, # optional 'withRefreshToken': False # optional } ) ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch latest token FetchLatestOutboundAppUserTokenRequest request = new FetchLatestOutboundAppUserTokenRequest(); request.setAppId("google-contacts"); request.setUserId("user-123"); request.setTenantId("tenant-id"); // optional FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchLatestOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch latest user token token, err := descopeClient.Management.OutboundApplication().FetchLatestUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "my-app-id", UserID: "user-id", TenantID: "tenant-id", // optional }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch latest user token $response = $descopeSDK->management->outboundApps->fetchLatestUserToken([ 'appId' => 'my-app-id', 'userId' => 'user-id', 'tenantId' => 'tenant-id', // optional ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "userId": "xxxxx", "tenantId": "optional-tenant-id", "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the connection. * **`userId`** (required): The user ID for whom to fetch the token. * **`tenantId`** (optional): Only for a user-level token that was stored with a tenant association, when a user has separate tokens across different tenants. Passing it for a token that has no tenant association returns `404 Token not found`, so omit it for a plain user-level token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ### Fetch Token with Specific Scopes Use this method when you need a token with specific scopes. **Important**: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a `404 Token not found` error. ```js // Fetch user token with specific scopes const userToken = await descopeClient.management.outboundApplication.fetchTokenByScopes( 'my-app-id', 'user-id', ['read', 'write'], { withRefreshToken: false }, // optional 'tenant-id' // optional ); ``` ```python # Fetch user token with specific scopes user_token = descope_client.mgmt.outbound_application.fetch_token_by_scopes( app_id='my-app-id', user_id='user-id', scopes=['read', 'write'], options={ 'withRefreshToken': False # optional }, tenant_id='tenant-id' # optional ) ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch token with specific scopes FetchOutboundAppUserTokenRequest request = new FetchOutboundAppUserTokenRequest(); request.setAppId("google-contacts"); request.setUserId("user-123"); request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly")); request.setTenantId("tenant-id"); // optional FetchOutboundAppUserTokenResponse response = outboundAppsService.fetchOutboundAppUserToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch user token with specific scopes request := &descope.FetchOutboundAppUserTokenRequest{ AppID: "my-app-id", UserID: "user-id", Scopes: []string{"read", "write"}, TenantID: "tenant-id", // optional Options: &descope.OutboundAppUserTokenOptions{ WithRefreshToken: false, // optional }, } token, err := descopeClient.Management.OutboundApplication().FetchUserToken(ctx, request) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch user token with specific scopes $response = $descopeSDK->management->outboundApps->fetchUserToken( 'my-app-id', 'user-id', ['read', 'write'], false, // withRefreshToken, optional false, // forceRefresh, optional 'tenant-id' // optional ); $accessToken = $response['token']['accessToken']; ``` ```ruby # Fetch user token with specific scopes result = client.fetch_outbound_app_user_token( app_id: 'my-app-id', user_id: 'user-id', scopes: %w[read write], with_refresh_token: false, # optional tenant_id: 'tenant-id' # optional ) access_token = result['token']['accessToken'] ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/user/token" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "userId": "xxxxx", "tenantId": "optional-tenant-id", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ], "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the connection. * **`userId`** (required): The user ID for whom to fetch the token. * **`tenantId`** (optional): Only for a user-level token that was stored with a tenant association, when a user has separate tokens across different tenants. Passing it for a token that has no tenant association returns `404 Token not found`, so omit it for a plain user-level token. * **`scopes`** (required): An array of exact scopes that match the original token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ## Fetching Tenant-Level Tokens In addition to user-specific tokens, you can also fetch tenant-level tokens for connections. These are useful when you need to access APIs on behalf of a tenant rather than a specific user. These are **true tenant-level tokens**, shared across the tenant's users, for example the ones you [add directly in the Console](/agentic-identity-hub/core-components/connections/storing-connections#method-4-add-tenant-token-via-the-descope-console). They are fetched from the dedicated endpoints below. Do not confuse them with [user-level tokens associated with a tenant](/agentic-identity-hub/core-components/connections/multi-tenancy#2-user-level-tokens-associated-with-a-tenant), which are still fetched from the user token endpoints above. An autonomous agent authenticated with `client_credentials` can fetch **tenant-level** tokens only, and only for tenants it is associated with. It cannot fetch user-level Connection tokens. See [Authentication for Token Fetching](#authentication-for-token-fetching). Descope provides the same two methods for tenant-level connection tokens as user-level tokens. Depending on whether you know the exact scopes of the token you need, you can use the following methods: ### Fetch Latest Tenant Token This method is recommended when you don't know the exact scopes or want the most recent valid token for the tenant, regardless of scopes. ```js // Fetch latest tenant token const latestTenantToken = await descopeClient.management.outboundApplication.fetchTenantToken( 'my-app-id', 'tenant-id', { forceRefresh: false } // optional ); ``` ```python # Fetch latest tenant token latest_tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token( app_id='my-app-id', tenant_id='tenant-id', options={ 'forceRefresh': False, # optional 'withRefreshToken': False # optional } ) ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch latest tenant token FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest(); request.setAppId("google-contacts"); request.setTenantId("tenant-123"); FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchLatestOutboundAppTenantToken(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch latest tenant token token, err := descopeClient.Management.OutboundApplication().FetchLatestTenantToken(ctx, &descope.FetchOutboundAppTenantTokenRequest{ AppID: "my-app-id", TenantID: "tenant-123", }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch latest tenant token $response = $descopeSDK->management->outboundApps->fetchLatestTenantToken([ 'appId' => 'my-app-id', 'tenantId' => 'tenant-123', ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/token/latest" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "tenantId": "tenant-123", "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the connection. * **`tenantId`** (required): The tenant ID if you're fetching a tenant-level token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ### Fetch Tenant Token with Specific Scopes Use this method when you need a tenant token with specific scopes. **Important**: You must provide the exact scopes that were used when the token was created. Otherwise, you'll receive a `404 Token not found` error. ```js // Fetch tenant token with specific scopes const tenantToken = await descopeClient.management.outboundApplication.fetchTenantTokenByScopes( 'my-app-id', 'tenant-id', ['read', 'write'], { withRefreshToken: false } // optional ); ``` ```python # Fetch tenant token with specific scopes tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token_by_scopes( app_id='my-app-id', tenant_id='tenant-id', scopes=['read', 'write'], options={ 'withRefreshToken': False # optional } ) ``` ```java import com.descope.client.Config; import com.descope.client.DescopeClient; import com.descope.sdk.mgmt.OutboundAppsService; // Initialize the Descope client DescopeClient descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("YOUR_MANAGEMENT_KEY") .build()); OutboundAppsService outboundAppsService = descopeClient.getManagementServices().getOutboundAppsService(); // Fetch tenant token with specific scopes FetchOutboundAppTenantTokenRequest request = new FetchOutboundAppTenantTokenRequest(); request.setAppId("google-contacts"); request.setTenantId("tenant-123"); request.setScopes(Arrays.asList("https://www.googleapis.com/auth/contacts.readonly")); FetchOutboundAppTenantTokenResponse response = outboundAppsService.fetchOutboundAppTenantTokenByScopes(request); String accessToken = response.getToken().getAccessToken(); ``` ```go // Fetch tenant token with specific scopes token, err := descopeClient.Management.OutboundApplication().FetchTenantToken(ctx, &descope.FetchOutboundAppTenantTokenRequest{ AppID: "my-app-id", TenantID: "tenant-123", Scopes: []string{"read", "write"}, }) if err != nil { // Handle error } accessToken := token.AccessToken ``` ```php // Fetch tenant token with specific scopes $response = $descopeSDK->management->outboundApps->fetchTenantToken([ 'appId' => 'my-app-id', 'tenantId' => 'tenant-123', 'scopes' => ['read', 'write'], ]); $accessToken = $response['token']['accessToken']; ``` ```bash curl -X POST "__BaseURL__/v1/mgmt/outbound/app/tenant/token" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer or " \ -d '{ "appId": "google-contacts", "tenantId": "tenant-123", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ], "options": { "withRefreshToken": false, "forceRefresh": false } }' ``` #### Request Parameters * **`appId`** (required): The ID of the connection. * **`tenantId`** (required): The tenant ID if you're fetching a tenant-level token. * **`scopes`** (required): An array of exact scopes that match the original token. * **`options`** (optional): Additional options for token fetching. * **`withRefreshToken`**: Defaults to **false**. Set this to **true** to include the refresh token in the response. * **`forceRefresh`**: Defaults to **false**. The API will return a refreshed token regardless of this value, but this will force our service to refresh the token on the client's behalf. ## Connection Token Response The `refresh_token` will not be returned unless `withRefreshToken` is set to **true** in the request. The response will include the user/tenant token details, similar to the example below: ```json { "token": { "id": "xxxx", "appId": "google-contacts", "userId": "xxxx", "tokenSub": "", "accessToken": "ya29.xxxx", "accessTokenType": "Bearer", "accessTokenExpiry": "1741107113", "hasRefreshToken": true, "refreshToken": "xxxx", "lastRefreshTime": "1741103514", "lastRefreshError": "", "scopes": [ "https://www.googleapis.com/auth/contacts.readonly" ] } } ``` ## Error Handling When working with connection tokens, you may encounter different types of errors. Here's what each error code means and how to handle them: ### Common Error Codes | Status Code | Meaning | Common Causes | |-------------|---------|---------------| | **401** | Unauthorized | Invalid Management Key, access token, or project ID | | **403** | Forbidden | Policy denied the fetch, insufficient permissions, or invalid tenant access for an autonomous agent | | **404** | Token not found | User never connected to the connection, token was cleared, or wrong scopes provided | | **500** | Server error | Invalid HTTP method (not POST) or malformed JSON payload | ### Error Handling Example ```js // Fetch a connection token with proper error handling. async function fetchConnectionToken(appId, userId, scopes = null) { const url = scopes ? "__BaseURL__/v1/mgmt/outbound/app/user/token" // specific scopes : "__BaseURL__/v1/mgmt/outbound/app/user/token/latest"; // latest token const body = scopes ? { appId, userId, scopes } : { appId, userId }; try { const response = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${PROJECT_ID}:${MANAGEMENT_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); if (!response.ok) { if (response.status === 404) { // Token not found: either never existed or was cleared recently console.log("Token not found. The user may not have connected to this connection, or the token may have been cleared."); } else if (response.status === 500) { // Server error: issue with HTTP request method or JSON payload console.log("Server error. Check your request method (should be POST) and ensure your JSON payload is properly formatted."); } else { console.log(`HTTP error occurred: ${response.status}`); } return null; } return await response.json(); } catch (e) { console.log(`Request failed: ${e.message}`); return null; } } // Make a request to a third-party API with proper error handling. async function makeApiRequest(accessToken, url, params = {}) { const query = new URLSearchParams(params).toString(); try { const response = await fetch(query ? `${url}?${query}` : url, { headers: { Authorization: `Bearer ${accessToken}` }, }); if (!response.ok) { if (response.status === 401) console.log("Access token may be invalid or expired"); else if (response.status === 403) console.log("Insufficient permissions for this request"); else console.log(`HTTP error occurred: ${response.status}`); return null; } return await response.json(); } catch (e) { console.log(`Request failed: ${e.message}`); return null; } } ``` ```python import requests from requests.exceptions import RequestException def fetch_connection_token(app_id, user_id, scopes=None): """Fetch a connection token with proper error handling.""" headers = {"Authorization": f"Bearer {PROJECT_ID}:{MANAGEMENT_KEY}"} try: if scopes: # Use specific scopes endpoint url = "__BaseURL__/v1/mgmt/outbound/app/user/token" data = {"appId": app_id, "userId": user_id, "scopes": scopes} else: # Use latest token endpoint url = "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" data = {"appId": app_id, "userId": user_id} response = requests.post(url, headers=headers, json=data, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if response.status_code == 404: # Token not found - either never existed or was cleared recently print("Token not found. The user may not have connected to this connection, " "or the token may have been cleared.") return None elif response.status_code == 500: # Server error - issue with HTTP request method or JSON payload print("Server error. Check your request method (should be POST) " "and ensure your JSON payload is properly formatted.") return None else: print(f"HTTP error occurred: {e}") return None except requests.exceptions.Timeout: print("Request timed out") return None except RequestException as e: print(f"Request failed: {e}") return None def make_api_request(access_token, url, params=None): """Make a request to a third-party API with proper error handling.""" headers = {"Authorization": f"Bearer {access_token}"} try: response = requests.get(url, headers=headers, params=params, timeout=30) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if response.status_code == 401: print("Access token may be invalid or expired") elif response.status_code == 403: print("Insufficient permissions for this request") else: print(f"HTTP error occurred: {e}") return None except requests.exceptions.Timeout: print("Request timed out") return None except RequestException as e: print(f"Request failed: {e}") return None ``` ```go package main import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "time" ) var httpClient = &http.Client{Timeout: 30 * time.Second} // fetchConnectionToken fetches a connection token with proper error handling. func fetchConnectionToken(appID, userID string, scopes []string) (map[string]interface{}, error) { endpoint := "__BaseURL__/v1/mgmt/outbound/app/user/token/latest" // latest token payload := map[string]interface{}{"appId": appID, "userId": userID} if len(scopes) > 0 { endpoint = "__BaseURL__/v1/mgmt/outbound/app/user/token" // specific scopes payload["scopes"] = scopes } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s:%s", projectID, managementKey)) req.Header.Set("Content-Type", "application/json") resp, err := httpClient.Do(req) if err != nil { fmt.Println("Request failed:", err) return nil, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result, nil case http.StatusNotFound: // Token not found: either never existed or was cleared recently fmt.Println("Token not found. The user may not have connected to this connection, or the token may have been cleared.") case http.StatusInternalServerError: // Server error: issue with HTTP request method or JSON payload fmt.Println("Server error. Check your request method (should be POST) and ensure your JSON payload is properly formatted.") default: fmt.Printf("HTTP error occurred: %d\n", resp.StatusCode) } return nil, nil } // makeAPIRequest calls a third-party API with proper error handling. func makeAPIRequest(accessToken, endpoint string, params map[string]string) (map[string]interface{}, error) { q := url.Values{} for k, v := range params { q.Set(k, v) } full := endpoint if len(q) > 0 { full = endpoint + "?" + q.Encode() } req, _ := http.NewRequest("GET", full, nil) req.Header.Set("Authorization", "Bearer "+accessToken) resp, err := httpClient.Do(req) if err != nil { fmt.Println("Request failed:", err) return nil, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result, nil case http.StatusUnauthorized: fmt.Println("Access token may be invalid or expired") case http.StatusForbidden: fmt.Println("Insufficient permissions for this request") default: fmt.Printf("HTTP error occurred: %d\n", resp.StatusCode) } return nil, nil } ``` ## Viewing and Managing Tokens in the Console After connecting users to a connection, you can view and manage their tokens directly in the Descope Console under the **Token Management** tab for your connection. ![Token Management dashboard in the Descope Console](/assets/outbound-app-dashboard.webp) For each user or tenant-level token, you can: - **View the access token** (and refresh token, if applicable) - **Manually refresh the access token** - **Delete the token** You can delete your pre-existing tokens programatically as well with these [functions](/agentic-identity-hub/core-components/connections/storing-connections#deleting-connection-tokens). This provides a convenient way to audit, troubleshoot, or revoke access for specific users or tenants without writing any code. # Connections (/agentic-identity-hub/core-components/connections) Discover how Descope's Connections enable seamless integration with third-party platforms, enhancing user experiences with additional OAuth consents. # Connections Connections and [Outbound Apps](/identity-federation/outbound-apps) provide the same capability (a token vault for OAuth tokens and API keys at the user or tenant level, with Descope managing storage and refresh), but they are **separate objects in the Console**. Connections live under **Agentic Identity Hub → Connections**; Outbound Apps live under **Connect → Outbound Apps**. A Connection you create here does not appear under Outbound Apps, and vice versa. Use **Connections** when building agents and MCP servers; use **Outbound Apps** for vaulting third-party tokens in a traditional application. The [Connections](https://app.descope.com/agentic-hub/connections) page is where you create and manage the credential vault your agents use to reach third-party services. From here you can: - **Create connections** to OAuth providers (Google, Slack, GitHub, and others) or API-key based services, so agents can retrieve credentials at runtime without holding long-lived secrets - **View all connections** in your project and inspect stored tokens and their status - **Manage tokens**: view, manually refresh, or delete stored access tokens and API keys per user or tenant Connections work across both MCP server and direct agent integration patterns. Any agent that needs to call a third-party API retrieves its credential from Connections at the moment of use rather than from a config file or environment variable. Connections support both **OAuth providers** (for interactive login and consent) and **API-key based services** (for machine or user-supplied secrets). Your MCP server retrieves these credentials at runtime using Descope SDKs or APIs, ensuring external tokens are managed centrally and never hardcoded in your infrastructure. If you're building AI agents or MCP servers that need access to external services (e.g., Google Calendar, Salesforce), Connections handle secure storage, token refresh, and access control. Your tools can reliably retrieve user/tenant-scoped OAuth tokens or API keys whenever they need to call third-party APIs. An agent *can* fetch a Connection token directly, but the recommended pattern puts a [Resource](/resources), an MCP server or gateway, between the agent and the vault. The agent authenticates to the Resource; the Resource pulls the API key or external OAuth token from Connections at runtime and calls the downstream service. That way long-lived third-party secrets are never delivered to the agent. ## How It Works 1. **[Create a Connection](/agentic-identity-hub/core-components/connections/create-connections)** - Choose a preconfigured OAuth provider, or create a custom connection for any service (including API key storage). 2. **[Storing Tokens](/agentic-identity-hub/core-components/connections/storing-connections)** - Use Descope SDKs or APIs to connect users/tenants to OAuth providers or to collect and store API keys. 3. **[Fetching Tokens](/agentic-identity-hub/core-components/connections/fetching-connection-tokens)** - Descope securely stores and manages all tokens and API keys, making them available for backend or agent use. ### Creating a Connection To set up a connection in Descope, follow the steps in our [Creating a Connection](/agentic-identity-hub/core-components/connections/create-connections) guide. ### Storing Connection Tokens You can only add **tenant-level** tokens directly within the Descope Console. Creating a Connection only defines the provider. To vault a credential, you run a user or tenant through the provider's OAuth consent once (or collect an API key). There are four ways to do this, covered in detail in [Storing Connection Tokens](/agentic-identity-hub/core-components/connections/storing-connections): - **[Backend Connect API](/agentic-identity-hub/core-components/connections/storing-connections#method-1-backend-connect-api)**: Your server obtains a connection URL and redirects the user through consent. This powers Adaptive Connect for MCP servers (returning a connect URL to the client for URL elicitation) and lets web apps build their own connect UI. - **[Descope Flows](/agentic-identity-hub/core-components/connections/storing-connections#method-2-descope-flows)**: Connect during original authentication / consent so the token is vaulted before the agent needs it — ideal inside your MCP server's consent flow. - **[Outbound App Widget](/agentic-identity-hub/core-components/connections/storing-connections#method-3-outbound-app-widget)**: Out-of-band connect when users sign into your platform; the agent fetches the vaulted token later when a tool runs. - **[Descope Console](/agentic-identity-hub/core-components/connections/storing-connections#method-4-add-tenant-token-via-the-descope-console)**: Paste a **tenant-level API key** in the Console (Token tab → **Add Tenant Token**). Intended for Descopers vaulting organization-owned keys, typically against a tenant for your own company with workforce SSO. #### Token Management Refresh token expiration is not available for all providers. Certain providers, like Instagram, have a fixed refresh token expiration time, and cannot be changed. Once you have tokens stored with Descope, associated with your Connection, you can view them from within the token management tab. These are all of the things you can view for each token: - **ID**: System-generated ID pairing that user's consent to the application. - **App ID**: The configured application ID which coincides with the token ID. - **Associated User**: The user ID of the user who's associated with the consent. - **Scopes**: The consented scopes correlate to the user's consent to the application. - **Access Token Expiration**: Expiration of the current access token for the user's consent. - **Refresh Token**: Boolean indicating whether a refresh token is available. - **Refresh Token Expiration**: Expiration of the current refresh token for the user's consent. - **Last Refreshed**: The last time the user's access token was refreshed. - **Last Refresh Error**: If applicable, the last error encountered while trying to refresh the user's access token. - **Token Subject**: The user reference on the provider side. For this example, it is associated with the unique user ID of the user's Google account. - **Access Token Type**: Specifies the format or method the access token uses, such as Bearer or MAC, which determines how it is used for authentication and authorization. - **Tenant ID**: The tenant ID of the tenant associated with the consent. ![Viewing a connection's token management in Descope](/assets/outbound-app-token-management.webp) You can also view the tokens in plain text, manually refresh any of the tokens (if OAuth-based), or delete any tokens from this tab. You can also remove tokens using our SDKs, documented [here](/agentic-identity-hub/core-components/connections/storing-connections#deleting-connection-tokens). ### Fetching Your Connection Tokens Once your users/tenants are connected and have tokens stored in our Connections vault, you can start [fetching the tokens](/agentic-identity-hub/core-components/connections/fetching-connection-tokens) to access third-party APIs within your MCP server or with your agents. # Multi-Tenancy with Connections (/agentic-identity-hub/core-components/connections/multi-tenancy) Learn how user and tenant-scoped connection tokens work, including tenant-associated user tokens and tenant-level shared tokens. # Multi-Tenancy with Connections When using Connections with MCP Auth, token storage and retrieval behavior depends on how the token is scoped to a user and/or tenant. ## Token Scope Models Connections support three token scope models: 1. **User-level tokens** 2. **User-level tokens associated with a tenant** 3. **Tenant-level tokens** ## 1) User-Level Tokens User-level tokens are stored for a specific user within a connection, with no tenant involved. - A user can hold multiple tokens for the same connection, told apart by their scope set or by an external identifier you set on each token. - Storing a token again for the same user with the same scopes and external identifier replaces the existing one. - Retrieval is done via the user token fetch endpoints. T1 U --> T2`} /> The two tokens above belong to the same user on the same connection. Nothing about a tenant tells them apart, only their scopes or external identifiers do. ## 2) User-Level Tokens Associated with a Tenant These tokens are also fetched via the user token fetch endpoints, but include tenant association. - They allow storing multiple tokens for the same user across multiple tenants within the same connection. - Tokens are treated as separate records based on tenant association. - Scope overlap does not collapse tenant-specific tokens; tenant association keeps them distinct. This model is useful when the same person belongs to multiple customer tenants and needs tenant-specific external credentials. |"acting in Acme"| T1 U -->|"acting in Globex"| T2`} /> Same user, same connection, but here the tenant association is what keeps the two tokens distinct, even if their scopes are identical. ## 3) Tenant-Level Tokens Tenant-level tokens are scoped to a tenant and shared among its users. - These tokens are typically used by multiple users in the same tenant. - Access is controlled by tenant context and authorization (for example, a tenant role such as Tenant Admin). - Retrieval is done via tenant-scoped token fetch patterns. T B --> T C --> T`} /> One token belongs to the tenant, not to any user. Every user in Acme fetches the same shared credential. ### Internal Workforce Agents Tenant-level tokens are not only for customer tenants. If you use Descope to manage your own internal workforce agents, you typically create a tenant that represents your company and store the organization's tokens at the tenant level within it. The tenant is internal rather than a customer, but the mechanism is identical, so it still gives you a place to vault organization-wide credentials in Descope for your internal agents to use. ## Choosing the Right Model - Use **user-level** tokens when credentials are personal and not tenant-specific. - Use **user-level + tenant** tokens when a user operates in multiple tenants and each tenant should have separate external credentials. - Use **tenant-level** tokens for shared tenant integrations managed by tenant administrators. # Storing Tokens (/agentic-identity-hub/core-components/connections/storing-connections) Learn the four ways to store user and tenant tokens for Connections, and when to use each one. # Storing Connection Tokens [Creating a Connection](/agentic-identity-hub/core-components/connections/create-connections) only defines the provider and its settings. The token vault starts empty. Storing a token means running a user or tenant through the provider's OAuth consent once (or collecting an API key), so that Descope can hold the credential and refresh it for you. After that, your MCP server or backend can fetch a fresh token whenever it needs to call the third-party API. This page covers the four ways to store tokens and when to reach for each. For retrieving tokens after they are stored, see [Fetching Connection Tokens](/agentic-identity-hub/core-components/connections/fetching-connection-tokens). A Connection can store multiple tokens for the same user or tenant, each with different scopes. Before storing anything, make sure you have [created a Connection](/agentic-identity-hub/core-components/connections/create-connections). ## Four Ways to Store Tokens There are four ways to vault a credential against a user or tenant. The first three differ mainly in where the connection is initiated and whether you need backend code. The fourth is Console-only and is limited to **tenant-level API keys**. | Method | Where it runs | Backend route needed | Scopes | Best for | | ------ | ------------- | -------------------- | ------ | -------- | | [Backend Connect API](#method-1-backend-connect-api) | Your server | Yes | Custom or default | MCP servers using Adaptive Connect with URL elicitation, or web apps where you want to build your own connect UI | | [Descope Flow](#method-2-descope-flows) | Descope Flow (customer-hosted / [Descope-hosted](/identity-federation/auth-hosting)) | No | Custom or default | Connecting at original auth / consent time (token vaulted before the agent needs it) | | [Outbound App Widget](#method-3-outbound-app-widget) | Your frontend | No | Default only | Out-of-band connect when users sign into your platform; agent fetches later | | [Console (Add Tenant Token)](#method-4-add-tenant-token-via-the-descope-console) | Descope Console | No | N/A (API keys) | Descopers storing **tenant-level API keys** for their own organization | Methods 1–3 end the same way: the user completes the provider's consent screen (or submits an API key in a flow/widget), and Descope vaults the credential against that user or tenant. Method 4 skips consent entirely — you paste a tenant-level API key in the Console. ## Method 1: Backend Connect API Use this method when your own server drives the connection. It fits two common cases: - An **MCP server** that discovers mid-request that it does not yet have a token for a tool, and returns a connection URL to the client so the user can grant access. This is [Adaptive Connect](#adaptive-connect-for-mcp-servers) with URL elicitation. - A **web app** where you want your own Connect button and connection screens, rather than Descope's widget or a hosted flow. ### How It Works The connect endpoint takes the user's access token and the target connection's `appId`, and returns a Descope-hosted authorization URL. Your frontend never calls Descope's connect endpoint directly. Instead, it hands the access token to a backend route that you expose, and that route makes the call: 1. Your frontend sends the user's access token to a backend route you expose. 2. Your backend calls the Descope connect endpoint with that token and the connection's `appId`. 3. Descope returns a `url`, which is the provider's authorization URL. 4. Your backend returns a redirect to that `url`. 5. The user completes consent at the provider and is sent to your `redirectUrl`. 6. Descope vaults the access and refresh tokens for that user. ### The Connect Endpoint Your backend route calls the connect endpoint to obtain the authorization URL. ```http POST /v1/mgmt/outbound/app/connect Authorization: Bearer __ProjectID__: Content-Type: application/json { "appId": "google-contacts", "options": { "redirectUrl": "https://your-app.com/connection-complete" } } ``` **Response:** ```json { "url": "__BaseURL__/v1/outbound/oauth/connect?appId=google-contacts&..." } ``` ```js // Request a connection URL const response = await fetch('__BaseURL__/v1/mgmt/outbound/app/connect', { method: 'POST', headers: { 'Authorization': `Bearer ${PROJECT_ID}:${MCP_ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ appId: 'google-contacts', options: { redirectUrl: 'https://your-app.com/connection-complete' } }) }); const { url } = await response.json(); // Return this URL to the MCP client ``` ```python import requests # Request a connection URL response = requests.post( '__BaseURL__/v1/mgmt/outbound/app/connect', headers={ 'Authorization': f'Bearer {PROJECT_ID}:{MCP_ACCESS_TOKEN}', 'Content-Type': 'application/json' }, json={ 'appId': 'google-contacts', 'options': { 'redirectUrl': 'https://your-app.com/connection-complete' } } ) connection_url = response.json().get('url') # Return this URL to the MCP client ``` ```go import ( "bytes" "encoding/json" "net/http" ) // Request a connection URL requestBody := map[string]interface{}{ "appId": "google-contacts", "options": map[string]interface{}{ "redirectUrl": "https://your-app.com/connection-complete", }, } jsonData, _ := json.Marshal(requestBody) req, _ := http.NewRequest("POST", "__BaseURL__/v1/mgmt/outbound/app/connect", bytes.NewBuffer(jsonData)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s:%s", projectID, mcpAccessToken)) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) // Handle response and extract connection URL ``` #### Request Parameters * **`appId`** (required): The ID of the connection you want to connect to. * **`tenantId`** (optional): Associates the stored token with a tenant. See [User-level tokens with tenant association](#user-level-tokens-with-tenant-association). * **`options`** (optional): Additional options for the connection request. * **`redirectUrl`** (required): The URL where the user is redirected after completing the OAuth flow. This must be a valid URL that your application can handle. #### Response The endpoint returns a JSON object with a `url` field containing the OAuth authorization URL that the user should be redirected to: ```json { "url": "__BaseURL__/v1/outbound/oauth/connect?appId=google-contacts&..." } ``` ### Adaptive Connect for MCP Servers Adaptive Connect is the MCP-specific application of this method. When a tool needs an external OAuth token that the user has not yet granted, your server returns a connection URL to the MCP client instead of failing. The client presents that URL to the user (URL elicitation), the user grants access, and the tool runs on retry. Adaptive Connect is designed for MCP servers where tools request connections on demand. For a web app connecting users at login instead, use [Descope Flows](#method-2-descope-flows). When a tool requires a token that does not exist or lacks the required scopes, your server: 1. **Detects the missing token** when attempting to fetch a connection token. 2. **Requests a connection URL** from Descope. 3. **Returns the connection URL** to the MCP client in a structured response. 4. **Lets the user grant permissions** through the OAuth flow. 5. **Retries the tool** after the connection is established. This is the same connect call as [Method 1](#method-1-backend-connect-api), with two differences: your **MCP server** plays the role of the backend route, and the **MCP client** (Claude, ChatGPT, and similar) plays the role of the frontend. Instead of redirecting the browser, the server hands the URL back to the client, which surfaces it to the user for consent. The simplest pattern is to catch the token fetch failure and return a connection URL in the error response: ```python from descope import DescopeClient import requests descope_client = DescopeClient(project_id="__ProjectID__") def handle_mcp_tool(user_id, tool_name, tool_params): """ Handle an MCP tool request with Adaptive Connect fallback. """ # Step 1: Attempt to fetch the connection token try: token_response = descope_client.mgmt.outbound_application.fetch_token( app_id="google-contacts", user_id=user_id, options={"withRefreshToken": False} ) access_token = token_response["token"]["token"] except Exception: # Step 2: Token fetch failed, request connection URL try: connect_response = requests.post( "__BaseURL__/v1/mgmt/outbound/app/connect", headers={ "Authorization": f"Bearer {PROJECT_ID}:{MCP_ACCESS_TOKEN}", "Content-Type": "application/json" }, json={ "appId": "google-contacts", "options": { "redirectUrl": "https://your-app.com/connection-complete" } }, timeout=10 ) if connect_response.status_code == 200: connection_url = connect_response.json().get("url") # Return structured response for tool elicitation return { "error": "connection_required", "requiresConnection": True, "connectionUrl": connection_url, "message": ( f"Failed to run {tool_name}, additional permissions required. " f"Please grant additional permissions using the following URL and try again: {connection_url}" ), "appId": "google-contacts" } return { "error": "connection_initiation_failed", "message": f"Failed to initiate connection: {connect_response.text}" } except Exception as connect_error: return { "error": "connection_request_failed", "message": f"Failed to request connection URL: {str(connect_error)}" } # Step 3: Token exists, execute the tool try: # Use access_token to call the third-party API # ... your tool logic here ... return { "success": True, "result": "Tool executed successfully" } except Exception as tool_error: return { "error": "tool_execution_failed", "message": str(tool_error) } ``` ```ts import DescopeClient from '@descope/node-sdk'; const descopeClient = DescopeClient({ projectId: '__ProjectID__' }); async function handleMcpTool( userId: string, toolName: string, _toolParams: unknown ) { let accessToken: string; // Step 1: Attempt to fetch the connection token try { const tokenResponse = await descopeClient.management.outboundApplication.fetchToken( 'google-contacts', userId, undefined, { withRefreshToken: false } ); accessToken = tokenResponse.token.accessToken; } catch { // Step 2: Token fetch failed, request connection URL try { const connectResponse = await fetch( '__BaseURL__/v1/mgmt/outbound/app/connect', { method: 'POST', headers: { Authorization: `Bearer ${PROJECT_ID}:${MCP_ACCESS_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ appId: 'google-contacts', options: { redirectUrl: 'https://your-app.com/connection-complete', }, }), } ); if (connectResponse.ok) { const { url: connectionUrl } = (await connectResponse.json()) as { url: string; }; // Return structured response for tool elicitation return { error: 'connection_required', requiresConnection: true, connectionUrl, message: `Failed to run ${toolName}, additional permissions required. Please grant additional permissions using the following URL and try again: ${connectionUrl}`, appId: 'google-contacts', }; } return { error: 'connection_initiation_failed', message: `Failed to initiate connection: ${await connectResponse.text()}`, }; } catch (connectError) { return { error: 'connection_request_failed', message: `Failed to request connection URL: ${String(connectError)}`, }; } } // Step 3: Token exists, execute the tool try { // Use accessToken to call the third-party API // ... your tool logic here ... return { success: true, result: 'Tool executed successfully', }; } catch (toolError) { return { error: 'tool_execution_failed', message: String(toolError), }; } } ``` ```go package main import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "github.com/descope/go-sdk/descope" "github.com/descope/go-sdk/descope/client" ) func handleMCPTool(ctx context.Context, descopeClient *client.DescopeClient, userID, toolName string) map[string]any { // Step 1: Attempt to fetch the connection token token, err := descopeClient.Management.OutboundApplication().FetchLatestUserToken(ctx, &descope.FetchOutboundAppUserTokenRequest{ AppID: "google-contacts", UserID: userID, Options: &descope.OutboundAppUserTokenOptions{ WithRefreshToken: false, }, }) if err != nil { // Step 2: Token fetch failed, request connection URL requestBody := map[string]any{ "appId": "google-contacts", "options": map[string]any{ "redirectUrl": "https://your-app.com/connection-complete", }, } jsonData, _ := json.Marshal(requestBody) req, err := http.NewRequestWithContext(ctx, http.MethodPost, "__BaseURL__/v1/mgmt/outbound/app/connect", bytes.NewBuffer(jsonData)) if err != nil { return map[string]any{ "error": "connection_request_failed", "message": fmt.Sprintf("Failed to request connection URL: %v", err), } } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s:%s", projectID, mcpAccessToken)) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return map[string]any{ "error": "connection_request_failed", "message": fmt.Sprintf("Failed to request connection URL: %v", err), } } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode == http.StatusOK { var connectResp struct { URL string `json:"url"` } _ = json.Unmarshal(body, &connectResp) // Return structured response for tool elicitation return map[string]any{ "error": "connection_required", "requiresConnection": true, "connectionUrl": connectResp.URL, "message": fmt.Sprintf( "Failed to run %s, additional permissions required. Please grant additional permissions using the following URL and try again: %s", toolName, connectResp.URL, ), "appId": "google-contacts", } } return map[string]any{ "error": "connection_initiation_failed", "message": fmt.Sprintf("Failed to initiate connection: %s", string(body)), } } accessToken := token.AccessToken // Step 3: Token exists, execute the tool _ = accessToken // Use accessToken to call the third-party API // ... your tool logic here ... return map[string]any{ "success": true, "result": "Tool executed successfully", } } ``` ## Method 2: Descope Flows Use a flow when you want Descope to host the connection experience as part of login or consent. With this method, **storing the Connection token happens when the agent or client originally authenticates** — for example in your MCP server's user consent flow — so third-party access is granted in the same session instead of out of band later through URL elicitation. With flows, the user must sign in through the flow first. The authenticated session is what Descope uses to associate the stored tokens with the right user or tenant. ### How It Works 1. The user authenticates (and optionally consents to your MCP server / app) through a Descope Flow. 2. Within that same flow, an Outbound App connect action (or API key action) runs. 3. The user completes provider consent (or enters an API key), and Descope vaults the credential against that user or tenant. 4. Later, when an agent or MCP tool needs the third-party API, your Resource fetches the token from the vault — no second connect step at tool time. A flow connects users through two building blocks: **Outbound App actions**, which do the work of connecting and storing tokens, and optional **screen components**, which give users something to interact with. You can also see how to trigger the same connections outside of flows with the SDKs or API in [Connecting Outbound Apps](/identity-federation/outbound-apps/connect#connecting-with-descope-flows). ### Screen Components Two screen components support Outbound App connections. A screen component only renders the UI. You still add the matching action after it for the connection to happen, the same as any button in a flow. - **Outbound App button**: a button that starts a connection when the user clicks it. You set which outbound app it targets in one of two ways. A **Dynamic Application** takes the app to connect from a value passed in earlier as a flow input, so one button can serve many apps. Alternatively, you can hardcode a specific outbound app on the button. - **API key input**: an input field that collects an API key from the user. It works with the **Outbound App / Connect API Key** and **Outbound App / Connect Tenant API Key** actions. ![An example of configuring the outbound app button within Descope flows](/assets/outbound-app-flow-button.webp) ### Flow Actions The action is what actually connects the user or tenant and vaults the token. There are actions for OAuth connections, API key connections, and token cleanup, at both the user and tenant level. Across the connect actions, the target connection is set the same way: enable the **default connection** checkbox to choose a specific outbound app from a dropdown, or leave it off so the app is passed in dynamically from a screen button or a flow input. #### OAuth Connect Actions - **Outbound App / Connect** (user level) - **Outbound App / Tenant Connect** (tenant level) Both connect a user or tenant to an OAuth provider, and support the same configuration: - **Open in Popup**: open the provider's consent screen in a popup instead of a full-page redirect. - **Outbound App Scopes**: the scopes to request when none were set earlier in the flow. If left empty, the default scopes defined on the Outbound App are used. Accepts dynamic flow values. - **Login Hint**: an optional login hint forwarded to the provider. Accepts dynamic flow values. - **Outbound App Resources**: the resources to request with the outbound app. These can also be appended to the `/authorize` request in the Connection settings, but you can hardcode them here or use a dynamic value. - **External Identifier**: an optional external identifier stored with the token in Descope once the connection succeeds. ![Outbound app connect action configuration](/assets/outbound-app-action-without-screens.webp) #### API Key Connect Actions - **Outbound App / Connect API Key** (user level) - **Outbound App / Connect Tenant API Key** (tenant level) Both collect a static API key and vault it against the user or tenant. Pair the action with an [API key input](#screen-components) on the screen to collect the key from the user. For machine-to-machine agents or backend automation, keys can also be inserted programmatically depending on your integration design. #### Token Management Actions - **Outbound App / Delete User Tokens**: removes a user's stored connection tokens from within a flow, for example as part of a disconnect or account-cleanup screen. ## Method 3: Outbound App Widget Use the Outbound App widget when connection happens **out of band** from the agent: end users connect when they sign into your platform (or an account settings page), Descope vaults the credential, and later the agent fetches that token when a tool needs to call the third-party API. The widget runs entirely on your frontend. It lists the outbound apps a user can connect, shows which ones they have already connected, and lets them connect to each one directly — with no backend connect route to maintain. The widget requests the **default scopes** defined in your Connection settings. It does not support requesting custom scopes per user. If you need custom scopes, use the [backend connect API](#method-1-backend-connect-api) or a [flow](#method-2-descope-flows). ### How It Works 1. The user signs into your web app or platform. 2. Your UI embeds the Outbound App widget; the user connects the third-party account there. 3. Descope vaults the access and refresh tokens (or API key) against that user. 4. Later, when an agent or MCP tool needs the credential, your Resource fetches it from the vault and calls the provider. The widget is embedded with the Descope frontend SDKs. For setup and framework-specific snippets, see the [Outbound Applications Widget](/widgets/users#outbound-applications-widget) documentation. ## Method 4: Add Tenant Token via the Descope Console You can also store **tenant-level API keys** directly in the Descope Console. This path is for you as a [Descoper](/management/company-settings#descopers) when you need to vault an organization-owned API key that agents or backends will fetch later. **Add Tenant Token** works only for **tenant-level API keys**. It does not create user-level tokens or run an OAuth consent flow. For user-level keys or OAuth, use [Methods 1 - 3](#four-ways-to-store-tokens). ### Typical Setup 1. Create a [tenant](/management/tenant-management) in Descope for **your own organization**. 2. Connect that tenant to your workforce IdP with [SSO](/auth-methods/sso) (for example Entra ID or Okta), so employees can sign in with your company identity. 3. Create an [API key Connection](/agentic-identity-hub/core-components/connections/create-connections#api-key-connections) for the third-party service. 4. Open the Connection's **Token** tab and select the blue **Add Tenant Token** button. ![Connection Tokens tab with Add Tenant Token](/assets/typical-setup-outbound-app-api-key-console.webp) 5. In the dialog: - Choose the **tenant** (your organization tenant from the setup above). - Paste the **API key** value. ![Add Tenant Token dialog](/assets/add-tenant-token-console.webp) 6. Save. Descope vaults the key against that tenant. You can then [fetch the tenant-level token](/agentic-identity-hub/core-components/connections/fetching-connection-tokens#fetching-tenant-level-tokens) from your MCP server or backend like any other tenant credential. ## User-Level Tokens with Tenant Association This section applies to user-level tokens associated with a tenant. You can also store tenant-level tokens shared by multiple users in the same tenant. See [Storing Tenant Tokens](#storing-tenant-tokens). A user-level token with tenant association is stored at the **user level** with a tenant reference, which lets you store separate tokens for the same user across multiple tenants. **In flows**, tenant association is determined automatically from session context: - If the user belongs to exactly one tenant, that tenant is applied automatically. - If the user belongs to multiple tenants, association follows the active tenant context set earlier in the flow, for example through the [Tenant Select](/flows/screens/inputs/tenantselect-component) component. The active tenant context relies on the `dct` (Descope Current Tenant) claim, so enable `dct` in your JWT template authorization claim configuration (see [JWT templates](/management/token/jwt-templates#authorization-claims-configuration)). If no tenant context is available, the token is stored at the user level with no tenant association. **Through the connect API**, pass `tenantId` alongside `appId` in the connect request: ```http POST /v1/mgmt/outbound/app/connect Authorization: Bearer __ProjectID__: Content-Type: application/json { "appId": "google-contacts", "tenantId": "tenant-123", "options": { "redirectUrl": "https://your-app.com/connection-complete" } } ``` ## Storing Tenant Tokens Tenant-scoped tokens are shared by all users in a tenant rather than tied to one user. - **OAuth**: store tenant tokens through [Descope Flows](#method-2-descope-flows) using the **Outbound App / Tenant Connect** action. - **API keys in a flow**: use the **Outbound App / Connect Tenant API Key** action. - **API keys in the Console**: use [Method 4: Console Add Tenant Token](#method-4-add-tenant-token-via-the-descope-console) when you (as a Descoper) are vaulting an organization-owned key for your own tenant. In the flow cases the experience behaves like the user-level version described above, but the credential is vaulted against the tenant. ## Deleting Connection Tokens You can remove stored tokens from the Console or programmatically. ### Using the Descope Console Open the **Token Management** tab for your connection in the Console, find the user or tenant, and delete the token from the dashboard. ### Using the SDKs or APIs #### Delete a Specific Token by ID ```js // Delete a specific token by its ID // Token deletion cannot be undone. Use carefully. await descopeClient.management.outboundApplication.deleteTokenById('token-id-123'); ``` ```python # Delete a specific token by its ID descope_client.mgmt.outbound_application.delete_token( token_id='token-id-123' ) ``` ```java import com.descope.sdk.mgmt.OutboundAppsService; outboundAppsService.deleteOutboundAppTokenById("token-id-123"); ``` ```go // Delete a specific token by its ID err := descopeClient.Management.OutboundApplication().DeleteTokenByID(ctx, "token-id-123") if err != nil { // Handle error } ``` ```php // Delete a specific token by its ID $descopeSDK->management->outboundApps->deleteTokenById('token-id-123'); ``` ```ruby # Delete a specific token by its ID client.delete_outbound_app_token_by_id(token_id: 'token-id-123') ``` ```bash curl -X DELETE "__BaseURL__/v1/mgmt/outbound/token?id=token-id-123" \ -H "Authorization: Bearer " ``` #### Delete Tokens by App ID and/or User ID You can delete tokens by providing an app ID, user ID, or both. At least one of `appId` or `userId` must be provided. ```js // Delete all tokens for a specific app and user // Token deletion cannot be undone. Use carefully. await descopeClient.management.outboundApplication.deleteUserTokens('my-app-id', 'user-id'); // Delete all tokens for a specific app await descopeClient.management.outboundApplication.deleteUserTokens('my-app-id'); // Delete all tokens for a specific user await descopeClient.management.outboundApplication.deleteUserTokens(undefined, 'user-id'); ``` ```python # Delete all tokens for a specific app and user descope_client.mgmt.outbound_application.delete_user_tokens( app_id='my-app-id', user_id='user-id' ) # Delete all tokens for a specific app descope_client.mgmt.outbound_application.delete_user_tokens( app_id='my-app-id' ) # Delete all tokens for a specific user descope_client.mgmt.outbound_application.delete_user_tokens( user_id='user-id' ) ``` ```java import com.descope.sdk.mgmt.OutboundAppsService; // Delete all tokens for a specific app and user DeleteOutboundAppUserTokensRequest deleteRequest = new DeleteOutboundAppUserTokensRequest(); deleteRequest.setAppId("google-contacts"); deleteRequest.setUserId("user-123"); outboundAppsService.deleteOutboundAppUserTokens(deleteRequest); ``` ```go // Delete all tokens for a specific app and user err := descopeClient.Management.OutboundApplication().DeleteUserTokens(ctx, "my-app-id", "user-id") if err != nil { // Handle error } // Delete all tokens for a specific app err = descopeClient.Management.OutboundApplication().DeleteUserTokens(ctx, "my-app-id", "") if err != nil { // Handle error } // Delete all tokens for a specific user err = descopeClient.Management.OutboundApplication().DeleteUserTokens(ctx, "", "user-id") if err != nil { // Handle error } ``` ```php // Delete all tokens for a specific app and user $descopeSDK->management->outboundApps->deleteUserTokens('my-app-id', 'user-id'); // Delete all tokens for a specific app $descopeSDK->management->outboundApps->deleteUserTokens('my-app-id', null); // Delete all tokens for a specific user $descopeSDK->management->outboundApps->deleteUserTokens(null, 'user-id'); ``` ```ruby # Delete all tokens for a specific app and user client.delete_outbound_app_user_tokens(app_id: 'my-app-id', user_id: 'user-id') # Delete all tokens for a specific app client.delete_outbound_app_user_tokens(app_id: 'my-app-id') # Delete all tokens for a specific user client.delete_outbound_app_user_tokens(user_id: 'user-id') ``` ```bash # Delete all tokens for a specific app and user curl -X DELETE "__BaseURL__/v1/mgmt/outbound/user/tokens?appId=google-contacts&userId=user-123" \ -H "Authorization: Bearer " # Delete all tokens for a specific app curl -X DELETE "__BaseURL__/v1/mgmt/outbound/user/tokens?appId=google-contacts" \ -H "Authorization: Bearer " # Delete all tokens for a specific user curl -X DELETE "__BaseURL__/v1/mgmt/outbound/user/tokens?userId=user-123" \ -H "Authorization: Bearer " ``` # Discovery Endpoints (/agentic-identity-hub/core-components/mcp-servers/discovery-url) Learn how the well-known discovery endpoint works for MCP servers and understand the OAuth metadata structure. # Discovery Endpoints Descope hosts a `.well-known` OpenID Connect discovery endpoint for each MCP server. This endpoint publishes OAuth metadata that MCP clients use to discover authorization endpoints, token endpoints, supported scopes, and other OAuth configuration details. The discovery endpoint follows the [OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html) specification and includes standard OAuth 2.1 fields, plus MCP-specific extensions. ## Discovery Endpoint Structure The well-known endpoint returns a JSON document containing OAuth configuration metadata. Here's an example structure: ```json { "issuer": "__BaseURL__/v1/apps/agentic/P32juVkF2iM8wAGoM8PiLkGi6POV/MS35d1Xw1Yo6zozsW7n8BU6xLmnAs", "jwks_uri": "__BaseURL__/P32juVkF2iM8wAGoM8PiLkGi6POV/.well-known/jwks.json", "authorization_endpoint": "__BaseURL__/oauth2/v1/apps/agentic/P32juVkF2iM8wAGoM8PiLkGi6POV/MS35d1Xw1Yo6zozsW7n8BU6xLmnAs/authorize", "response_types_supported": [ "code" ], "subject_types_supported": [ "public" ], "id_token_signing_alg_values_supported": [ "RS256" ], "token_endpoint": "__BaseURL__/oauth2/v1/apps/agentic/P32juVkF2iM8wAGoM8PiLkGi6POV/MS35d1Xw1Yo6zozsW7n8BU6xLmnAs/token", "userinfo_endpoint": "__BaseURL__/oauth2/v1/apps/P32juVkF2iM8wAGoM8PiLkGi6POV/userinfo", "scopes_supported": [ "mcp:schedule_meetings", "mcp:read_hubspot_contacts", "mcp:send_confirmation_email", "outbound.token.fetch" ], "claims_supported": [ "iss", "aud", "iat", "exp", "sub", "name", "email", "email_verified", "phone_number", "phone_number_verified", "picture", "family_name", "given_name" ], "revocation_endpoint": "__BaseURL__/oauth2/v1/apps/P32juVkF2iM8wAGoM8PiLkGi6POV/revoke", "registration_endpoint": "__BaseURL__/v1/mgmt/mcp/client/P32juVkF2iM8wAGoM8PiLkGi6POV/MS35d1Xw1Yo6zozsW7n8BU6xLmnAs/register", "code_challenge_methods_supported": [ "S256" ], "client_id_metadata_document_supported": true } ``` ## Field Descriptions ### Standard OAuth Fields These fields follow the standard OpenID Connect Discovery specification: The `jwks_uri` is the same for every MCP server within a Descope project, as the same private key is used to sign all JWTs created within your Descope project. - **`issuer`**: The unique identifier for the authorization server. Used to verify tokens and construct other endpoint URLs. The issuer URL is structured as `/v1/apps/agentic//`, where `` is your region's [Descope base URL](/management/project-settings/multi-regional#descope-base-urls) (for example `https://api.descope.com`), or your [custom domain](/how-to-deploy-to-production/custom-domain) if you have one set up for your Descope project. The MCP Server ID is not modifiable once the MCP server is created. - **`jwks_uri`**: The URL of the JSON Web Key Set (JWKs) endpoint containing public keys for verifying signed tokens. - **`authorization_endpoint`**: The endpoint where clients initiate the authorization flow. - **`token_endpoint`**: The endpoint for exchanging authorization codes for access tokens. - **`userinfo_endpoint`**: The endpoint for retrieving user information using an access token. - **`scopes_supported`**: An array of scopes supported by the MCP server. This list is **automatically updated** based on the scopes you define in your [MCP server configuration](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes). - **`claims_supported`**: An array of standard OAuth claims that may be included in tokens. Currently, this list **cannot be modified** and contains standard OpenID Connect claims such as `iss`, `aud`, `sub`, `email`, `name`, etc. - **`revocation_endpoint`**: The endpoint for revoking access or refresh tokens. - **`response_types_supported`**: Supported OAuth response types (always `["code"]` for authorization code flow). - **`subject_types_supported`**: Supported subject identifier types (always `["public"]`). - **`id_token_signing_alg_values_supported`**: Supported JWT signing algorithms (always `["RS256"]`). - **`code_challenge_methods_supported`**: Supported PKCE code challenge methods (always `["S256"]`). ### Conditional Fields (Based on MCP Client Registration Configuration) - **`registration_endpoint`**: The endpoint for Dynamic Client Registration (DCR). This field is only present when DCR is **enabled** for the MCP server. If DCR is disabled, this field will not appear in the discovery document. - **`client_id_metadata_document_supported`**: A boolean indicating whether Client-Initiated Metadata Discovery (CIMD) is supported. This is `true` when CIMD is **enabled**, and `false` when CIMD is **disabled**. ## OAuth Protected Resource Metadata The OAuth Protected Resource Metadata must be hosted by the MCP server developer, not by Descope. You'll need to host this JSON document at `/.well-known/oauth-protected-resource` on your MCP server. MCP servers advertise their authorization server using [OAuth Protected Resource Metadata](https://modelcontextprotocol.io/specification/draft/basic/authorization#protected-resource-metadata-discovery-requirements). This metadata document is typically returned in the `WWW-Authenticate` header when an MCP server responds with a `401 Unauthorized` error, or can be accessed via a well-known endpoint. The protected resource metadata document references the Descope well-known discovery endpoint and provides resource-specific information: ```json { "authorization_servers": [ "__BaseURL__/v1/apps/agentic/P32juVkF2iM8wAGoM8PiLkGi6POV/MS37k7ewcHY6bDkMEtubEidQ8cvAy" ], "bearer_methods_supported": [ "header" ], "resource": "[YOUR MCP SERVER URL]", "resource_documentation": "[YOUR MCP SERVER DOCS URL]", "scopes_supported": ["mcp:hubspot","mcp:google-calendar"] } ``` ### Protected Resource Metadata Fields - **`authorization_servers`**: An array containing the issuer URL(s) of the authorization server(s) that can issue access tokens for this resource. Each URL in this array should correspond to the `issuer` field from a well-known discovery document. - **`bearer_methods_supported`**: The methods supported for presenting access tokens. Always `["header"]` for MCP servers, indicating tokens should be sent in the `Authorization: Bearer ` header. - **`resource`**: The URL of the protected resource (your MCP server URL). - **`resource_documentation`**: (Optional) A URL pointing to documentation for the protected resource. - **`scopes_supported`**: A subset of scopes supported by this resource. This may be a filtered list of the scopes defined in the well-known discovery document's `scopes_supported` field. Depending on the MCP client you're using, the `scopes_supported` field may have to exactly match the scopes you've defined in your MCP server configuration, even though this is not required by the MCP specification. ### Relationship to Well-Known Discovery Document The protected resource metadata document references the well-known discovery endpoint through the `authorization_servers` field. MCP clients follow this workflow: 1. **Discover the protected resource metadata** (via `WWW-Authenticate` header or well-known endpoint) 2. **Extract the `authorization_servers` URL(s)** from the metadata 3. **Fetch the well-known discovery document** from each authorization server URL 4. **Use the discovery document** to find OAuth endpoints (`authorization_endpoint`, `token_endpoint`, `jwks_uri`, etc.) 5. **Initiate the OAuth flow** using the discovered endpoints This two-step discovery process allows MCP servers to advertise their authorization server location independently of the detailed OAuth configuration, which is then fetched from the well-known discovery endpoint. ## How MCP Clients Use the Discovery Endpoint MCP clients use the discovery endpoint to: 1. **Discover OAuth endpoints** without hardcoding URLs 2. **Understand supported scopes** before initiating authorization 3. **Determine if DCR or CIMD is available** via the `registration_endpoint` and `client_id_metadata_document_supported` fields 4. **Verify token signing keys** using the `jwks_uri` 5. **Understand supported authentication methods** and algorithms Here is a simplified sequence diagram of how MCP clients use the discovery endpoint: The discovery endpoint enables MCP clients to integrate with your MCP server dynamically, adapting to configuration changes automatically. Visit our [MCP Servers](/agentic-identity-hub/core-components/mcp-servers) documentation for more information on how to configure your MCP server and get this discovery document for your own server. # MCP Servers (/agentic-identity-hub/core-components/mcp-servers) Learn how to configure MCP servers and onboard clients using Descope OAuth 2.1, SSO, audience-scoped tokens, and tool-level scopes. # MCP Servers The [MCP Servers](https://app.descope.com/agentic-hub/mcp-servers) page is where you configure and manage the MCP servers your agents connect to. From here you can: - **View all MCP servers** registered in your project, along with their registered clients and configuration - **Configure server settings**: scopes, client registration methods (DCR/CIMD), consent flows, and session management - **Manage registered clients**: view, filter, and delete the clients that have connected to each server MCP servers are created as [Resources](/resources) in the Descope Console and appear here in the Agentic Identity Hub, where you manage the client and policy surface that sits on top of them. ## Descope as the Authorization Server Descope acts as your **authorization server**: it registers clients, runs login and consent, and issues tokens. Your MCP implementation is the **resource server** that validates them. For the full model and why to structure it this way, see the [MCP overview](/mcp#how-it-fits-together). When you create a [Resource](/resources) in Descope for your MCP Server, you can also define [MCP scopes and Connection scope mappings](/resources/scopes-and-roles#mcp-server-resources) on the Resource. Descope issues tokens with an `aud` claim that includes your MCP server URL. Your server URL must be present in the audience list for the token to be valid. You can create unlimited MCP Server Resources per Descope project. ## Settings See the [MCP Server Settings](/agentic-identity-hub/core-components/mcp-servers/settings) documentation for details on configuring your MCP server, including server details, client registration, scopes, and consent/assessment flows. ### Usage Samples Under the usage samples section, you can find code examples of how to configure your MCP server to use Descope authentication. You will also find your Discovery URL (.well-known) as well as your Issuer URL. ![Usage Samples](/assets/usage-samples.webp) For starter templates for how to get started with MCP Auth with Descope, you can visit our [AI examples](https://github.com/descope/ai/tree/main/examples) repository. #### OAuth Protected Metadata Resource You will need to host the OAuth Protected Metadata Resource at the following URL: `/.well-known/oauth-protected-resource`. This is the URL that will be used by MCP clients to discover the authorization server endpoints, token endpoints, and supported scopes. Here is an example of the JSON you will need to host: ```json { "authorization_servers": [ "__BaseURL__/v1/apps/agentic/P32juVkF2iM8wAGoM8PiLkGi6POV/MS37k7ewcHY6bDkMEtubEidQ8cvAy" ], "bearer_methods_supported": [ "header" ], "resource": "https://differentially-unmurmurous-lamonica.ngrok-free.dev/mcp", "resource_documentation": "https://differentially-unmurmurous-lamonica.ngrok-free.dev/docs", "scopes_supported": ["mcp:hubspot","mcp:google-calendar"] } ``` Once you've hosted the JSON, try connecting to your MCP server using a client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to validate that you can complete the OAuth discovery and login process. ## How MCP Server authentication works After you've configured your [MCP server settings](#settings), MCP clients can discover and connect to your server. The authentication and authorization flow consists of the following steps: 1. [Authorization Server Discovery](#authorization-server-discovery) 2. [Client Authentication](#client-authentication) 3. [Token Validation & Tool Execution](#token-validation-tool-execution) ### Authorization Server Discovery Descope hosts the authorization server and publishes a `.well-known` metadata document that contains the OAuth endpoints and supported scopes for your MCP server. The MCP client/server discovery process exposes the authorization URL, token exchange endpoint, and JWK signing keys so MCP clients can authenticate and request scopes without you maintaining OAuth infrastructure. MCP clients discover these endpoints by first checking the `WWW-Authenticate` header returned in a 401 Unauthorized response. If the header is not present, the client falls back to requesting well-known metadata URIs. To learn more about how MCP clients use the discovery endpoint, you can read more about the [authorization server discovery](/agentic-identity-hub/core-components/mcp-servers/discovery-url) process. ### Client Authentication This section applies only to authentication flows with user interaction (authorization code flow). For machine-to-machine (M2M) authentication using the `client_credentials` flow, user consent and authentication is not required. When a client makes an `/authorize` request, Descope runs a [User Consent Flow](/agentic-identity-hub/core-components/mcp-servers/settings#user-consent-flow) to authenticate the user and collect scope approval. The consent screen displays the descriptions you configured for each MCP scope, helping users understand what access they are granting. After consent, Descope issues an access token containing only approved scopes and includes the MCP server URL in the `aud` (audience) claim. The access token may contain multiple audiences, but must include your [MCP Server URL](#mcp-server-details) for the token to be valid for your MCP server. ### Token Validation & Tool Execution Your MCP server validates Descope access tokens using the public key from the JWKs endpoint (our [session validation](/sessions/validation) functions make this easy). After validation, the server checks the `aud` claim and confirms the correct tool scope is present before executing the requested tool. If the tool needs third-party credentials, the server retrieves them from [Connections](/agentic-identity-hub/core-components/connections), Descope's secure token vault, and may exchange the Descope access token or use a [Management Key](/management#management-keys) to retrieve the external service token before tool execution. #### Complete Auth + Tool Execution Flow ## Clients This section shows all the clients that have been registered with your MCP server. Clients are organized by their **Name**, **Client ID**, **Status**, **Tags**, **Created Time**, and **Registration Method**. You can search or filter for a specific client by any of these attributes. ### Viewing and Managing Clients ![Clients](/assets/clients.webp) You can delete clients by: - Clicking the three dots menu to the right of a client and selecting **Delete** - Selecting multiple clients and clicking the **Delete** button in the top right corner When you delete a client, Descope will prompt you for confirmation. Once a client is deleted, the client ID and secret are no longer valid for this MCP server. ### Client Details Clicking on a specific client displays detailed configuration information: ![Client Details](/assets/client-details.webp) **Client Configuration:** - **Name**: Modifiable name that originates from the DCR or CIMD configuration - **Client ID** and **Client Secret**: System-generated and not modifiable - **Scopes**: The [scopes](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes) allowed for this client on this MCP server - Mandatory scopes cannot be removed from the client - Only optional scopes can be modified or removed - **Approved Redirect URIs**: Client-specific redirect URIs automatically populated from [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd) or [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) registration requests - **Tags**: Client tags that can be set manually from the console for use in [policies](/agentic-identity-hub/policies) # Managing MCP Servers (/agentic-identity-hub/core-components/mcp-servers/management) Learn how to create, list, update, and delete MCP Servers using the Descope Management API. # MCP Server Management Use the [Management API](/management) to manage **MCP Servers** and **MCP Server Clients** (OAuth clients registered for a specific MCP Server). All endpoints require a [Management Key](/management#management-keys). **Authentication:** Send your [Project ID](https://app.descope.com/settings/project) and [Management Key](https://app.descope.com/settings/company/settings) as a bearer token: ```bash Authorization: Bearer __ProjectID__: ``` ## MCP Server Endpoints ### Create MCP Server **Endpoint:** `POST /v1/mgmt/mcp/server/create` Create a new MCP Server. The response returns the created `server` object. **Request body:** - **Identity:** `name`, `description`, `tags`, `logo` - **Access:** `audienceWhitelist`, `approvedScopes` (see [Approved scopes](#approved-scopes) below), `approvedCallbackUrls` - **Registration & consent:** `dynamicRegistration` (DCR/CIMD and flow), `skipConsentScreen`, `consentFlowId`, `consentFlowHostingURL` - **Session & CIMD:** `sessionSettings`, `cimdSettings` #### Approved scopes `approvedScopes` is a single list of scopes that clients can request for this MCP Server. Each scope has: - `name` — Scope identifier (e.g. `mcp:test`, `mcp:google.read`). - `description` — Shown on consent screens. - `optional` — (Optional) If `true`, the client may omit this scope; if `false` or omitted, it can be required. - `values` — (Optional) If present, this scope is a [**connection scope**](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-server-scopes): it grants access to the given [Connections](/agentic-identity-hub/core-components/connections) (resource URLs or connection identifiers). If `values` is omitted, the scope has no connection associated—it is considered a normal permission scope. ![MCP server scopes](/assets/mcp-server-scopes.webp) **Example: scopes with no connection** ```json "approvedScopes": [ { "name": "mcp:test", "description": "Test Scope", "optional": true }, { "name": "mcp:tools:write", "description": "Write access to tools", "optional": false } ] ``` **Example: scopes with and without connections** ```json "approvedScopes": [ { "name": "mcp:test", "description": "Test Scope", "optional": true }, { "name": "mcp:google.read", "description": "Read from Google Calendar", "values": ["https://api.googles.com/readonly"] } ] ``` **Create example (full request)** ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/create" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{ "name": "Test Server from API", "description": "Test description", "audienceWhitelist": ["https://app.example.com/api"], "dynamicRegistration": { "enabled": true, "flowId": "sign-up-or-in" }, "approvedScopes": [ { "name": "mcp:test", "description": "Test Scope", "optional": true }, { "name": "mcp:google.read", "description": "Read from Google Calendar", "values": ["https://api.googles.com/readonly"] } ], "cimdSettings": { "enabled": true, "domainPolicies": { "policies": [ { "domainPattern": "*", "enabled": true } ] } } }' ``` ### Load MCP Server **Endpoint:** `POST /v1/mgmt/mcp/server/load` Load a single MCP Server by ID. The response includes the full `server` object. **Request body:** - **Required:** `id` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/load" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": ""}' ``` ### Load All MCP Servers **Endpoint:** `POST /v1/mgmt/mcp/servers/all` Load all MCP Servers in the project. The response returns a `servers` array. **Request body:** - **Optional:** Pagination and filter fields (see API reference). Can be `{}`. ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/servers/all" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{}' ``` ### Update MCP Server **Endpoint:** `POST /v1/mgmt/mcp/server/update` Update an existing MCP Server. The response returns the updated `server` object. **Request body:** - **Required:** `server` — Full [MCP Server](#create-mcp-server) object including `id`; include all fields you want to keep (same shape as create, including [approvedScopes](#approved-scopes)). ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/update" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{ "server": { "id": "", "name": "Test Server (Updated)", "description": "Updated description", "audienceWhitelist": ["https://app.example.com/api"], "dynamicRegistration": { "enabled": true, "flowId": "sign-up-or-in" }, "approvedScopes": [ { "name": "mcp:test", "description": "Test Scope", "optional": true }, { "name": "mcp:tools:write", "description": "Write access", "optional": false }, { "name": "mcp:google.read", "description": "Read from Google Calendar", "values": ["https://api.googles.com/readonly"] } ], "approvedCallbackUrls": ["https://myapp.example.com/callback"], "skipConsentScreen": false } }' ``` ### Delete MCP Server **Endpoint:** `POST /v1/mgmt/mcp/server/delete` Delete a single MCP Server. **Request body:** - **Required:** `id` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/delete" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": ""}' ``` ### Delete MCP Servers (bulk) **Endpoint:** `POST /v1/mgmt/mcp/servers/delete` Delete multiple MCP Servers by ID. **Request body:** - **Required:** `ids` — Array of MCP Server IDs ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/servers/delete" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"ids": ["", ""]}' ``` ## MCP Server Client Endpoints MCP Server Clients are OAuth clients (agents or applications) registered for a specific MCP Server. ### Create MCP Server Client **Endpoint:** `POST /v1/mgmt/mcp/server/client/create` Create a new MCP Server Client. The response returns `id`, `clientId`, and `cleartext` (client secret—store it securely; it is only returned once). **Request body:** - **Required:** `name`, `mcpServerId` - **Optional:** `approvedCallbackUrls`, `scopes`, `tags`, `logo` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/create" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{ "name": "Support Agent", "mcpServerId": "", "approvedCallbackUrls": ["https://myapp.example.com/oauth/callback"], "scopes": ["mcp:tools:read", "mcp:tools:write"], "tags": ["agent", "support"] }' ``` ### Update MCP Server Client **Endpoint:** `POST /v1/mgmt/mcp/server/client/update` Update an existing MCP Server Client. The response returns the updated `client` object. **Request body:** - **Required:** `id`, `mcpServerId` - **Optional:** `name`, `approvedCallbackUrls`, `scopes`, `tags`, `logo` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/update" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{ "id": "", "mcpServerId": "", "name": "Support Agent (Updated)", "approvedCallbackUrls": ["https://myapp.example.com/oauth/callback"], "scopes": ["mcp:tools:read", "mcp:tools:write", "mcp:admin"], "tags": ["agent", "support", "v2"] }' ``` ### Load MCP Server Client **Endpoint:** `POST /v1/mgmt/mcp/server/client/load` Load a single MCP Server Client by ID. The response returns the `client` object. **Request body:** - **Required:** `id`, `mcpServerId` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/load" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": "", "mcpServerId": ""}' ``` ### Search MCP Server Clients **Endpoint:** `POST /v1/mgmt/mcp/server/clients/search` Search MCP Server Clients for a given MCP Server. The response returns a `clients` array and `total`. **Request body:** - **Required:** `mcpServerId` - **Optional:** `page`, `limit`, `text`, `name`, `clientId`, `status`, `registrationMethod`, `tag`, `sort` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/clients/search" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{ "mcpServerId": "", "page": 0, "limit": 20, "name": "Support", "status": "active" }' ``` ### Delete MCP Server Client **Endpoint:** `POST /v1/mgmt/mcp/server/client/delete` Delete a single MCP Server Client. **Request body:** - **Required:** `id`, `mcpServerId` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/delete" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": "", "mcpServerId": ""}' ``` ### Delete MCP Server Clients (bulk) **Endpoint:** `POST /v1/mgmt/mcp/server/clients/delete` Delete multiple MCP Server Clients by ID. **Request body:** - **Required:** `ids` (array of client IDs), `mcpServerId` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/clients/delete" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"ids": ["", ""], "mcpServerId": ""}' ``` ### Get MCP Server Client Secret **Endpoint:** `POST /v1/mgmt/mcp/server/client/secret` Retrieve the current client secret. The response returns `cleartext` (the secret). **Request body:** - **Required:** `id`, `mcpServerId` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/secret" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": "", "mcpServerId": ""}' ``` ### Rotate MCP Server Client Secret **Endpoint:** `POST /v1/mgmt/mcp/server/client/secret/rotate` Rotate a client's secret. The response returns the new `cleartext` secret (store it securely; it is only returned once). **Request body:** - **Required:** `id`, `mcpServerId` ```bash curl -X POST "__BaseURL__/v1/mgmt/mcp/server/client/secret/rotate" \ -H "Authorization: Bearer __ProjectID__:" \ -H "Content-Type: application/json" \ -d '{"id": "", "mcpServerId": ""}' ``` # Registration Methods (/agentic-identity-hub/core-components/mcp-servers/registration-methods) Learn about the client registration methods for MCP servers. # Client Registration Methods MCP servers support three client registration mechanisms for onboarding OAuth clients. Each method has different use cases and benefits, and clients supporting all options should follow a specific priority order when choosing which method to use. ## Client Registration Approaches Descope supports three client registration mechanisms for [MCP servers](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-client-registration): - **Client ID Metadata Documents (CIMD)**: When client and server have no prior relationship - **Dynamic Client Registration (DCR)**: For backwards compatibility or specific requirements - **Pre-registration**: Pre-defined client in Descope, with it's own unique Client ID and Secret For new MCP server deployments, [Client ID Metadata Documents (CIMD)](#client-id-metadata-documents-cimd) is the recommended approach as it provides the best balance of security, flexibility, and ease of use for both clients and servers. Pre-registration is ideal for controlled environments with known clients, while DCR should be used primarily for backwards compatibility. Most MCP servers will want to enable both CIMD and DCR to support the widest range of clients, with CIMD as the primary method and DCR as a fallback option. Clients registered through **CIMD or DCR** automatically get access to the MCP server Resource they registered with; no [policy](/policies) is required. A **pre-registered** (manually created) client gets no access by itself: you must also add a policy whose subject is the client and whose target is the Resource. ## Client ID Metadata Documents (CIMD) Since CIMD is relatively new, most MCP clients do not currently support this method of registration. Client ID Metadata Documents (CIMD) enable clients to use HTTPS URLs as client identifiers, where the URL points to a JSON document containing client metadata. This approach addresses the common MCP scenario where servers and clients have no pre-existing relationship, making it the **recommended method** for most MCP deployments. All CIMD clients that present the **same metadata URL are the same client** in Descope, sharing one client ID. For example, every Claude user connecting to your MCP server presents Claude's metadata URL, so all of them appear under a single Claude client. What distinguishes them is their [agentic identities](/agentic-identity-hub/core-components/agents): each user's consent creates its own agentic identity under that shared client. The following diagram illustrates the complete flow when using CIMD: To enable CIMD for your MCP server, see the [MCP Server](/agentic-identity-hub/core-components/mcp-servers/settings#mcp-client-registration) documentation. When CIMD is enabled, the `client_id_metadata_document_supported` field in your [discovery document](/agentic-identity-hub/core-components/mcp-servers/discovery-url#conditional-fields-based-on-mcp-client-registration-configuration) will be set to `true`. ### Example Metadata Document Here's an example of a Client ID Metadata Document, hosted by a client: ```json { "client_id": "https://app.example.com/oauth/client-metadata.json", "client_name": "Example MCP Client", "client_uri": "https://app.example.com", "logo_uri": "https://app.example.com/logo.png", "redirect_uris": [ "http://127.0.0.1:3000/callback", "http://localhost:3000/callback" ], "grant_types": ["authorization_code"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` ## Dynamic Client Registration (DCR) Dynamic Client Registration (DCR) is the OAuth 2.0 Dynamic Client Registration Protocol (RFC 7591) that allows MCP clients to obtain OAuth client IDs programmatically without user interaction. This option is included primarily for **backwards compatibility** with earlier versions of the MCP authorization spec. ### When to Use DCR DCR is best suited for: - **Legacy clients**: Supporting clients that don't support CIMD - **Fallback option**: As a fallback when CIMD is not available - **Specific requirements**: When programmatic registration is required but CIMD isn't feasible However, compared to CIMD, DCR also has some strong limitations. Most notably: - **Server-side management overhead**: Each registration creates a new client in Descope, resulting in many clients that need to be managed over time. - **Less flexible**: Clients must go through a registration step before they can authenticate ### Discovery Authorization servers advertise support for Dynamic Client Registration by including a `registration_endpoint` in their OAuth Authorization Server metadata: ```json { "registration_endpoint": "__BaseURL__/v1/mgmt/mcp/client/P32juVkF2iM8wAGoM8PiLkGi6POV/MS35d1Xw1Yo6zozsW7n8BU6xLmnAs/register" } ``` When DCR is enabled, this endpoint appears in your MCP server's [discovery document](/agentic-identity-hub/core-components/mcp-servers/discovery-url#conditional-fields-based-on-mcp-client-registration-configuration). ## Pre-registration Pre-registration involves manually registering clients with the authorization server before they can connect. This method is suitable when you have an existing relationship with the client and want to maintain explicit control over which clients can access your MCP server. ### When to Use Pre-registration Pre-registration is best suited for: - **Known clients**: When you have a pre-existing relationship with the client (e.g., internal tools, partner integrations) - **Simplified client setup**: When clients prefer not to host metadata documents - **Static deployments**: When client configurations rarely change In Descope, you can pre-register clients under the [Clients](/agentic-identity-hub/core-components/mcp-servers#clients) section after the MCP server configuration is saved. # MCP Server Settings (/agentic-identity-hub/core-components/mcp-servers/settings) Learn how to configure MCP server settings, including server details, client registration, scopes, and flows. # MCP Server Settings This section provides an overview of all configurable aspects of an MCP server, under the **Settings** tab. ## MCP Server Details If you would like to add additional claims to any access tokens created for this MCP Server, you can use the [custom claims](/flows/actions/custom-claims) flow action in your user consent flow. These are the basic configurable details of an MCP server: - **Name** (required) - **Description** (optional) - **MCP Server URL** (optional): This is the base URL of your MCP endpoint and should end with `/mcp`. During authentication, this value is added as an audience (`aud`) in access tokens so the token is valid for this MCP Server. Tokens can include multiple audiences, but this URL must be one of them. ![MCP Server Details](/assets/mcp-server-details.webp) ## MCP Client Registration In this section, you can configure how clients can register with your MCP server. Descope supports three client registration mechanisms: **Client ID Metadata Documents (CIMD)**, **Dynamic Client Registration (DCR)**, and **Pre-registration**. Learn more about each method and when to use them in our [Client Registration Methods](/agentic-identity-hub/core-components/mcp-servers/registration-methods) documentation. ![MCP Client Registration](/assets/mcp-client-registration.webp) Clients must be pre-registered under the [Clients](/agentic-identity-hub/core-components/mcp-servers#clients) section after the MCP server configuration is saved, not in this client registration configuration section. The two settings you can configure in this section of the MCP server settings are the following: | Setting | Purpose | |---|---| | **[CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)** | Securely discover client OAuth metadata from a client-hosted HTTPS URL. You can also specify **approved domains** when enabling. | | **[DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr)** | Allow clients to self-register and receive a `client_id` if CIMD is unsupported. | By default, all MCP servers will have DCR enabled, and CIMD disabled. ### Approved Domains You can specify approved domains for CIMD client registration. This is a list of domains that are allowed to register with your MCP server. If a client is not from an approved domain, the client registration will fail. Approved domains can either be a specific domain, or a wildcard domain. For example, `example.com` or `*.example.com`. ## MCP Server Scopes MCP server scopes are defined on the [MCP Server Resource](/resources) in the console, and this Settings view edits the same Resource. Each scope maps to tool access and can link to [Connection](/agentic-identity-hub/core-components/connections) scopes for third-party credentials. See [MCP Server Resource scopes](/resources/scopes-and-roles#mcp-server-resources). For each scope, configure: | Field | Purpose | |---|---| | **Scope name** | The machine-friendly scope string enforced by your MCP server (e.g., `menu:read`, `calendar:write`) | | **Connection scopes** | OAuth scopes or permissions that should be requested on external services via [Connections](/agentic-identity-hub/core-components/connections) | | **Description to show on the consent screen for end users** | Human-readable explanation shown on the **user consent screen** | | **Mark as mandatory scope (toggle)** | Optionally set the scope as optional (scopes are mandatory by default) | Here is an example scope configuration: ![MCP server scopes](/assets/mcp-server-scopes.webp) ### Include All Authorization Info in Access Tokens In the **MCP Server Scopes** section, click **Manage** to toggle the **Include all authorization info in access tokens** setting. This controls which authorization data is embedded in the JWTs issued to MCP clients. When **enabled**, access tokens for **dynamically registered** clients (those that register via [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) or [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)) include **all tenants, roles, and permissions** associated with the user, regardless of which MCP scopes the client requested or the user approved on the consent screen. Scope consent still controls what the client may **request**; the JWT is not limited to the scope-to-role mapping for those scopes. When **disabled** (default), access tokens for those clients only include tenants, roles, and permissions derived from the approved scopes and your MCP server scope configuration. ![Include all authorization info in access tokens](/assets/mcp-scopes-settings.webp) ## Flows Each MCP server supports two configurable flows that control how users authenticate and how MCP clients (agents) are onboarded. ![Flows](/assets/mcp-flows.webp) ### User Consent Flow End users go through the **User Consent Flow** when authenticating to an MCP server. This flow runs during `/authorize` requests and controls user authentication, scope consent, and conditional logic. The following things can be configured in the User Consent Flow: - **User Authentication** - Guide users through your chosen authentication methods, such as SSO (Okta, Azure AD, etc.), passwordless/password-based login, social login, or MFA and step-up authentication. - **Scope Consent** - Display a consent screen where users explicitly approve the MCP server scopes being requested. Scope descriptions configured on the MCP server are shown here to clearly communicate what access is being granted. - **Conditional Logic** - Add conditional logic based on user, tenant, or request context, such as tenant-specific authentication requirements, additional verification for sensitive scopes, or conditional MFA and step-up flows. Branch on the connecting client's [custom attributes](/flows/dynamic-keys#inbound-app-and-mcp-client-custom-attributes) using `mcpClient.customAttributes.`. - **Connection Actions** - You can use [Connection actions](/agentic-identity-hub/core-components/connections/storing-connections#method-2-descope-flows) in your User Consent Flow to collect external OAuth tokens or API keys during authentication. Check out our [Flows](/flows) documentation for more information on how to configure and use the User Consent Flow. ### Client Registration Flow The **Client Registration Flow** runs when an MCP client registers using [DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods#dynamic-client-registration-dcr) or [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd). This flow is used to verify and classify clients before they are allowed to interact with your MCP server and users can start signing in. By default, newly registered clients are **unverified**. **Tag and Classify Clients**: Assign tags to clients for identification and future access control, for example tagging Claude as `sales-agent`, ChatGPT as `viewer-agent`, or internal tools as `internal-agent`. These tags can later be used in [policies](/agentic-identity-hub/policies) across the Agentic Identity Hub. **Verify Client Status**: Review newly registered clients and update their status by marking trusted clients as verified, blocking or rejecting untrusted clients, or using connectors (such as [AbuseIPDB](/connectors/connector-configuration-guides/fraud/abuseipdb)) to detect malicious originating IPs and block registration from those IPs. This allows you to enforce reputation checks or platform detection before granting access to your MCP server and users can start signing in. # Claude Code (/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/claude-code) Point Claude Code at Descope for Cross App Access to MCP servers. # Claude Code This guide configures Claude Code to use your Descope project as its Enterprise-Managed Authorization (XAA) identity provider. You sign in to Descope once, and Claude Code obtains an XAA token (ID-JAG) in the background whenever your agent calls an MCP server. There is no per-server login. Throughout this guide, the [identity provider](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags) Claude Code signs in to is your Descope project. Use your Descope **issuer URL** wherever a provider URL is required. Your issuer URL is `https://api.descope.com/`, or a different host if you use a [custom domain](/how-to-deploy-to-production/custom-domain) or different regional [base URL](/management/project-settings/multi-regional#descope-base-urls). ## Steps ### Turn on the feature Set `CLAUDE_CODE_ENABLE_XAA=1` in your shell profile so it persists. The gate is checked both when you run the commands below and when your agent connects to a server. ```bash export CLAUDE_CODE_ENABLE_XAA=1 ``` ### Connect to Descope Once This configures the one identity-provider connection that every server reuses. The simplest path uses [CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd): omit the client ID and secret, and Claude Code registers with Descope dynamically. ```bash claude mcp xaa setup --issuer ``` ![claude mcp xaa setup](/assets/claude-mcp-xaa-setup.webp) If you have pre-registered a confidential client in Descope for Claude Code, pass its credentials instead. Put the secret in the environment variable that `--client-secret` reads, then run setup. ```bash export MCP_XAA_IDP_CLIENT_SECRET='' claude mcp xaa setup --issuer --client-id --client-secret ``` The `--client-secret` flag takes no inline value. It reads the secret from `MCP_XAA_IDP_CLIENT_SECRET`. Add `--callback-port ` only if your Descope connection does not allow any loopback port for the browser sign-in. ### Sign into Descope This opens Descope in your browser and caches the session. ```bash claude mcp xaa login ``` If you cannot use a browser, pass a Descope-issued ID token directly instead: ```bash claude mcp xaa login --id-token ``` ### Add an MCP Server Give Claude Code the server's URL. Repeat this step for each server you want to use. If the server supports CIMD, omit the client ID and secret and Claude Code registers with the server's authorization server dynamically. ```bash claude mcp add --xaa --transport ``` If the server's authorization server does not support CIMD, you must supply a client ID and secret for it. These are **the client credentials at the MCP server's authorization server**, not your Descope credentials from the previous step. They are controlled by the server owner, not by Descope. ```bash claude mcp add --xaa --transport --client-id --client-secret ``` Set `--transport` to `http` or `sse` to match the server; only HTTP and SSE servers are supported. The `--client-secret` flag takes no inline value; it prompts you for the secret, or reads it from `MCP_CLIENT_SECRET`. This would be a different environment variable than the one in the [connect step](#connect-to-descope-once). ### Use the Server in Claude Code Your agent obtains a token in the background when it calls the server. ![claude mcp add](/assets/claude-xaa-completed.webp) There is nothing more to run. ## Manage Your Connection Use these commands to check or reset your Descope connection: - `claude mcp xaa show` checks your current connection. - `claude mcp xaa login --force` signs you in to Descope again, for example after your access was reset. - `claude mcp xaa clear` clears the connection so you can start over. ## Troubleshooting | What you see | What it means and who to ask | |---|---| | `XAA is not enabled (set CLAUDE_CODE_ENABLE_XAA=1)` | The gate is off. Set `CLAUDE_CODE_ENABLE_XAA=1` in your shell profile and restart your shell. | | `XAA: no IdP connection configured` | You have not connected to Descope yet. Run `claude mcp xaa setup`, then `claude mcp xaa login`. | | `XAA: server '' needs an AS client_id or a missing AS client secret` | The server is missing its client ID or secret. Re-run `claude mcp add --xaa` for that server. | | `Resource server does not implement OAuth 2.0 Protected Resource Metadata`, PRM discovery failed, or no authorization server supports `jwt-bearer` | The server does not publish the metadata Descope needs, or does not support the JWT bearer grant. Ask the server owner to finish setup. See [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). | | The token request is denied because you do not have the scope | Your access has not been granted. Ask your Descope admin to grant you the scope or access profile. | | The server rejects the token even though sign-in worked | The token's signature may use an algorithm the server does not verify. Ask your Descope admin to check the signing algorithm set for that server. | # Manage Agents in Your Enterprise (/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags) Connect Claude Code and VS Code to third-party MCP servers with XAA or a gateway. # Manage Agents in Your Enterprise Use this guide when **pre-built clients you do not control** — Claude Code, VS Code, Cursor, and similar — need to reach **third-party MCP servers and APIs you do not protect with Descope**, such as HubSpot, Asana, Linear, or Canva, over a standard protocol like MCP. The agent vendor and the tool vendor each have their own auth. Your enterprise still needs one place to decide which users and agents may reach which tools. Descope is that identity provider: clients sign in once to Descope, and Descope governs access to those third-party servers. What each agent may reach is decided by [policies](/agentic-identity-hub/policies). You can see and revoke every agent in [Agentic Identity](/agentic-identity-hub/core-components/agents). This page covers the two main ways to make that connection: **Cross App Access (XAA)** and a **gateway**. For building MCP servers or agents you write yourself, see [Use cases](/agentic-identity-hub/use-cases). ## Why this matters Without Cross App Access, every third-party MCP server becomes its own authorization island. Each agent prompts for a separate consent screen, and IT has no single answer to which agents can reach which tools for which users. The diagrams below use the same five MCP servers. Compare per-server consent with a single central IdP using XAA. ### Without Cross App Access (XAA) ### With Cross App Access (XAA) ## Two ways to connect | Approach | What it does | When it fits | | --- | --- | --- | | **[XAA](#enforce-with-xaa)** | Descope mints an XAA token (ID-JAG); the third-party AS validates it. Nothing between the agent and the tool. | The target supports Cross App Access / ID-JAG. | | **[Gateway](#enforce-with-a-gateway)** | Agent calls a gateway you run; Descope is IdP + policy; Connections hold credentials. | The target lacks XAA, **or** you want gateway security controls (prompt-injection detection, central inspection, unified audit, and so on) that XAA does not provide. | These are complementary, not an either/or. Many enterprises use XAA where the vendor supports it and a gateway for everything else — and sometimes a gateway even when XAA is available, when they want those extra controls in the path. } title="Connect with XAA" href="#enforce-with-xaa" description="Downstream supports Cross App Access. Descope mints a short-lived XAA token (ID-JAG) per request for Claude Code, VS Code, and similar clients." /> } title="Connect with a Gateway" href="#enforce-with-a-gateway" description="Put a gateway in the path for third-party servers without XAA, or when you want gateway-only security and routing features." /> ## Enforce with XAA Use this path when the target can validate an **XAA token** (ID-JAG) — typically a third-party MCP server or API that supports Cross App Access, or an MCP server **you** protect with Descope (Descope as both issuer and validator; see [Use cases → Governing agents internally](/agentic-identity-hub/use-cases#governing-agents-internally)). Descope is the IdP your pre-built agent signs into. When the agent needs a target Resource, Descope mints a short-lived assertion for it. The authorization server for that Resource validates the assertion against Descope's public keys and issues an access token. The user never sees a second consent screen on that tool. Because Descope mints the token per request, it enforces [policies](/agentic-identity-hub/policies) in the request path with nothing sitting between your agent and the resource. ### How it works Two things have to be true before an agent can reach a third-party server. The target has to be a **Resource** in Descope, and a **policy** has to allow the agent to request an XAA token (ID-JAG) for it. An XAA token grants access to a [Resource](/resources), and its `resource` claim is that Resource's URL, so you register the third-party MCP server as a Resource whose URL matches the server you actually connect to. This is a Resource, not a [Connection](/agentic-identity-hub/core-components/connections): you mint a signed assertion the downstream authorization server honors; you are not vaulting a token to replay later. Which Resources an agent may reach, and with which scopes, is decided by [policies](/agentic-identity-hub/policies) grounded in the real users in your tenant. An agent never gets broader access than the user it acts for. For the full token exchange (curl examples, claims, confidential clients, downstream registration), see [How XAA and ID-JAG work → When Descope is the IdP](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works#when-descope-is-the-idp). ### XAA setup ### Create an MCP Server Resource In the Descope Console, create a [Resource](/resources/managing-resources) of type **MCP Server**. Set the **MCP Server URL** to the **exact URL of the third-party MCP server** you want to reach. That URL becomes the `resource` claim of the ID-JAG, and it is the value your agent sends as `resource` when it requests the assertion. For example, if Canva's MCP server is `https://mcp.canva.com/mcp`, set the Resource's MCP Server URL to `https://mcp.canva.com/mcp`. The URL must match the downstream server exactly. A trailing-slash or path mismatch produces an assertion the downstream server will reject. ### Add the same scopes as the downstream server Define scopes on the Resource that are **identical to the scopes the downstream MCP server expects**. The ID-JAG, and the access token the downstream server mints from it, carries these scopes, so they must match what that server publishes. Check the third-party server's documentation or its OAuth metadata for the scope names, and mirror them on your Resource. ### Set Descope as the client's IdP (and pre-register it if needed) In your MCP client's Enterprise-Managed Authorization (Cross App Access) settings, set the identity provider to **Descope**, using your project issuer. The issuer is `/v1/apps/__ProjectID__`, where `` is your region's [Descope base URL](/management/project-settings/multi-regional#descope-base-urls) (for example `https://api.descope.com`), or your [custom domain](/how-to-deploy-to-production/custom-domain) if you have one set up for your Descope project. The ID-JAG carries the same issuer as your Descope access tokens. Whether you also pre-register a [client](/agentic-identity-hub/core-components/clients) depends on the MCP client: - Clients that support **[CIMD](/agentic-identity-hub/core-components/mcp-servers/registration-methods#client-id-metadata-documents-cimd)** (for example [Claude Code](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/claude-code) or [VS Code](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/vscode)) register with Descope automatically; there is nothing to create. - Clients that do **not** support CIMD must be pre-registered: create the client in Descope and hand its ID and secret to the MCP client. See the [XAA client setup guides](#xaa-client-setup-guides) for the exact per-client steps. ### Write a token-exchange policy Create a [policy](/agentic-identity-hub/policies) that governs who can obtain an ID-JAG for this Resource. Set the grant type to **Delegated access (token exchange)**, then define: - **Subject**: which agents or users the rule applies to (for example a client tag like `verified-agent`, or a user role). - **Target**: the MCP Server Resource you created, and the **allowed scopes** on it. Only subjects a policy allows can exchange their Descope token for an ID-JAG on that Resource, and only for the scopes the policy permits. Because the subject resolves to a real user in your tenant, an agent never gets broader access than that user has. The token exchange policy is the control point for XAA on the Descope side. It decides which ID-JAG clients reach which XAA-enabled Resources, so a client with no matching policy cannot obtain an assertion at all. This matters most for **manually registered** clients. Clients that arrive through [CIMD or DCR](/agentic-identity-hub/core-components/mcp-servers/registration-methods) are granted access to the Resource they registered with, but a client you create by hand in Descope starts with none. If you manually register an ID-JAG client, write the policy in this step or the exchange will fail. ### Exchange the token at runtime The agent calls the [token endpoint](/api/third-party-apps/token-endpoint) (RFC 8693) to exchange its Descope token for an ID-JAG scoped to the Resource, then presents that ID-JAG to the downstream server's authorization server to receive an access token. Clients like [Claude Code and VS Code](#xaa-client-setup-guides) do this for you automatically. ### XAA client setup guides These guides walk through configuring **Cross App Access (XAA)** on popular MCP clients with Descope as the identity provider. They are not gateway setup — for gateways, see [Enforce with a Gateway](#enforce-with-a-gateway). Once configured, the client signs in to Descope once and can obtain XAA tokens for every XAA-enabled MCP server you registered, without another prompt. The same XAA client setup applies when the MCP server is **yours** and Descope is both issuer and validator — see [Use cases → Governing agents internally](/agentic-identity-hub/use-cases#governing-agents-internally). } title="Claude Code" href="/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/claude-code" description="Configure Claude Code for XAA with Descope: enable Cross App Access, set the issuer, and add MCP servers from the CLI." /> } title="VS Code" href="/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/vscode" description="Configure VS Code for XAA with Descope: identity provider in settings.json and enterprise-managed auth in mcp.json." /> ## Enforce with a Gateway Gateway-based enterprise-managed authorization is in early access, and must be enabled by Descope Support for your project. A **gateway** sits between the pre-built client and third-party MCP servers or APIs. Use it when: - The third party does **not** support Cross App Access (most servers today — HubSpot, Asana, and similar often only speak ordinary OAuth or API keys), or - You want controls XAA alone does not provide: prompt-injection detection, inspection of tool calls in one place, unified audit, tenant-scoped credential routing, and similar gateway security features. Flow: 1. Claude Code, VS Code, or another client authenticates to Descope (same IdP story as XAA). 2. The agent calls **your gateway** as the MCP entrypoint, not HubSpot or Asana directly. 3. Descope policies decide whether that agent may use that gateway tool / downstream server. 4. The gateway pulls the right credential from [Connections](/agentic-identity-hub/core-components/connections) (OAuth tokens or API keys you or a tenant admin stored) and calls the third-party service. Descope remains the identity provider and policy decision point. The gateway is the enforcement — and often security — point in the request path. We work out of the box with [agentgateway](https://agentgateway.dev/), or you can bring your own gateway and configure it to use Descope as a PDP. See [MCP Gateways](/mcp/gateways) for how to model Resources, Connections, and policies when you build one. # VS Code (/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags/vscode) Configure VS Code Cross App Access with Descope for MCP servers. # VS Code VS Code authenticates to your Descope project once, then connects to your enterprise-managed MCP servers silently. There are two pieces of configuration: the identity provider, set once in `settings.json`, and each MCP server, listed in `mcp.json`. The [identity provider](/agentic-identity-hub/enterprise-managed-authorization/issue-id-jags) VS Code signs in to is your Descope project. Use your Descope **issuer URL** wherever a provider URL is required. Your issuer URL is `https://api.descope.com/`, or a different host if you use a [custom domain](/how-to-deploy-to-production/custom-domain), a different regional [base URL](/management/project-settings/multi-regional#descope-base-urls), or a Descope Private Cloud deployment. In a [managed organization for VS Code](https://code.visualstudio.com/docs/enterprise/policies), your admin usually delivers the identity-provider settings for you through an enterprise policy (such as: Windows Group Policy, macOS managed preferences, or `/etc/vscode/policy.json` on Linux). If you are setting things up individually, then put the configurations below in your local `settings.json` file. ## Steps ### Configure the identity provider In `settings.json`, set `mcp.enterpriseManagedAuth.idp` with the issuer URL and OIDC client credentials your Descope admin gives you. ```json "mcp.enterpriseManagedAuth.idp": { "issuer": "", "clientId": "", "clientSecret": "" } ``` If your admin delivers this through enterprise policy, it is already set and you can skip this step. ### Add each MCP server In `mcp.json`, list each server with its URL and an `oauth` block that turns on enterprise-managed authentication. ```json { "servers": { "": { "type": "http", "url": "", "oauth": { "clientId": "", "enterpriseManaged": true } } } } ``` The `oauth.clientId` here is the client ID at the **MCP server's** authorization server: the credential the server owner gives you, not the Descope identity-provider client ID from the previous step. ### Set the server's client secret Do not put the secret in `mcp.json`. Use the **Set Client Secret** code lens that appears above the `oauth` block to store it. VS Code keeps it in your operating system's secret store, not in the file. ### Sign in once The first time you connect to an enterprise-managed server, VS Code opens a browser to sign you in to Descope. After that, every enterprise-managed server connects silently, with no per-server prompt. ## When something doesn't work | What you see | What it means and who to ask | |---|---| | Sign-in never starts, or VS Code reports no identity provider | The Descope identity provider is not configured. Set `mcp.enterpriseManagedAuth.idp` in `settings.json`, or ask your admin whether it should arrive through enterprise policy. | | The server connects without enterprise-managed auth, or VS Code cannot discover its authorization server | The MCP server did not advertise protected-resource metadata or list its `authorization_servers`. Ask the server owner to finish setup. See [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). | | The connection is denied because you do not have the scope | Your access has not been granted. Ask your Descope admin to grant you the scope or access profile. | | The server rejects the token even though sign-in worked | The token's signature may use an algorithm the server does not verify. Ask your Descope admin to check the signing algorithm set for that server. | # Components Reference (/management/styles/code-mode/components-reference) How the components object in Descope Code Mode is structured, every shared state and variant key, and a categorized index of all component keys. # Components Reference The `components` object in a Code Mode style holds per-component overrides. Most individual CSS variables are easy to understand from their names, so instead of listing every one, this page explains the **pattern** they all follow and gives a categorized index so you can find the right key quickly. For `globals` (colors, typography, spacing, and so on), see the [Globals Reference](/management/styles/code-mode/globals-reference). For specific quirks, like badges, logos, and the required-field indicator, see [Advanced Styling Examples](/management/styles/advanced-styling-examples). A component's keys don't appear automatically in Code Mode. You can either add them manually following the pattern below, or make the change in the GUI first, which will generate the corresponding keys in Code Mode. ## Structure Every component follows the same three-layer shape: 1. **Base properties**: flat CSS custom properties that always apply. 2. **State modifiers**: keys starting with `_` that apply only when that state is active, like hover, disabled, or focused. 3. **Dimensions**: named object keys, like `mode` or `size`. We call these "dimensions" in this doc because each one is an independent axis of configuration, a component can be a certain mode *and* a certain size *and* a certain variant, all at once, each set separately. A dimension's sub-keys are the options you can pick along that axis; only the sub-key matching the component's current setting applies. Any of these can nest inside each other. An option inside a dimension can contain its own state modifiers, and the other way around too. Here's `button` with all three layers, trimmed to the essentials: ```json { "components": { "button": { "--descope-button-border-radius": "var(--descope-radius-sm)", "--descope-button-cursor": "pointer", "_disabled": { "--descope-button-main": "var(--descope-colors-surface-light)" }, "mode": { "primary": { "--descope-button-main": "var(--descope-colors-primary-main)", "_disabled": { "--descope-button-main": "var(--descope-colors-surface-dark)" } } } } } } ``` Here, the base properties always apply. `_disabled` only applies when the button is disabled. `mode` is the dimension here, and `primary` is one of the options it holds, so `mode.primary` only applies when the button's mode is set to primary. Nested inside it, `_disabled` sets the disabled color just for that combination. `mode` isn't special, it's just one dimension. `button` also has a `size` dimension, whose options are `sm`, `md`, `lg`, and so on, and a `variant` dimension, whose options are `contained`, `outline`, and `link`. So `button.size.lg` overrides the large-size button, and `button.variant.outline` overrides the outline-style button, the same way `button.mode.primary` overrides the primary-color button: ```json { "components": { "button": { "size": { "lg": { "--descope-button-font-size": "18px" } }, "variant": { "outline": { "--descope-button-border-color": "var(--descope-button-main)" } } } } } ``` ## State Modifiers These `_`-prefixed keys recur across many components. Not every component supports every modifier. | Modifier | Applies when | |---|---| | `_fullWidth` | The component is set to stretch to its container's width | | `_bordered` | A border is enabled on the component | | `_disabled` | The component is disabled | | `_focused` | The component has keyboard/input focus | | `_hover` | The pointer is hovering the component | | `_active` | The component is in an active/pressed state | | `_checked` | A checkbox, radio, or toggle is checked/on | | `_selected` | An item is the selected one in a list or group | | `_invalid` | The component's value has failed validation | | `_readonly` / `_readOnly` | The component is read-only. Casing varies by component, so match what that component's block uses | | `_loading` | The component is in a loading state | | `_empty` | The component has no content or items | | `_hidden` | The component is hidden | | `_required` | The component is marked required, exposing its required-indicator variable | | `_square` | The component renders as a fixed square (icon-only buttons) | | `_editable` | The component allows inline editing (for example, an editable avatar) | | `_obfuscated` | Input content is masked, as with password fields | | `_collapsible` | A container can be expanded/collapsed | | `_fillTitle` | A collapsible container's title area grows to fill available space | | `_border` | Used only by `collapsibleContainer` for its border. Note this is singular, not `_bordered` | | `_shrinkToIndicator` | A badge collapses to a small dot indicator instead of showing text | | `_hideCursor` | Hides the text caret, used in the passcode component | | `_italic` / `_lowercase` / `_uppercase` | Text style transforms on the `text` component | | `_horizontal` | Lays a component out horizontally instead of the default direction (used by `timerButton`, `countrySubdivisionCityField`) | | `_vertical` | Lays a component out vertically instead of the default direction (used by `divider`) | | `_timerInside` | Renders a timer inline inside its parent button rather than beside it | | `_iconFillCurrentColor` | An icon inherits `currentColor` instead of its own fill | | `_hasValue` | A floating-label input currently has a value (used to keep the label raised) | | `_hideWhenEmpty` | Hides the component completely when it has no content. Nests inside `_empty` rather than standing on its own (used by `enrichedText`) | ## Dimensions These named object keys group related overrides under enumerated sub-keys, as described in [Structure](#structure) above. | Dimension | Typical options | Used by | |---|---|---| | `mode` | Color intent. Varies per component, commonly `primary`, `secondary`, `error`, `success`, `warning`, `default` | `alert`, `badge`, `button`, `link`, `loaderLinear`, `loaderRadial`, `notificationCard`, `text` | | `size` | `xs`, `sm`, `md`, `lg`, `xl`, `2xl` (a component uses whichever subset applies) | `avatar`, `badge`, `button`, `calendar`, `inputWrapper`, `notificationCard`, `passcode`, `radioButton`, `timer`, `uploadFile`, and others | | `variant` | A component's alternate visual forms, e.g. `button`: `contained`/`outline`/`link`; `list`: `tiles`; `listItem`: `tile`; `text`: `h1`-`body2` | `badge`, `button`, `list`, `listItem`, `text` | | `shadow` | `sm`, `md`, `lg`, `xl`, `2xl` | `badge`, `collapsibleContainer`, `container`, `tooltip` | | `textAlign` | `left`, `center`, `right` | `alert`, `button`, `link`, `radioGroup`, `recoveryCodes`, `text`, `textArea`, `textField`, `timer` | | `borderRadius` | `sm`, `md`, `lg`, `xl`, `2xl`, `3xl` | `collapsibleContainer` (also used by `container`, but see the note below) | | `spacing` / `spaceBetween` | `xs`-`xl` sizing steps for gaps and internal padding | `alert`, `collapsibleContainer`, `container` | | `gap` | `xs`-`xl` sizing steps for the space between list items | `list` | | `horizontalPadding` / `verticalPadding` | `sm`, `md`, `lg` | `collapsibleContainer`, `container` | | `itemPadding` | `xs`-`xl` | `appsList` | | `iconPosition` | `left`, `right` | `collapsibleContainer` | | `position` | Anchor points like `top-start`, `top-center`, `top-end`, `bottom-start`, `bottom-center`, `bottom-end` | `attachment` | | `direction` | `column`, `row` (each with nested `horizontalAlignment`/`verticalAlignment`) | `container` | | `horizontalAlignment` / `verticalAlignment` | `start`, `center`, `end` | `container` | | `labelType` | `floating`, `static` | `inputWrapper`, `multiSelectComboBox` | | `data-descope-provider` | Social login provider keys, e.g. `apple` | `button` (for provider-specific hover/focus styling) | | `score` | `0`-`4` password strength levels | `passwordStrength` | | `timerPosition` | `end` (default is the unset/start position) | `timerButton` | | `enabled` | `true`/`false` | `hcaptcha` | | `id` | Specific instance targeting, e.g. `"ROOT"` for a screen's root container | `container` | ## Available Components | Key | Styles | |---|---| | `inputWrapper` | Shared base tokens most text-style inputs inherit from | | `textField` | Single-line text input | | `textArea` | Multi-line text input | | `emailField` | Email input | | `password` | Password input (login) | | `newPassword` | Password input with policy preview (signup/reset) | | `numberField` | Numeric input | | `phoneField` | Phone number input with country code dropdown | | `phoneInputBoxField` | Phone number input, boxed variant | | `dateField` | Date picker input | | `monthDayField` / `monthDayFieldPicker` | Month/day picker input and its calendar overlay | | `addressField` | Address input | | `countrySubdivisionCityField` | Country/state/city grouped input | | `autocompleteField` | Text input with autocomplete suggestions | | `comboBox` / `multiSelectComboBox` | Dropdown select (single and multi) | | `hybridField` | Field that can switch input types | | `mappingsField` / `multiLineMappings` / `samlGroupMappings` | Key-value mapping inputs (attribute/group mappings) | | Key | Styles | |---|---| | `checkbox` | Checkbox input | | `radioButton` / `radioGroup` | Radio input and its group wrapper | | `switchToggle` | On/off toggle switch | | `buttonSelectionGroup` / `buttonSelectionGroupItem` | Single-select button group and its items | | `buttonMultiSelectionGroup` | Multi-select button group | | Key | Styles | |---|---| | `button` | Primary action button, including social provider variants | | `outboundAppButton` | Button used to launch an outbound app connection | | `timerButton` | Button paired with a countdown timer (resend code, and so on) | | `link` | Inline text link | | Key | Styles | |---|---| | `passcode` | OTP/passcode digit input | | `passwordStrength` | Password strength meter | | `policyValidation` | Password policy checklist | | `recoveryCodes` | Recovery code display | | `securityQuestionsSetup` / `securityQuestionsVerify` | Security question setup and verification screens | | `totpImage` / `notpImage` | TOTP/notification placeholder images | | `hcaptcha` | hCaptcha widget container | | `userPasskeys` | Passkey management list | | `userAuthMethod` | Auth method management list | | `userAttribute` | User attribute display row | | `trustedDevices` | Trusted device management list | | `scopesList` | OAuth scope consent list | | `multiSso` | Multi-SSO tenant picker | | `outboundApps` | Outbound app connection list | | `thirdPartyAppLogo` | Paired logo display for OAuth consent screens | | Key | Styles | |---|---| | `container` | General-purpose layout container, including the screen root | | `modal` | Modal overlay | | `collapsibleContainer` | Expand/collapse section | | `divider` | Horizontal or vertical rule | | Key | Styles | |---|---| | `alert` | Inline alert message | | `notificationCard` | Toast-style notification | | `loaderLinear` / `loaderRadial` | Linear progress bar and spinner | | `badge` | Status badge/pill | | `tooltip` | Hover tooltip | | `timer` | Standalone countdown display | | Key | Styles | |---|---| | `grid` | Data grid/table | | `list` / `listItem` | List container and its items | | `appsList` | Application/tenant list | | `calendar` | Calendar/date grid | | `codeSnippet` | Syntax-highlighted code block | | `filter` | Filter row builder | | Key | Styles | |---|---| | `logo` | Project logo and its fallback | | `image` | Generic image component | | `icon` | Generic icon component | | `avatar` | User avatar | | `attachment` | Positioned overlay attached to another element | | `uploadFile` | File upload dropzone | | Key | Styles | |---|---| | `text` | Text component, mapped to the `globals.typography` variants | | `enrichedText` | Rich text with inline links/formatting | ## Examples These are complete, real component blocks. They aren't trimmed, so you can see how base properties, state modifiers, and dimensions actually combine in practice. `button` is the richest example in a style file: base properties, five state modifiers, a provider-specific hover state, five color modes, four sizes, three text alignments, and three variants. ```json { "components": { "button": { "--descope-button-border-radius": "var(--descope-radius-sm)", "--descope-button-cursor": "pointer", "--descope-button-font-family": "var(--descope-fonts-font1-family)", "--descope-button-host-height": "3em", "--descope-button-icon-size": "1.5em", "_disabled": { "--descope-button-contrast": "var(--descope-colors-surface-main)", "--descope-button-main": "var(--descope-colors-surface-light)" }, "_focused": { "--descope-button-outline-color": "var(--descope-button-light)" }, "_fullWidth": { "--descope-button-host-width": "100%" }, "_square": { "--descope-button-host-height": "3em", "--descope-button-host-width": "3em" }, "data-descope-provider": { "apple": { "mode": { "primary": { "_hover": { "--descope-button-background-color": "var(--descope-colors-warning-highlight)" } } } } }, "mode": { "primary": { "--descope-button-contrast": "var(--descope-colors-primary-contrast)", "--descope-button-main": "var(--descope-colors-primary-main)", "--descope-button-dark": "var(--descope-colors-primary-dark)" }, "error": { "--descope-button-contrast": "var(--descope-colors-error-contrast)", "--descope-button-main": "var(--descope-colors-error-main)", "--descope-button-dark": "var(--descope-colors-error-dark)" } }, "size": { "md": { "--descope-button-font-size": "16px" }, "lg": { "--descope-button-font-size": "18px" } }, "variant": { "contained": { "--descope-button-background-color": "var(--descope-button-main)", "--descope-button-label-text-color": "var(--descope-button-contrast)", "_hover": { "--descope-button-background-color": "var(--descope-button-dark)" } }, "outline": { "--descope-button-border-color": "var(--descope-button-main)", "--descope-button-label-text-color": "var(--descope-button-main)" } } } } } ``` Note the pattern inside `mode`: each mode sets generic `-main`/`-dark`/`-contrast` variables on the button itself. `variant.contained` then reads those generic variables instead of a specific color palette. This is what lets the same `contained` variant definition work correctly no matter which `mode` is active. `checkbox` is a good example of a component that does almost no color or spacing work on its own. It just points at `inputWrapper`'s tokens. That's why editing `inputWrapper` changes every field type at once: ```json { "components": { "checkbox": { "--descope-checkbox-font-family": "var(--descope-input-wrapper-font-family)", "--descope-checkbox-font-size": "var(--descope-input-wrapper-font-size)", "--descope-checkbox-host-direction": "var(--descope-input-wrapper-direction)", "--descope-checkbox-input-background-color": "var(--descope-input-wrapper-background-color)", "--descope-checkbox-input-border-color": "var(--descope-input-wrapper-border-color)", "--descope-checkbox-input-size": "1.35em", "--descope-checkbox-label-text-color": "var(--descope-input-wrapper-label-text-color)", "--descope-checkbox-label-required-indicator": "var(--descope-input-wrapper-required-indicator)" } } } ``` To restyle the border and background of every checkbox, text field, dropdown, and date picker at once, edit `inputWrapper` instead of each field type on its own. `badge` shows a variant dimension (`variant.contained`) that nests its own `mode` dimension inside it, overriding only how `primary` mode looks specifically when the badge is `contained`: ```json { "components": { "badge": { "--descope-badge-border-radius": "var(--descope-radius-xs)", "--descope-badge-font-family": "var(--descope-fonts-font1-family)", "_shrinkToIndicator": { "--descope-badge-border-radius": "50%", "--descope-badge-host-height": "13px", "--descope-badge-host-width": "13px", "--descope-badge-text-indent": "-9999px" }, "mode": { "primary": { "--descope-badge-text-color": "var(--descope-colors-primary-main)", "_bordered": { "--descope-badge-border-color": "var(--descope-colors-primary-light)" } } }, "size": { "md": { "--descope-badge-font-size": "16px" } }, "variant": { "contained": { "--descope-badge-background-color": "var(--descope-colors-surface-dark)", "mode": { "primary": { "--descope-badge-text-color": "var(--descope-colors-surface-main)" } } } } } } } ``` Without the `variant.contained.mode.primary` override, a contained primary badge would inherit the outer `mode.primary` text color. That color wouldn't have enough contrast against the solid background `variant.contained` sets. This is the general pattern for handling "this combination of options needs its own tweak": nest the more specific dimension inside the less specific one. # Globals Reference (/management/styles/code-mode/globals-reference) A complete reference of every color and typography token available in Descope Code Mode, with the JSON keys and CSS variables for each. # Globals Reference [Code Mode](/management/styles#code-mode) lets you edit a Descope style as raw JSON instead of using the GUI. This page lists `globals` object keys, which include the `colors` and `typography` keys that apply to every screen and component in a style. For component-level keys, like badges, logos, and input states, see [Components Reference](/management/styles/code-mode/components-reference). ## Where to Edit These Values 1. Open the [Styles tab](https://app.descope.com/styles) in the Descope Console. 2. Select a style file, then toggle **Code Mode** on. 3. Edit `globals.colors` or `globals.typography` directly, or change a value in the GUI first to reveal its corresponding key in Code Mode. ## Structure Every style's globals follow this shape. Each block below is a real, complete example, not a placeholder: ```json { "globals": { "colors": { "primary": { "main": "#6D829CFF", "light": "#ACB8C7", "dark": "#404E5F", "highlight": "#EBEEF2", "contrast": "#FFFFFF" } }, "fonts": { "font1": { "label": "Roboto", "family": ["Roboto", "ui-sans-serif", "system-ui", "Arial", "sans-serif"], "url": "https://fonts.googleapis.com/css?family=Roboto:100,200,300,400,500,600,700,800,900" } }, "typography": { "h1": { "font": "var(--descope-fonts-font2-family)", "size": "60px", "weight": "300" } }, "spacing": { "xs": "2px", "sm": "4px", "md": "8px", "lg": "16px", "xl": "32px" }, "radius": { "xs": "5px", "sm": "10px", "md": "15px", "lg": "20px", "xl": "25px", "2xl": "30px", "3xl": "35px" }, "border": { "xs": "1px", "sm": "2px", "md": "3px", "lg": "4px", "xl": "5px" }, "shadow": { "narrow": { "sm": "0 1px 2px -1px", "md": "0 2px 4px -2px", "lg": "0 4px 6px -4px", "xl": "0 8px 10px -6px", "2xl": "0 16px 16px -8px" } }, "direction": "ltr" } } ``` ## Colors There are six color palettes. Each one maps to a section of the **Colors** panel in the GUI, and each uses the same five shade keys. | Key | GUI label | Typical use | |---|---|---| | `primary` | Primary | Your main brand color, used for buttons, links, and focus states | | `secondary` | Secondary | Secondary actions and accents | | `success` | Success | Success messages and confirmations | | `warning` | Warning | Warnings and cautionary states | | `error` | Error | Error messages and destructive actions | | `surface` | Greys | Neutral tones used for backgrounds, borders, and surfaces | Each palette above takes the same five shade keys: | Shade key | Description | |---|---| | `main` | The base color for the palette | | `light` | A lighter variant of `main` | | `dark` | A darker variant of `main` | | `highlight` | Used for hover and highlight states | | `contrast` | A contrasting color, typically used for text or icons placed on top of `main` | Full path format: `globals.colors..`. For example: `globals.colors.primary.main`. ```json { "globals": { "colors": { "primary": { "main": "#124990FF", "dark": "#0B2C56", "light": "#1966CA", "highlight": "#3D87E7", "contrast": "#FFFFFF" }, "secondary": { "main": "#BCAECAFF", "dark": "#715988", "light": "#FFFFFF", "highlight": "#FFFFFF", "contrast": "#000000" }, "surface": { "main": "#EBEBEBFF", "dark": "#8D8D8D", "light": "#FFFFFF", "highlight": "#FFFFFF", "contrast": "#000000" }, "success": { "main": "#00B100FF", "dark": "#006A00", "light": "#00F800", "highlight": "#40FF40", "contrast": "#FFFFFF" }, "warning": { "main": "#F8E71CFF", "dark": "#A19505", "light": "#FBF287", "highlight": "#FFFEF2", "contrast": "#000000" }, "error": { "main": "#C71D12FF", "dark": "#77110B", "light": "#EE4C42", "highlight": "#F59892", "contrast": "#FFFFFF" } } } } ``` ## Typography Typography is built from two font family slots and seven preset text variants. ### Font families `globals.fonts` holds two slots, each a full object rather than a single value: | Property | Description | |---|---| | `label` | Display name shown in the GUI font picker | | `family` | Ordered CSS font stack (the chosen font plus system fallbacks) | | `url` | Only present for custom or Google-hosted fonts. A stylesheet URL the flow loads to fetch the font file | ```json { "globals": { "fonts": { "font1": { "label": "Roboto", "family": ["Roboto", "ui-sans-serif", "system-ui", "Arial", "sans-serif"], "url": "https://fonts.googleapis.com/css?family=Roboto:100,200,300,400,500,600,700,800,900" }, "font2": { "label": "Sans Serif", "family": ["ui-sans-serif", "system-ui", "-apple-system", "Arial", "sans-serif"] } } } } ``` Each slot's `family` array generates the `--descope-fonts-font1-family` and `--descope-fonts-font2-family` CSS variables. Typography and component tokens reference these with `var(...)`. To add a custom font, follow the [Custom Fonts](/management/styles#custom-fonts) steps in the GUI first. Code Mode only shows the result. It isn't the place to write new `@font-face` rules directly. ### Text variants Each variant accepts the same three properties: `font`, `size`, and `weight`. | JSON key | GUI label | |---|---| | `h1` | Heading 1 | | `h2` | Heading 2 | | `h3` | Heading 3 | | `subtitle1` | Subtitle 1 | | `subtitle2` | Subtitle 2 | | `body1` | Body 1 | | `body2` | Body 2 | Property reference: | Property | Type | Example | |---|---|---| | `font` | CSS variable reference | `var(--descope-fonts-font2-family)` | | `size` | Pixel value, as a string | `"60px"` | | `weight` | CSS `font-weight` value, as a string | `"300"` | ### Font weight values The GUI exposes nine named weight increments. In Code Mode, set the raw numeric value: | GUI label | `weight` value | |---|---| | Thin | `100` | | Extra Light | `200` | | Light | `300` | | Regular | `400` | | Medium | `500` | | Semi Bold | `600` | | Bold | `700` | | Extra Bold | `800` | | Black | `900` | A variant only needs the properties you're overriding. Anything you omit falls back to the style's default: ```json { "globals": { "fonts": { "font1": { "family": [ "Poppins", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ], "label": "Poppins", "url": "https://fonts.googleapis.com/css?family=Poppins:100,200,300,400,500,600,700,800,900" }, "font2": { "family": [ "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" ], "label": "Sans Serif" } }, "typography": { "h1": { "font": "var(--descope-fonts-font1-family)" }, "h2": { "size": "40px", "weight": "700" }, "h3": { "size": "30px" }, "body1": { "size": "15px" }, "body2": { "size": "12px" }, "subtitle2": { "size": "22px" }, "subtitle1": { "size": "24px" } } } } ``` ## Spacing `globals.spacing` defines five step sizes used for padding, gaps, and margins across components. Components use them as `var(--descope-spacing-)`: | Key | Value | |---|---| | `xs` | `2px` | | `sm` | `4px` | | `md` | `8px` | | `lg` | `16px` | | `xl` | `32px` | ## Corner Radius `globals.radius` defines seven roundness steps, used as `var(--descope-radius-)`: | Key | Value | |---|---| | `xs` | `5px` | | `sm` | `10px` | | `md` | `15px` | | `lg` | `20px` | | `xl` | `25px` | | `2xl` | `30px` | | `3xl` | `35px` | ## Border Width `globals.border` defines five stroke widths, used as `var(--descope-border-)`: | Key | Value | |---|---| | `xs` | `1px` | | `sm` | `2px` | | `md` | `3px` | | `lg` | `4px` | | `xl` | `5px` | ## Shadow `globals.shadow` defines two families of shadow fragments, `narrow` and `wide`. Each one has five sizes: `sm`, `md`, `lg`, `xl`, and `2xl`. These are partial `box-shadow` values (offset and blur, no color). A component combines one of these with its own `-shadow-color` variable when it renders: ```json { "globals": { "shadow": { "wide": { "sm": "0 2px 3px -0.5px", "md": "0 4px 6px -1px" }, "narrow": { "sm": "0 1px 2px -1px", "md": "0 2px 4px -2px" } } } } ``` A component typically layers both, for example: ```json "--descope-container-box-shadow": "var(--descope-shadow-wide-lg) var(--descope-container-shadow-color), var(--descope-shadow-narrow-lg) var(--descope-container-shadow-color)" ``` ## Direction `globals.direction` is a single value, `"ltr"` or `"rtl"`, that drives the `--descope-direction` variable most components inherit for `host-direction`. Set this once to flip layout for right-to-left languages, instead of overriding direction on each component. ## Applying a Color to a Component Colors defined in `globals.colors` are used elsewhere in the theme through CSS variable references, not hard-coded values. For example, a notification card references the error palette like this: ```json { "components": { "notificationCard": { "mode": { "error": { "--descope-notification-card-background-color": "var(--descope-colors-error-main)", "--descope-notification-card-border-color": "var(--descope-colors-error-light)" } } } } } ``` This is why editing a `globals.colors` shade updates every component that references it. The color only lives in one place. See [Advanced Styling Examples](/management/styles/advanced-styling-examples) for more patterns like this at the component level. # Create or Modify Tenant (/management/tenant-management/handling-tenants-in-flows/add-attributes-to-tenant) Create a tenant or update tenant attributes using Descope Flow actions. # Create or Modify Tenant Use Flow actions to create a tenant during signup or update an existing tenant's attributes. This guide focuses on tenant management in Flows in the Descope Console. For SDK / API tenant management, see [Tenant Management](/management/tenant-management). ## Creating Tenants in Flows The **Create Tenant** action creates a new tenant during user signup if one doesn't already exist. You can configure several attributes during tenant creation: - **Tenant Name**: The display name for the tenant - **Self Provision Domains**: Domains associated with the tenant - **Tenant ID**: A unique identifier for the tenant - **Roles**: Permissions assigned to the tenant These fields support dynamic values using the `{{}}` syntax. ![create tenant action](/assets/add-attributes-to-tenant.webp) ## Modifying Tenants in Flows The **Update Tenant** action updates an existing tenant with new attributes. You can configure several attributes during tenant modification in a previous screen: - **Tenant Name**: The display name for the tenant - **Self Provision Domains**: Domains associated with the tenant ![screen collection](/assets/screen-collection-of-update-tenant-attributes.webp) ![update tenant action](/assets/update-tenant-action.webp) ## Example Flow Here's an example flow that creates a tenant and assigns user permissions: ![example flow with create tenant action](/assets/add-attributes-to-tenant-flow.webp) This flow demonstrates how to: 1. Create a new tenant with custom attributes 2. Add users to the tenant 3. Update roles and permissions as needed If a tenant already exists for a user's domain, the flow can update their roles and attributes instead of creating a new tenant. # Add User to Tenant (/management/tenant-management/handling-tenants-in-flows/add-user-to-tenant) Assign a user to a tenant in a Descope Flow with Update User / Add Tenant or User / Invite. # Add User to Tenant There are two ways in a flow to assign a user to a tenant. Choose based on how you expect membership to be determined. ## Using the Update User / Add Tenant Action This action updates the user's tenant based on the tenant's email domain. ![update user action](/assets/add-user-to-tenant.webp) To ensure this works, set the email domain on the tenant level. ![update user action](/assets/add-user-to-tenant-tenant.webp) ## Using the User / Invite Action This action adds the invitee to tenants based on the inviter's tenants. ![invite user action](/assets/add-user-to-tenant-invite.webp) This action allows you to invite a user using a [connector](/connectors), as well as controlling the email template that is being sent and the URL the invitee will receive in that message. This action also allows you to choose the Application that will be used to sign-in/up the invitee. Moreover, this allows you to control the roles the invitee will acquire while accepting the invite. For more details on the user invite flows, see [this guide](/management/user-management/invite-users#descope-flows). # Handling Tenants in Flows (/management/tenant-management/handling-tenants-in-flows) Create and update tenants, and add users to tenants, using Descope Flows. # Handling Tenants in Flows Use Descope Flows to create or update tenants and to attach users to them during onboarding or invite flows. | Goal | Guide | | ---- | ----- | | Create or modify a tenant | [Create or Modify Tenant](/management/tenant-management/handling-tenants-in-flows/add-attributes-to-tenant) | | Add a user to a tenant | [Add User to Tenant](/management/tenant-management/handling-tenants-in-flows/add-user-to-tenant) | | Let the user pick a tenant on a screen | [Tenant Select component](/flows/screens/inputs/tenantselect-component) | For the broader tenant model, see [Tenant Management](/management/tenant-management) and the [B2B guide](/b2b). # SCIM with Azure (/management/tenant-management/scim/azure-scim) Learn how to set up SCIM provisioning between Microsoft Entra ID (Azure) and Descope to automate user and group management. # SCIM Provisioning with Microsoft Entra ID (Azure) This guide describes how to configure SCIM provisioning between **Microsoft Entra ID (formerly Azure AD)** and **Descope**, enabling Azure to automatically create, update, deactivate, and manage groups for users in your Descope tenant. ## Prerequisites Azure only pushes users to Descope if they are **assigned to the Enterprise Application** — either directly or as a member of a group that is assigned to the app. With JIT disabled, a user who is not assigned in Azure cannot log in to your application — Azure rejects authentication for unassigned users. (In multi-app setups where SSO and SCIM use different Azure applications, authentication can succeed via the SSO app but Descope reports the user as unknown because no SCIM record exists.) For ongoing onboarding, the recommended pattern is to assign a **group** (for example, an "All Employees" group or a department group) to the Enterprise Application rather than assigning users one by one. New employees added to the group are then provisioned automatically by Azure's next provisioning cycle. See [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices) for the full pattern. Before starting: * A **tenant** must exist in Descope with an associated **Access Key** that has the `Tenant Admin` role. * The Azure Enterprise Application must have assigned users and/or groups. SSO does not need to be enabled or configured for the tenant: SCIM provisioning works on its own, including for tenants with no SSO configuration at all. Group mapping, defined in the tenant's Roles & Groups tab, and attribute mapping, defined in the tenant's SSO settings, both apply to SCIM even while SSO stays disabled. See the [SCIM Management overview](/management/tenant-management/scim) for details. Entra runs SCIM on a **~40 minute** cycle (not configurable). Creates and updates often land later than expected. **Disabled users** usually are not pushed to Descope until that cycle includes them — or until you run **Provision on Demand** / a provisioning job manually. See [Microsoft's docs](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/application-provisioning-when-will-provisioning-finish-specific-user) and [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices#provisioning-timing). ## Step 1: Enable SCIM Provisioning in Azure 1. In the Azure portal, go to your **Enterprise Application** connected to Descope. 2. Navigate to **Provisioning** in the left sidebar. 3. Click **Get Started** to begin SCIM setup. ![Enable SCIM in Azure](/assets/enable-scim-provisioning-within-azure1.webp) ![SCIM Setup Step 2 in Azure](/assets/enable-scim-provisioning-within-azure2.webp) ## Step 2: Configure the SCIM Endpoint and Authentication 1. Set the **Provisioning Mode** to `Automatic`. 2. Fill in the following values: | Field | Value | | ---------------- | --------------------------------------------------------------------------------- | | **Tenant URL** | `__BaseURL__/scim/v2` | | **Secret Token** | `ProjectID:AccessKey` (must be scoped to the tenant with the `Tenant Admin` role) | The Tenant URL can be found in the SCIM tab of your tenant's authentication settings in the Descope Console. ![Tenant URL](/assets/scim-tab.webp) 3. Click **Test Connection** to verify. 4. Click **Save**. ![Azure SCIM configuration](/assets/scim-configuration-within-azure.webp) ## Step 3: Configure Attribute Mappings Once saved, navigate to the **Mappings** section. ### User Mappings Azure should map standard user fields such as `givenName`, `surname`, `email`, and `userPrincipalName` to the SCIM schema. ![Azure SCIM user mappings](/assets/scim-azure-user-mappings.webp) ### Group Mappings Azure can also manage SCIM group creation, updates, and deletions. Groups pushed from Azure will appear in Descope and can be mapped to roles. ![Azure SCIM group mappings](/assets/scim-azure-group-mappings.webp) ## Step 4: Start or Test Provisioning You can test provisioning by: * Selecting **Provision on Demand** for individual users. * Starting full provisioning from the main **Provisioning** panel. ![Start provisioning in Azure](/assets/scim-start-azure-provisioning.webp) ![On-demand provisioning in Azure](/assets/scim-on-demand-azure-provisioning.webp) Once provisioning is active, Entra pushes users and groups to Descope on its schedule (~40 minutes), not on the user's next SSO login. Use **Provision on Demand** when you need an immediate create, update, or disable to land in Descope. # SCIM (/management/tenant-management/scim) Learn how to provision and manage users and groups using SCIM with Descope, including setup guides for Okta, Azure, and other IdPs. # SCIM Management Descope supports [SCIM 2.0 (System for Cross-domain Identity Management)](https://datatracker.ietf.org/doc/html/rfc7644), enabling identity providers (IdPs) such as Okta, Azure, Ping Identity, and others to automatically provision, update, and deprovision users and groups in your Descope project. Once SCIM provisioning is configured, updates made in the IdP—such as user creation, profile edits, group assignments, or deactivation—are automatically pushed to Descope. These updates are applied to user sessions the next time the user logs in or refreshes their session token (JWT). SCIM enables centralized identity lifecycle management and ensures that Descope remains consistent with your IdP's directory. SCIM provisioning does not require SSO to be enabled for the tenant. Access key creation, the IdP's connection test, and user and group sync all work even when SSO was never set up. Group mapping is configured in the Roles & Groups tab of the tenant's authentication settings, and attribute mapping is configured in the SSO tab. Both can be configured even when SSO is disabled. See [Group and Attribute Mapping](#group-and-attribute-mapping) below. Users created via SCIM while SSO is disabled are marked as SCIM-provisioned users only, not as SSO users. ## Who Gets Provisioned: Assignment in the IdP A common misconception is that enabling SCIM means "every user in the IdP can now log in". That is not how SCIM works. Provisioning is driven entirely by the IdP — a user only appears in Descope after the IdP decides to push them, which happens when the user is **assigned to the application on the IdP side** representing Descope (directly, or by being a member of a group that is assigned to the app). Different IdPs name this differently — Azure calls it an *Enterprise Application*, Okta calls it an *app integration* — but the concept is the same. This means: * **Descope cannot provision a user on its own.** Descope is the SCIM service provider; it only receives what the IdP sends. * **A new employee joining your customer's organization is not automatically provisioned to your app.** Their IT admin must assign that employee to the application on the IdP side (in Azure, Okta, etc.). * **If a user is missing in Descope when JIT is disabled, the cause is almost always that the user was not assigned to the app in the IdP.** In most setups this blocks login entirely — the IdP rejects authentication for unassigned users before Descope is reached. In setups where SSO and SCIM use different IdP applications, authentication can succeed via the SSO app and Descope will then report the user as unknown because no SCIM record exists. See the [SCIM Best Practices guide](/management/tenant-management/scim/scim-best-practices) for the recommended onboarding pattern using group-based assignment, and the [SSO troubleshooting guide](/other-troubleshooting/sso-troubleshooting#scim-configuration-issues) for diagnosing provisioning failures. ### Group Mapping vs. Provisioning These two concepts are often confused but are completely separate: | Mechanism | Where it lives | What it does | | ------------------------------------ | -------------- | --------------------------------------------------------------------------------------------------------- | | **Provisioning** (who exists) | IdP | Decides which users get pushed to Descope, based on who is assigned to the application on the IdP side. | | **Group → Role Mapping** (what they can do) | Descope (Roles & Groups tab) | Once a user is provisioned, maps the groups the IdP sent to Descope Roles included in the user's JWT. | A user mapped to a role via group mapping does **not** get provisioned because of the mapping. The IdP must first assign the user to the app and push them. Only then does Descope apply role mapping to the groups the IdP sent. ## SCIM vs. JIT Provisioning You can disable JIT provisioning if you would rather rely on just SCIM for user management. However, you can still use JIT provisioning to create users as they log in simultaneously with SCIM, which can be useful for certain use cases. Descope supports both SCIM and Just-In-Time (JIT) provisioning via SSO. | Provisioning Method | Recommended Use Case | | ------------------- | ----------------------------------------------------------------------------------- | | **SCIM** | When your IdP supports full user lifecycle management (create, update, deactivate). | | **JIT** | When you only need to create/update users as they log in using SSO. | ## What SCIM Can Do SCIM provisioning in Descope allows your IdP to: * Create and update user profiles * Deactivate users and remove their access * Create, update, and delete groups * Assign users to groups These are implemented in accordance with the SCIM 2.0 protocol and validated during IdP setup (e.g., via Okta's or Azure's provisioning tests). SCIM groups are automatically mapped to Descope Roles. Read more about [Group and Attribute Mapping](#group-and-attribute-mapping) below. ## Group and Attribute Mapping When groups are pushed to Descope via SCIM, they are interpreted as **Roles**. These roles are: * Included in the user's JWT (`roles` claim, under the associated tenant) * Are resolved using the same group mapping rules that apply to SSO logins, ensuring consistency between SCIM and SSO Similarly, SCIM-pushed **user attributes** (e.g., name, email, phone number, department) are stored in the Descope user profile and available in flows and session data. Group mapping is configured once, in the Roles & Groups tab of the tenant's authentication settings in the Descope Console, and applies to both SSO logins and SCIM provisioning. That tab holds group-to-role mapping, default roles, and FGA group mapping. Attribute mapping stays in the SSO tab and remains per SSO method, so precedence still matters for it: within the bound SSO configuration, the attribute mapping of an enabled SSO method (SAML or OIDC) takes precedence. When neither method is enabled, the configured attribute mapping still applies, which is what lets a SCIM-only tenant rely on it. Group mapping does not depend on which SSO method the tenant uses. It is kept in step across the tenant's SAML and OIDC settings, so switching the tenant between SAML, OIDC, and no SSO at all does not lose or change it, and SCIM-only tenants can rely on it as well. The mapping belongs to the SSO configuration the SCIM access key is bound to (keys created without an `sso_id` use the tenant's default SSO configuration; see [Multi-Tenant and Multi-SSO Architecture](#multi-tenant-and-multi-sso-architecture)). Tenant admins can also configure the mappings themselves through the SCIM setup guides in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), which include the mapping steps. ### Group-to-Role Mapping Group mappings are configured in the **Roles & Groups** tab and apply to both SCIM and JIT flows. For example: * Group `engineering` → Role `developer` * Group `finance` → Role `auditor` If a SCIM or SSO login provides one of these groups, the mapped role will be assigned. By default, Descope adds mapped roles to whatever roles the user already has on the tenant, so a role assigned manually in the Console stays in place after a SCIM sync. To have SCIM/SSO group mapping replace the user's existing tenant roles on every sync or login instead of adding to them, turn on **Override roles** in [Project-Level SSO Settings](/auth-methods/sso/settings#role-mapping-add-vs-override). ![SCIM Group Mapping and Default Roles Configuration](/assets/scim-sso-group-mapping-default-roles.webp) ### Default Roles If no mapped roles are found from the user's groups, Descope assigns **Default Roles**, as defined in the same **Roles & Groups** tab. * These apply to both SCIM and JIT flows * Useful for assigning fallback access (e.g., `read-only`) when group data is missing ## Creating SCIM Access Keys To authorize SCIM requests from your IdP to Descope, a bearer token is required in the following format: ``` Authorization: Bearer __ProjectID__: ``` The Access Key must: * Be scoped to a specific tenant * Include the `Tenant Admin` role * Be valid (not expired or revoked) ### Option 1: Manual Access Key Creation You can manually create an access key via the [Descope Console](https://app.descope.com/accessKeys), scoped to the relevant tenant. Combine this key with your project ID to form the required bearer token. ### Option 2: Automated SCIM Access Key Creation Descope offers two fully managed ways to create SCIM-compatible access keys for tenant administrators. #### SSO Setup Suite The Descope SSO Setup Suite includes a built-in option for tenants to configure SCIM themselves. This approach requires no manual access key handling and is ideal for enterprise self-service onboarding. ![SCIM Key Creation via SSO Setup Suite](/assets/scim-key-via-sso-suite.webp) #### Create SCIM Access Key Flow Action You can build custom onboarding flows that programmatically generate SCIM access keys using the `Create SCIM Access Key` action. This allows for flexible automation during tenant provisioning. ![SCIM Key Creation in Flow](/assets/scim-key-via-flow-action.webp) ## Managing SCIM Tokens SCIM tokens can be rotated or revoked to maintain security and control over your SCIM integration. You can either **Rotate token**, **Revoke token**, or **Rotate and Revoke token**. You can manage SCIM tokens in the Descope console in the [Tenants page](https://app.descope.com/tenants/) -> **Authentication Methods** -> **SSO** -> **SCIM Provisioning** or in the **SSO Setup Suite**. ![SCIM Token Management](/assets/manage-scim-tokens.webp) When you rotate and revoke or just revoke a SCIM token, all pre-existing SCIM tokens that may have been rotated will suddenly become invalidated. This means any IdP configurations still using old tokens will stop working immediately. ### Rotate This operation creates a new SCIM token while keeping existing tokens active. To start using the new token, update your IdP's SCIM configuration. The old token remains valid until manually revoked. **Use this when:** - You want to test a new token before fully switching - You need zero-downtime token rotation - You're planning a gradual migration to a new token ### Revoke This operation revokes the current SCIM token without creating a new one. This effectively disables SCIM provisioning for the tenant. **Use this when:** - You want to disable SCIM provisioning entirely - You're switching to JIT (Just-In-Time) provisioning instead ### Rotate and Revoke After rotating and revoking, update your IdP's SCIM configuration with the new bearer token. Until then, SCIM provisioning will not work. This operation creates a new SCIM token and immediately revokes the old one. Your IdP will need to be updated with the new token to continue SCIM provisioning. **Use this when:** - You suspect a token may have been compromised - You're conducting regular security maintenance - You need to invalidate the old token immediately ## SCIM Session Behavior SCIM updates do not immediately revoke existing user sessions. Instead: * Changes are applied on the next login or token refresh * Role changes, deactivations, and profile updates take effect without requiring user input To enforce stricter security policies, consider shortening session durations or logging a user out (revoking their session) when group membership or access levels change. ## SCIM API Access Descope provides a [SCIM Management API](/api/management/tenants/scim) for programmatic management of SCIM configurations. This includes endpoints to: * View user and group records * Validate SCIM push activity * Revoke or rotate access keys * Test provisioning status SCIM-related functionality is not available via the Descope SDKs and must be accessed through the HTTP API. ## Multi-Tenant and Multi-SSO Architecture Descope is designed for multi-tenant SaaS environments and supports multiple SSO configurations per tenant. SCIM provisioning is tied to each **SSO configuration**, not just the tenant. This allows: * One tenant to support multiple IdPs (e.g., Okta and Azure) * Each SSO configuration to have its own SCIM integration * Fine-grained, isolated identity management for each IdP under the same tenant A SCIM access key is bound to the SSO configuration (sso_id) it was created for. If that `sso_id` no longer resolves to any SSO configuration in the tenant (for example, because the configuration was deleted), SCIM requests using that key are rejected with a `400` error. To restore provisioning, create a new SCIM access key tied to a valid SSO configuration. ## SCIM Configuration Guides Descope offers detailed setup guides for configuring SCIM provisioning with popular identity providers: | Identity Provider | Guide | | ----------------------------- | -------------------------------------------------------------------------- | | Okta | [SCIM Provisioning with Okta →](/management/tenant-management/scim/okta-scim) | | Azure (Entra ID) | [SCIM Provisioning with Azure →](/management/tenant-management/scim/azure-scim) | | Each guide provides: * Step-by-step setup instructions * Attribute and group mapping guidance * Troubleshooting and validation steps For day-to-day operational guidance — how customers should onboard new users so they are automatically provisioned, why group mapping is not the same as provisioning, and how to debug "user can't log in" reports — see [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices). If a tenant already has SCIM provisioning configured with a different provider, you don't need to have their IT admin reconfigure the IdP. See [SSO & SCIM Migration](/migrate/sso#scim-provisioning-optional) for how to proxy their existing SCIM traffic to Descope seamlessly. # SCIM with Okta (/management/tenant-management/scim/okta-scim) Learn how to configure SCIM provisioning between Okta and Descope to automate user and group lifecycle management. # SCIM Provisioning with Okta This guide explains how to set up SCIM provisioning between **Okta** and **Descope**, enabling Okta to push users, user updates, deactivations, and groups to a Descope tenant. ## Prerequisites * A Descope **tenant** must be configured with an associated **Access Key** that includes the `Tenant Admin` role. * Assigned users and groups must exist in the Okta application. SSO between Okta and Descope does not need to be configured or enabled first: SCIM provisioning works on its own, including for tenants with no SSO configuration at all. Group mapping, defined in the tenant's Roles & Groups tab, and attribute mapping, defined in the tenant's SSO settings, both apply to SCIM even when SSO is disabled. See the [SCIM Management overview](/management/tenant-management/scim) for details. Okta only pushes users to Descope if they are **assigned to the Okta application** (the "Assignments" tab) — either directly or as a member of a group that is assigned. Note that Okta's **Push Groups** feature (which pushes group *objects* to Descope via SCIM) is separate from Assignments — being a member of a pushed group does not mean the user is assigned to the app, and only assigned users are provisioned. Similarly, Okta's **Group Attribute Statements** (SAML) and OIDC group claims, which control what groups are sent in the authentication assertion, are a separate concern again. With JIT disabled, a user who is not assigned cannot log in to your application. For ongoing onboarding, the recommended pattern is to assign a **group** to the Okta application rather than assigning users one by one. New employees added to the group are then provisioned automatically. See [SCIM Best Practices](/management/tenant-management/scim/scim-best-practices) for the full pattern. ## Step 1: Enable SCIM Provisioning in Okta 1. Open your Okta application for Descope. 2. Go to the **General** tab. 3. Scroll down and check **Enable SCIM provisioning**. 4. Click **Save**. This will reveal the **Provisioning** tab. ![Enable SCIM provisioning in Okta](/assets/enable-scim-provisioning-within-idp.webp) ## Step 2: Configure the SCIM Integration 1. Go to the **Provisioning** tab and click **Edit** in the SCIM Connection section. 2. Enter the following values: | Field | Value | | ------------------------------------- | --------------------------------------------------------- | | **SCIM Connector Base URL** | `https://api.descope.com/scim/v2` | | **Unique Identifier Field for Users** | `email` | | **Supported Actions** | Enable: Push New Users, Push Profile Updates, Push Groups | | **Authentication Mode** | HTTP Header | | **Authorization Header** | `Bearer __ProjectID__:` | The SCIM Connector Base URL can be found in the SCIM tab of your tenant's authentication settings in the Descope Console. ![Tenant URL](/assets/scim-tab.webp) 3. Click **Test Connector Configuration**. A successful test confirms support for creating users, updating attributes, and group management. ![SCIM connector configuration in Okta](/assets/scim-connection-configuration.webp) ## Step 3: Enable Provisioning Actions In the **To App** section under Provisioning: * Check the following options: * **Create Users** * **Update User Attributes** * **Deactivate Users** Click **Save**. ![SCIM To App settings in Okta](/assets/scim-application-settings.webp) ## Step 4: Push Groups from Okta to Descope 1. Go to the **Push Groups** tab in your Okta app. 2. Select groups to push to Descope. 3. These groups will be interpreted as Descope Roles and can be used for access control in flows and session-based policies. For additional details on role mapping, see the [SSO Group Mapping Guide](/auth-methods/sso/saml#group-mapping). # SCIM Best Practices (/management/tenant-management/scim/scim-best-practices) Best practices for onboarding users with SCIM — group-based app assignment, provisioning vs. role mapping, and audit-log verification. # SCIM Best Practices This guide explains how SCIM provisioning should be operated day-to-day once it is configured — in particular, how new employees should be onboarded so they can log in to your application, and how to avoid the most common mistake teams hit after they enable SCIM. If you have not configured SCIM yet, start with the [SCIM Management overview](/management/tenant-management/scim) and the IdP-specific guides for [Azure](/management/tenant-management/scim/azure-scim) or [Okta](/management/tenant-management/scim/okta-scim). ## The Core Idea: Provisioning is Driven by the IdP Descope is the **SCIM service provider**. It receives users and groups that the IdP pushes — it does not pull users from the IdP, and it cannot decide on its own that a given user should exist. Whether a user appears in Descope is determined entirely by whether the IdP decides to push them, which in turn depends on whether that user is **assigned to the application on the IdP side** representing Descope. Different IdPs use different names for this — Azure calls it an *Enterprise Application*, Okta calls it an *app integration* — but the concept is identical: the IdP only pushes users who are in scope for that application. This has a few practical consequences: * When a new employee joins your customer's organization, they will not be in Descope until their IT admin assigns them to the application on the IdP side (directly, or via a group that is assigned to the app). * Descope cannot trigger provisioning. There is no "force resync" Descope can run against the IdP — that action lives in the IdP (for example, *Provision on Demand* in Azure). * If JIT is disabled (the recommended setup when using SCIM) and the user was never assigned to the app, the user cannot log in. In most setups the IdP rejects authentication itself before Descope is reached. The exception is when SSO and SCIM use different IdP applications and the user is assigned to the SSO app but not the SCIM app — authentication then succeeds and Descope reports the user as unknown because no SCIM record exists. ## Recommended Onboarding Pattern: Group-Based Assignment The most common pain point we see is customers assigning users to the application on the IdP side one by one. This works initially, but it means every new hire has to be remembered and assigned manually. The next CISO, engineer, or finance hire who is not added to the app cannot log in — and from the customer's perspective, "SCIM is broken". **The recommended pattern is to assign a group — not individual users — to the application on the IdP side.** The group can be anything that reflects how your customer wants to gate access to your product: | Customer's access model | Group to assign to the application on the IdP side | | ---------------------------------------------------- | ------------------------------------------------------- | | Everyone in the company should be able to log in | The IdP's built-in "Everyone" / "All Users" group | | Only certain departments should have access | One or more department groups (e.g. `engineering`, `security`) | | Access is controlled via an existing access group | The existing IT-managed group (e.g. `app-yourproduct-users`) | Once the group is assigned, onboarding a new employee becomes a single step: **add them to the group**. The IdP will provision them to Descope on its next cycle, and any roles configured via [group mapping](/management/tenant-management/scim#group-and-attribute-mapping) will be applied automatically based on the groups the IdP sends along. This approach also keeps SCIM and SSO consistent — you can assign the same group to the application for both SSO and SCIM provisioning, so a user added to the group can both be created in Descope (via SCIM) and authenticate (via SSO) without any extra configuration. ## Group Mapping is Not Provisioning This is the single most frequent point of confusion: customers see they have a group mapped to a role in Descope (for example, `security` → `admin`) and assume that membership in `security` is what causes the user to be provisioned. It is not. * **Provisioning** decides *who exists* in Descope. It is controlled in the IdP, by who is assigned to the application on the IdP side. * **Group → Role mapping** decides *what an already-provisioned user can do*. It is configured in the tenant's Roles & Groups tab in Descope and runs after the user has been pushed. A user can be in a group that is mapped to a role in Descope and still never appear in Descope, if that user is not in scope for the IdP's provisioning of the app. The IdP must push the user first; only then does Descope apply role mappings to the groups it received. Practically, this means a customer should ensure two things: 1. The user is in a group (or directly assigned) that the IdP will provision to the app. 2. The groups the IdP sends along with that user are mapped to the right Descope roles in the tenant's Roles & Groups tab. ## Provisioning Timing SCIM provisioning is not instantaneous. Each IdP runs SCIM on its own schedule: * **Azure (Microsoft Entra ID)** runs provisioning cycles approximately every **40 minutes** by default. That interval is not configurable, so Entra SCIM does not behave like a near-real-time sync. * New assignments and profile updates usually wait for the next cycle unless you use **Provision on Demand**. * **Disabling a user in Entra does not automatically update Descope right away.** Entra often will not push the disable until a provisioning job that includes that user runs. Trigger **Provision on Demand** for the user, or start/restart provisioning on the Enterprise Application, if you need the disable reflected in Descope immediately. * Details: [Microsoft's documentation](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/application-provisioning-when-will-provisioning-finish-specific-user) and [SCIM with Azure](/management/tenant-management/scim/azure-scim). * **Okta** typically provisions on assignment and on group changes, but the exact timing depends on the org's configuration. When a customer reports that a newly assigned user "still can't log in", or that a disabled user "still works in our app", first check whether Entra has actually run a cycle (or an on-demand job) for that user — Descope only changes state when the IdP pushes SCIM. ## Verifying Provisioning via Audit Logs When a customer says a user cannot log in, the first question to answer is: **did the IdP ever provision this user to Descope?** The fastest way to answer that is the audit log. 1. Open the [Audit and Troubleshoot page](https://app.descope.com/audits) in the Descope Console. 2. Filter for `SCIMEvent` events, scoped to the relevant tenant. 3. Look for an event corresponding to the user in question (search by email or login ID). If you see no `SCIMEvent` for that user, the IdP has not pushed them — which means they are not assigned to the application on the IdP side (directly or via group), or the IdP's last provisioning cycle has not run yet. The fix is on the IdP side. For the full list of SCIM-related audit events, see the [SCIM Audit Events reference](/audit-trails-and-integrations/audit-events/scim-audit-events). ## Common Pitfalls * **Assigning users one by one.** Works for the initial rollout, fails silently every time a new employee joins. Always prefer group-based assignment. * **Assuming Descope-side group mapping causes provisioning.** It does not. The IdP must assign and push the user first. * **Disabling JIT without group-based assignment in place.** Disabling JIT is the right call when SCIM is the source of truth, but only if every user who needs to log in is in scope for the IdP's provisioning. Otherwise users will fail authentication with no automatic recovery path. * **Forgetting that the same separation applies for deprovisioning.** Removing a user from the assigned group in the IdP will cause the IdP to deactivate them in Descope — but only after the IdP actually pushes that change. Removing them from a role-mapped group only changes their roles. * **Expecting Entra disables to show up in Descope immediately.** Entra's ~40 minute cycle (and incomplete automatic push of disables) means a disabled account can remain active in Descope until someone runs **Provision on Demand** or another provisioning job that includes that user. ## Related Reading * [SCIM Management](/management/tenant-management/scim) — overview, access keys, token management, API access * [SCIM with Azure (Microsoft Entra ID)](/management/tenant-management/scim/azure-scim) * [SCIM with Okta](/management/tenant-management/scim/okta-scim) * [SSO & SCIM Migration](/migrate/sso#scim-provisioning-optional): moving an existing SCIM setup to Descope without the tenant reconfiguring their IdP * [JIT Provisioning](/sso/jit-provisioning) — when to use JIT instead of, or alongside, SCIM * [SSO Troubleshooting — SCIM Configuration Issues](/other-troubleshooting/sso-troubleshooting#scim-configuration-issues) * [SCIM Audit Events](/audit-trails-and-integrations/audit-events/scim-audit-events) # SAML Certificate and Metadata Rotation (/management/tenant-management/sso/cert-and-metadata-rotation) Keep SAML SSO working when the IdP rotates signing certificates or when you rotate Descope SP signing and encryption keys. # SAML Certificate and Metadata Rotation IdPs rotate signing certificates on a schedule (or when someone clicks "generate new cert"). If Descope still has the old cert, SAML assertions fail signature checks and login breaks — often with an assertion-handling error in [Audits](https://app.descope.com/audits). This page covers how to stay ahead of that (including expiry alerts), and how to rotate Descope's own SP keys when you need to. Prefer configuring each tenant through the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). The suite (and Console metadata URL field) is the least painful way to keep IdP details current. ## IdP Signing Certificate Descope verifies every SAML assertion with the IdP's public certificate from the tenant's SSO config. ### Metadata URL vs Static Certs | Approach | Best for | Tradeoff | | -------- | -------- | -------- | | **Metadata URL** | Hands-off auto-rotation when the IdP updates its metadata | No Descope cert-expiry audit events for that connection | | **Static upload** (primary + additional certs) | Expiry alerts and controlled dual-cert cutovers | You (or the customer) must update Descope when the IdP rotates | Upload certs **statically** (primary + additional) if you want expiry alerts. Use a **metadata URL** if you'd rather have hands-off auto-rotation — but then you won't get these expiry events. When the IdP rotates its signing cert **and updates its metadata**, Descope picks up the new cert on the next fetch. Manual paste of Login URL / Entity ID / Certificate does **not** auto-update. ### Certificate Expiry Alerts For statically uploaded IdP certificates, Descope can emit a `SAMLCertificateExpiry` warn audit event as expiry approaches. Contact [Descope Customer Success](/support) to enable the `SAMLCertificateExpiry` audit event for your project — it is behind a feature flag and is not on by default. To alert customers (or your ops team): 1. Create a [Management Flow](/flows/management-flows) whose **Start** action uses an event trigger on `SAMLCertificateExpiry`, and compose the notification from the event payload (`triggeringEvent` — see [event triggers](/flows/dynamic-keys#event-triggers)). 2. And/or route the warn audit event to a [notification](/connectors/connector-configuration-guides/messaging) or [audit streaming](/audit-trails-and-integrations/audit-trail-streaming) connector. ### How to Rotate (Static Certs) When you manage the IdP signing cert manually: 1. Add the **new** certificate to the **additional certificates** list (keep the current primary so login still works). 2. Switch the IdP to the new cert (or wait until the IdP starts signing with it). 3. Promote / set the new cert as primary in Descope if needed, then **drop the old** certificate from the list once the IdP no longer uses it. 4. Run a test login (Setup Suite connection test or a real SP-initiated login). If the customer uses the Setup Suite, send them a fresh link and ask them to re-test after they finish the IdP-side rotation. ### When Login Breaks After an IdP Change 1. Confirm the failure in [Audits](https://app.descope.com/audits) (`LoginFailure` / SAML assertion errors, often `E062606`). 2. On the IdP, check whether a new signing certificate is active and whether metadata was published with it. 3. In Descope (or the Setup Suite), refresh the connection: - **Metadata URL:** re-save or re-fetch so Descope reloads metadata. - **Manual:** follow [How to Rotate (Static Certs)](#how-to-rotate-static-certs) (or paste the new primary cert if you aren't using additional certs yet). 4. Run a test login. ## Descope SP Signing and Encryption Keys By default Descope generates SP keys for the tenant. The IdP uses Descope's **public** certificate (from SP metadata or download) to verify AuthnRequests and, when configured, to encrypt assertions. Rotate or replace those keys when your security policy requires it, or when you've uploaded custom keys and need to renew them. Steps and PEM format: [SAML Signing and Encryption Keys](/management/tenant-management/sso/saml-signing). After you change SP keys: 1. Give the IdP the new public certificate (or point it at Descope's updated SP metadata URL). 2. Test SP-initiated login before rolling out to all users. Leaving the IdP on the old SP cert is as broken as leaving Descope on an old IdP cert. ## Checklist - [ ] Choose **metadata URL** (auto-rotation, no expiry events) or **static certs** (expiry alerts + dual-cert cutover). - [ ] If you need `SAMLCertificateExpiry`, ask Descope CS to enable the feature flag. - [ ] Wire alerts: Management Flow on `SAMLCertificateExpiry` and/or an audit / messaging connector. - [ ] For static rotation: additional certs → switch IdP → drop the old cert → test login. - [ ] After SP key rotation: update the IdP with the new public cert / metadata, then test login. - [ ] Keep [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting) and Audits handy for `E062604` / `E062606`. # Cross-App Access (/management/tenant-management/sso/cross-app-access) Configure Cross-App Access on a tenant in the Descope Console # Cross-App Access The **Cross-App Access** tab under a tenant's SSO settings is where you accept [ID-JAG](/agentic-identity-hub/enterprise-managed-authorization/how-xaa-works) assertions from that tenant's workforce IdP. Use it when you host an MCP server and want enterprise customers' users and agents to reach it through their own IdP. It sits under SSO because the assertions come from the same IdP the tenant signs in with. For the product story (customer-managed agents, policies, MCP Resource), see [Let customers manage their agents](/agentic-identity-hub/enterprise-managed-authorization/validate-id-jags). Customers can enter the same settings themselves in the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration). | Path | Who does it | Where | | --- | --- | --- | | **[Console](https://app.descope.com)** | You, on the customer's behalf | Tenant → **Authentication Methods → SSO → Cross-App Access** | | **[SSO Setup Suite](/auth-methods/sso/sso-setup-suite#cross-app-access-xaa-configuration)** | The customer's IT admin | **Cross App Access** section, alongside SSO and SCIM | Whether the Suite section appears is controlled by [SSO Suite Features](/auth-methods/sso/settings#sso-suite-features). ## Turning it on In the Console, open the tenant, go to **Authentication Methods → SSO**, and select the **Cross-App Access** tab. Enable **Allow Cross-App Access (ID-JAG)**, which is off by default. Turning it on reveals the sections below. ![allow cross-app access (xaa) toggle off](/assets/tenant-xaa-enable.webp) - **Resource Server Details** — values to hand to the customer. - **Trusted Issuer** — where you accept their IdP. - **JIT Provisioning** — how their users become users in your project. ## Resource Server Details This section is read-only. The customer's IdP must send the tenant ID in the **`aud_tenant`** claim. Descope uses it to work out which tenant's Cross-App Access configuration an incoming assertion should be evaluated against. If `aud_tenant` is missing, or carries a value different from the one shown here, token validation fails. This is the most common reason a correctly signed ID-JAG is still rejected. It shows the two values that identify your resource server to the customer's IdP. The customer copies both into the cross-app access configuration on their side. | Value | Example | Where it lands | | --- | --- | --- | | **Audience** | `__BaseURL__/v1/apps/__ProjectID__` | The `aud` claim of the ID-JAGs their IdP mints | | **Tenant ID** | `T2fmCOB4Ps5bBPnN4EVAb4cNdjPX` | The `aud_tenant` claim of those same ID-JAGs | Together the two values keep customers separated. `aud` says the assertion is for your project, and `aud_tenant` says which customer inside it. An ID-JAG minted for one tenant cannot be redeemed against another. ![Resource Server Details section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-resource-server-details.webp) ## Trusted Issuer Which IdP Descope accepts assertions from, and how it verifies their signatures. The customer gives you their IdP's issuer URL. Descope uses it two ways: every incoming assertion's `iss` claim must match it, and Descope fetches the public keys that verify the assertion's signature from there (or from the JWKs URL you set). On each incoming assertion Descope checks that the signature verifies, that `iss` is the issuer registered on this tenant, that `aud` matches the [Audience](#resource-server-details) shown above, and that the assertion has not expired. An assertion that fails any of these is rejected and no access token is issued. Registering the issuer on a specific tenant is what keeps customers apart. An assertion is only ever evaluated against the issuer configured for the tenant it targets. | Field | Required | Description | | --- | --- | --- | | **Issuer URL** | Yes | The expected `iss` claim on incoming ID-JAGs. If the issuer is discoverable, saving the configuration fetches the JWKs URL and the rest of the metadata automatically. | | **JWKs URL** | Only if the issuer is not discoverable | The issuer's JSON Web Key Set, used to fetch the public keys that verify the JWT signature. You can set it manually; otherwise Descope attempts to discover it from the Issuer URL. | | **Sign Algorithm** | No | The algorithm used to verify the JWT signature, for example `RS256` or `ES256`. Left blank, the algorithm is read from the token header. | | **User Information Endpoint URL** | No | An endpoint called after the token validates, to fetch additional attributes about the subject user that are not carried in the JWT. | A tenant can trust more than one issuer. **+ Add issuer** adds another set of these fields when a customer runs more than one IdP, or is midway through moving between them. Descope accepts an assertion if it matches any issuer configured on the tenant. ![The Trusted Issuer section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-trusted-issuer.webp) ## JIT Provisioning The assertion names a subject in the customer's directory, not yet a user in yours. Something has to create the Descope user record that the access token is issued for. **Enable JIT Provisioning** creates *and updates* that user from the claims in the ID-JAG every time one validates. The first time a given employee's agent calls your server, the user appears in the tenant; later assertions keep their attributes current. Nothing has to be set up in advance. With the toggle off, provisioning has to happen some other way, in practice through [SCIM](/management/tenant-management/scim). Leaving it off means the exchange only succeeds for users who already exist in the tenant. ![The JIT Provisioning section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-jit.webp) ### Choosing between JIT and SCIM The tradeoff is not about how users get created. It is about what happens when they leave. | | JIT provisioning | SCIM provisioning | | --- | --- | --- | | **User created** | On the first ID-JAG exchange | Pushed by the customer's IdP ahead of time | | **Setup required** | None | The customer configures SCIM on their tenant | | **Offboarding** | Nothing tells Descope the user is gone | The IdP pushes the deactivation to Descope | JIT only ever hears about a user when that user shows up. When an employee leaves and their IdP account is disabled, their agent stops obtaining new ID-JAGs, but the Descope user record from an earlier assertion stays active, and so does any Descope session already issued for it. SCIM is the channel for that event. The customer's IdP pushes the deactivation, Descope marks the user inactive, and the next refresh fails. That is what enterprise customers usually expect for offboarding. A deactivation takes effect on the next login or token refresh, so an `access_token` already in an agent's hands remains valid until it expires. Shorten your [session and refresh token durations](/management/project-settings#session-management) if you need a tighter window, and revoke the session directly for an immediate cutoff. See [SCIM session behavior](/management/tenant-management/scim#scim-session-behavior). Enable JIT when you do not manage that tenant's users with SCIM. When SCIM provisions the tenant's users, leave JIT off so lifecycle events arrive through the same channel that created the user. Entra pushes SCIM changes on a roughly 40 minute cycle, and disables often wait for a provisioning job that includes that user. If a customer needs a deactivation reflected immediately, they can run **Provision on Demand** in Entra. See [SCIM best practices](/management/tenant-management/scim/scim-best-practices#provisioning-timing). ### User Attribute Mapping Same attribute mapping as SSO, except the values come from claims on the **ID-JAG** instead of a SAML assertion or `id_token`. It matters most when the customer's IdP sends custom claims you want on the user profile, since JIT builds the user from these claims. ![The User Attribute Mapping section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-user-attribute-mapping.webp) SCIM provisions users according to the existing SCIM and SSO configuration and mapping. It does not read the ID-JAG claims mapped here, so these mappings only take effect on the JIT path. ### Group Attribute Mapping Enter the **name of the claim** that carries group association, commonly `groups`. Descope reads group names out of that claim on the ID-JAG and maps them onto the tenant's groups, so IdP group membership can drive access without a separate assignment step. ![The Group Attribute Mapping section in the Cross-App Access (XAA) configuration](/assets/tenant-xaa-group-attribute-mapping.webp) Not every IdP sends groups in an ID-JAG. Okta, for example, does support sending group values in the assertion, so check what your customer's IdP emits before relying on this mapping. # Authorization with SSO Providers (/management/tenant-management/sso/how-authorization-works-with-sso-providers) Understand how authorization works with SSO providers in Descope. Learn about access control, roles, and permissions to secure your applications effectively. # Authorization with SSO Providers This guide will walk you through the key differences and options available for managing authorization in your applications, when using SSO with an external OIDC or SAML IdP. All [Tenants](/management/tenant-management) that are created in Descope can have their own Roles and Permissions defined. ## SAML SAML is a widely used standard for SSO, particularly in enterprise environments. It allows users to authenticate through a central identity provider (IdP) and then access various services without needing to log in again. When using SAML with Descope, you can leverage the following features for authorization: 1. **Group-based authorization (RBAC and FGA)**: - SAML lets you define groups in the IdP; membership is sent in the assertion. - Map those groups to Descope **roles** (RBAC) and/or **FGA / ReBAC relations**. The two maps are independent. - On login, Descope applies the mapped roles and writes the mapped FGA tuples for the user. ![group mapping in descope](/assets/group-mapping-saml.webp) 2. **Role mapping**: - Create roles in Descope that match your IdP groups so enterprise access control stays centralized in the IdP. ### Example: Group Mapping Configuration | Azure AD Group | Descope Role | |----------------|-------------------| | Engineering | Developer Team | | HR | HR Team | | Sales | Sales Team | A user in `Engineering` gets the `Developer Team` role after SAML SSO. The same group can also map to FGA relations if you configure them. How-to (RBAC + FGA, API names, defaults): [SSO user and group mapping](/sso/sso-mapping). Console field detail: [SAML → Group mapping](/auth-methods/sso/saml#group-mapping). ## OIDC (OpenID Connect) OIDC builds on OAuth 2.0 and does not standardize a groups claim. Descope still supports **group → role** and **group → FGA** maps for OIDC tenants, configured in the same Roles & Groups tab SAML tenants use — you map whatever claim your IdP puts in the token (often a custom `groups` claim). How to wire that up: [OIDC → Group claims](/auth-methods/sso/oidc#group-claims-oidc). Other OIDC authorization options: 1. **Attribute Mapping**: - OIDC providers can include roles, permissions, or group memberships as claims in the `id_token`. - In Descope, map those claims to user fields or [custom attributes](/management/user-management#custom-user-attributes). - Your app can then enforce access from those attributes. 2. **Custom Claims and JWT Templates**: - If you would like to include any attributes mapped from the IdP, you can include custom attributes as [custom claims in the JWT](/management/token/jwt-templates), alongside any mapped roles or permissions. - This approach is flexible and allows you to tailor the authorization model to fit your application’s needs. ## Summary - **SAML and OIDC**: Map IdP groups to Descope roles (RBAC) and/or FGA relations — see [SSO user and group mapping](/sso/sso-mapping). - **OIDC extras**: Attribute mapping and [JWT templates](/management/token/jwt-templates) when you need custom claims beyond groups. # SSO (/management/tenant-management/sso) Manage tenant SSO configurations in Descope with the Setup Suite, Console, Management SDKs, and APIs. # SSO Management In a B2B setup, each [tenant](/management/tenant-management) can use its own Identity Provider (IdP). Those connections are **tenant SSO configurations**. With Descope you configure SAML or OIDC per tenant, map attributes and groups, and keep authentication consistent across your customers' apps. [Sub-tenants](/management/tenant-management/sub-tenants) can inherit SSO from a parent tenant or use their own config. ## How to Manage Tenant SSO ### SSO Setup Suite (Recommended) For any IdP (Okta, Entra, Google Workspace, or generic SAML/OIDC), generate a [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) link and either send it to the customer's IT admin or open it yourself. Templates, attribute/group mapping, connection test, and SCIM live there — you shouldn't need separate per-IdP written guides. ### Descope Console View and edit a tenant's SSO under [Tenants → Authentication Methods → SSO](https://app.descope.com/tenants). For the Cross-App Access tab (trusted ID-JAG issuers, JIT, resource server details), see [Cross-App Access](/management/tenant-management/sso/cross-app-access). ### Management SDKs and API Automate tenant SSO (including maps) with the [Management SSO SDKs](/management/tenant-management/sso/sdks) or the [Management tenants SSO API](/api/management/tenants/sso). ### Infrastructure as Code [Terraform](/managing-environments/terraform#sso-settings-and-admin-portal) covers project-level SSO auth settings, SSO Setup Suite options, invite emails, and the hosted Admin Portal. Per-tenant SSO connections and SCIM are dynamic — configure them via Setup Suite, Console, or Management SDK/API, not as long-lived Terraform resources. ## Related - [SSO overview](/auth-methods/sso) - [Cross-App Access](/management/tenant-management/sso/cross-app-access) - [Certificate and metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation) - [SAML Signing and Encryption Keys](/management/tenant-management/sso/saml-signing) - [Authorization with SSO providers](/management/tenant-management/sso/how-authorization-works-with-sso-providers) - [Tenants overview](/management/tenant-management) # Configuring a Mock SAML Tenant (/management/tenant-management/sso/mock-saml-testing) This article will show how you can test SSO using a Mock SAML IDP. # How to Setup a Mock SAML Tenant We recommend testing your Descope SSO configurations before deploying, but you may not have an IdP already set up. In this case, you can use [mocksaml.com](https://mocksaml.com) to create a dummy IdP and test your Descope SSO login with just a few simple steps. ## Configure Descope Project After making sure you have SSO enabled as an authentication method in the Descope console, you will need to configure your tenants. After choosing the specific tenant and navigating to the `Authentication Methods` tab on the left, you can enable `SAML` under `SSO`. Add `example.com` to the SSO domains. ![Set Up SSO SAML Domain](/assets/sso-saml-domain.webp) Further down the page, under SSO Configuration, select `Retrieve the connection details dynamically using a metadata URL` and set `metadata URL for IdP` to `https://mocksaml.com/api/saml/metadata`. ![Add the SSO Metadata URL](/assets/sso-metadata-url.webp) ## Testing You can now test your SSO Login with any mock `example.com` email address. ![SSO with any example.com address](/assets/sso-example-login.webp) This will take you to the MockSAML site where you can test your SSO Login. ![SSO on the MockSAML site](/assets/sso-mocksaml-login.webp) # SAML Signing And Encryption Keys (/management/tenant-management/sso/saml-signing) This article will show how you can use your own certificates for SAML request signing and response encryption on your tenant. # SAML Signing And Encryption Keys Signing SAML requests and encrypting responses keep communication between the IdP and Descope (as SP) trustworthy. By default, Descope generates a private key when the tenant is created to sign SAML requests and decrypt encrypted responses. You can also upload your own private keys when your org needs to control key material. For **rotating IdP signing certificates** or staying current via metadata URLs after an IdP change, see [Certificate and metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation). That page is the ops runbook; this one is about configuring Descope's SP keys. For the broader SAML security model (request signing, assertion validation, and single logout), see [SAML Security](/security-best-practices/saml-security). ### Descope Console In tenant settings, under the SAML SSO authentication method, a section called "SSO Keys" allows configuring the signing and encryption keys: ![Descope SSO SAML keys section](/assets/sso-saml-keys.webp)
#### Using Descope's keys Descope as a Service Provider (SP), signs the SAML request using Descope's private key. By using the signing public certificate, the IdP to verify the validity of the request's signature The Identity Provider (IdP) is capable of consuming and utilizing the public certificates provided through the Service Provider's (SP) metadata URL. In scenarios where manual configuration is preferred, these certificates can be downloaded from the configuration page so they can be manually uploaded to the IdP. Example for uploading and configuring the certificates in Okta as the IdP: ![Okta example for upload public cert section](/assets/sso-saml-keys-public.webp) #### Using custom keys When using custom keys, the "Upload Custom Key" expects a PEM certificate file representing a *__private key__*. After uploading the private keys and saving the configuration, the public key pairs are updated in the metadata URL or available for download to be used in the IdP. An example for a PEM file format: ```text -----BEGIN PRIVATE KEY----- MIIB1QIBADANBgkqhkiG9w0BAQEFAASCAb8wggG7AgEAAl0DH3YqFv4mzt67RAAm KqZSY32GtoUqkLXzSJOIew2ofiKx3ojdJvL69pXZLKNoKkKb8RQKyWdhAIkbTEFX 3k8mroXea5NMfB9NAH0AASQ6uoK5XYs7mMubQgu1dhcCAwEAAQJdAjrb+LAUaQe8 +cFTze0UeK48Ow5nxn4wvniriIA9v3vaMGJ0Hl6qkFO1qq76O+uvSehxPHnzBrfs SXkQ8nScyeGpoTpn0DCnMnFRiY1hAMy6SqVdC4t7UP9u6oCBAi8B+POU6nCyUOnL FlPVGFoBxSoxC7q7tJytq+xaPfGBN63AT3sdnXm06YAH1uE/1wIvAZVPf+1sDjIP c4hFNPzIPh/x1M3qDN9eBr6tdPwymuPmpQ1lik/b9ZpMfXGns8ECLwDTVfcci+BF tyP1i06jq4AUKg1u8E+BTxXs37YBOOOxDvpvCYMiln6eP6SITavvAi8A6n71d8rl p6by4+uOjZXZA6hpw7zfN7hx1I4MugEZRjPiWI7f5/ZN8bjBdylcwQIvAQp1f9vQ S+P5ktRlO7vEm10LtKotJ85Rp+le7PX56re+nntKVZFsliKW0yPmWJE= -----END PRIVATE KEY----- ``` # With SDKs (/management/tenant-management/sso/sdks) Learn how to easily implement SSO management and authorization for your app via backend SDKs with Descope. # SSO with Management SDKs You must configure [SSO](/auth-methods/sso) for each tenant independently. These settings can be configured via the [Descope Console](https://app.descope.com/tenants), the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite), the [Management tenants SSO API](/api/management/tenants/sso), or the SDK as shown below. To *start* an SSO login (redirect + code exchange), use the auth [SSO API](/api/sso) or [SSO with Backend SDKs](/auth-methods/sso/with-sdks/backend) — that is separate from configuring a tenant's IdP connection. These are all of the SDK functions you can use in your backend to configure and manage SSO for your tenants. ### Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ### Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope client, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key="xxxx") except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ### Identity Provider (IdP) Details You can either set the identity provider details using a metadata URL from the IdP or enter them in the console. The values for each field can be obtained from the admin console of the identity provider. ```javascript // Configure SSO setting for a tenant manually. Alternatively, `configure_via_metadata` can be used instead. // Args: // tenantId (str): The tenant ID to be configured const tenantId = "xxxxxx" // idpURL (str): The URL for the identity provider. const idpURL = "https://example_idpURL.com" // entityId (str): The entity ID (in the IDP). const entityId = "descope" // idpCert (str): The certificate provided by the IDP. const idpCert = "xxxxxx" // redirectURL (str): An Optional Redirect URL after successful authentication. const redirectURL = "https://example_descope_app.com/saml" // domain (str): An optional domain used to associate users authenticating via SSO with this tenant const domain = "example_descope_app.com" let resp = await descopeClient.management.sso.configureSettings(tenantId, idpURL, idpCert, entityId, redirectURL, domain) if (!resp.ok) { console.log("Unable to configure tenant sso.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully configured sso configuration for tenant manually.") console.log(resp.data) } // Configure SSO setting for am IDP metadata URL. Alternatively, `configure` can be used instead. // Args: // tenantId (str): The tenant ID to be configured const tenantId = "xxxxxx" // idpMetadataURL (str): The URL to fetch SSO settings from. const idpMetadataURL = "https://example_idpURL.com/api/v1/apps/xxxxxxx/sso/saml/metadata?" // redirectURL (str): An Optional Redirect URL after successful authentication. const redirectURL = "https://example_descope_app.com/saml" // domain (str): An optional domain used to associate users authenticating via SSO with this tenant const domain = "example_descope_app.com" let resp = await descopeClient.management.sso.configureMetadata(tenantId, idpMetadataURL, redirectURL, domain) if (!resp.ok) { console.log("Unable to configure tenant sso.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully configured sso configuration for tenant via metadata url.") console.log(resp.data) } ``` ```python # Configure SSO setting for a tenant manually. Alternatively, `configure_via_metadata` can be used instead. # Args: # tenant_id (str): The tenant ID to be configured # idp_url (str): The URL for the identity provider. # entity_id (str): The entity ID (in the IDP). # idp_cert (str): The certificate provided by the IDP. # redirect_url (str): An Optional Redirect URL after successful authentication. # domain (str): An optional domain used to associate users authenticating via SSO with this tenant try: resp = descope_client.mgmt.sso.configure(tenant_id="xxxxxx", idp_url="https://example_idp_url.com", entity_id="descope", idp_cert="xxxxxx", redirect_url="https://example_descope_app.com/saml", domain="example_descope_app.com") print ("Successfully configured sso configuration for tenant manually") except AuthException as error: print ("Unable to configure tenant sso.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) # Configure SSO setting for am IDP metadata URL. Alternatively, `configure` can be used instead. # Args: # tenant_id (str): The tenant ID to be configured # idp_metadata_url (str): The URL to fetch SSO settings from. # redirect_url (str): An Optional Redirect URL after successful authentication. # domain (str): An optional domain used to associate users authenticating via SSO with this tenant try: resp = descope_client.mgmt.sso.configure_via_metadata(tenant_id="xxxxxx", idp_metadata_url="https://example_idp_url.com/api/v1/apps/xxxxxxx/sso/saml/metadata?", redirect_url="https://example_descope_app.com/saml", domain="example_descope_app.com") print ("Successfully configured sso configuration for tenant via metadata url") except AuthException as error: print ("Unable to configure tenant sso.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Configure SSO setting for a tenant manually. Alternatively, `configure_via_metadata` can be used instead. // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): The tenant ID to be configured tenantID := "xxxxxx" // idpURL (str): The URL for the identity provider. idpURL := "https://example_idp_url.com" // idpCert (str): The certificate provided by the IDP. idpCert := "xxxxxx" // entityID (str): The entity ID (in the IDP). entityID := "descope" // redirectURL (str): An Optional Redirect URL after successful authentication. redirectURL := "https://example_descope_app.com/saml" // domain (str): An optional domain used to associate users authenticating via SSO with this tenant domain := "example_descope_app.com" err := descopeClient.Management.SSO().ConfigureSettings(ctx, tenantID, idpURL, idpCert, entityID, redirectURL, domain) if (err != nil){ fmt.Println("Unable to configure tenant sso: ", err) } else { fmt.Println("Successfully configured sso configuration for tenant manually.") } // Configure SSO setting for a tenant manually. Alternatively, `configure_via_metadata` can be used instead. // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): The tenant ID to be configured tenantID := "xxxxxx" // idpMetadataURL (str): The URL to fetch SSO settings from. idpMetadataURL := "https://example_idp_url.com/api/v1/apps/xxxxxxx/sso/saml/metadata?" // redirectURL (str): An Optional Redirect URL after successful authentication. redirectURL := "https://example_descope_app.com/saml" // domain (str): An optional domain used to associate users authenticating via SSO with this tenant domain := "example_descope_app.com" err := descopeClient.Management.SSO().ConfigureMetadata(ctx, tenantID, idpMetadataURL, redirectURL, domain) if (err != nil){ fmt.Println("Unable to configure tenant sso: ", err) } else { fmt.Println("Successfully configured sso configuration for tenant via metadata url.") } ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // You can configure SSO settings manually by setting the required fields directly String tenantId = "tenant-id"; // Which tenant this configuration is for String idpUrl = "https://idp.com"; String entityId = "my-idp-entity-id"; String idpCert = ""; String redirectUrl = "https://my-app.com/handle-saml"; // Global redirect URL for SSO/SAML String domain = "domain.com"; // Users logging in from this domain will be logged in to this tenant try { ss.configureSettings(tenantId, idpUrl, idpCert, entityId, redirectUrl, domain); } catch (DescopeException de) { // Handle the error } // Alternatively, configure using an SSO metadata URL try { ss.configureMetadata(tenantId, "https://idp.com/my-idp-metadata"); } catch (DescopeException de) { // Handle the error } ``` ### Configure SSO Redirect URL You can update only the redirect URLs for a specific SSO configuration without touching the rest of the IdP settings. Leave either URL unset (using whichever "no value" convention your chosen SDK uses, such as `nil`, `None`, or `null`) to leave it unchanged, but make sure to provide a valid URL for at least one of them. ```javascript // Args: // tenantId (str): The tenant ID to be configured. const tenantId = "xxxxxx" // samlRedirectUrl (str): Optional redirect URL for SAML SSO. Leave undefined to leave unchanged. const samlRedirectUrl = "https://example_descope_app.com/saml" // oauthRedirectUrl (str): Optional redirect URL for OAuth SSO. Leave undefined to leave unchanged. const oauthRedirectUrl = "https://example_descope_app.com/oauth" // ssoId (str): Optional SSO configuration ID (only needed if the tenant has multiple SSO configs). const ssoId = "" let resp = await descopeClient.management.sso.configureSSORedirectURL(tenantId, samlRedirectUrl, oauthRedirectUrl, ssoId) if (!resp.ok) { console.log("Unable to configure SSO redirect URL.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully configured SSO redirect URL.") } ``` ```python # Args: # tenant_id (str): The tenant ID to be configured. # saml_redirect_url (str): Optional redirect URL for SAML SSO. Pass None to leave unchanged. # oauth_redirect_url (str): Optional redirect URL for OAuth SSO. Pass None to leave unchanged. # sso_id (str): Optional SSO configuration ID (only needed if the tenant has multiple SSO configs). try: descope_client.mgmt.sso.configure_sso_redirect_url(tenant_id="xxxxxx", saml_redirect_url="https://example_descope_app.com/saml", oauth_redirect_url="https://example_descope_app.com/oauth", sso_id=None) print ("Successfully configured SSO redirect URL.") except AuthException as error: print ("Unable to configure SSO redirect URL.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context. ctx := context.Background() // tenantID (string): The tenant ID to be configured. tenantID := "xxxxxx" // samlRedirectURL (*string): Optional redirect URL for SAML SSO. Pass nil to leave unchanged. samlRedirectURL := "https://example_descope_app.com/saml" // oauthRedirectURL (*string): Optional redirect URL for OAuth SSO. Pass nil to leave unchanged. oauthRedirectURL := "https://example_descope_app.com/oauth" // ssoID (string): The SSO configuration ID (use "" if the tenant has only one SSO config). ssoID := "" err := descopeClient.Management.SSO().ConfigureSSORedirectURL(ctx, tenantID, &samlRedirectURL, &oauthRedirectURL, ssoID) if err != nil { fmt.Println("Unable to configure SSO redirect URL: ", err) } else { fmt.Println("Successfully configured SSO redirect URL.") } ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // Args: String tenantId = "tenant-id"; // The tenant ID to be configured String samlRedirectUrl = "https://my-app.com/handle-saml"; // Optional redirect URL for SAML SSO. Pass null to leave unchanged. String oauthRedirectUrl = "https://my-app.com/handle-oauth"; // Optional redirect URL for OAuth SSO. Pass null to leave unchanged. String ssoId = null; // Optional SSO configuration ID (only needed if the tenant has multiple SSO configs) try { ss.configureSSORedirectURL(tenantId, samlRedirectUrl, oauthRedirectUrl, ssoId); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // TenantId (string): The tenant ID to be configured. // SamlRedirectUrl (string?): Optional redirect URL for SAML SSO. Leave null to leave unchanged. // OauthRedirectUrl (string?): Optional redirect URL for OAuth SSO. Leave null to leave unchanged. // SsoId (string?): Optional SSO configuration ID (only needed if the tenant has multiple SSO configs). var request = new ConfigureSSORedirectURLRequest { TenantId = "tenant-id", SamlRedirectUrl = "https://example_descope_app.com/saml", OauthRedirectUrl = "https://example_descope_app.com/oauth", }; await client.Mgmt.V1.Sso.Redirect.PostAsync(request); ``` ```php // Args: // tenantId (string): The tenant ID to be configured. $tenantId = "xxxxxx"; // samlRedirectUrl (?string): Optional redirect URL for SAML SSO. Pass null to leave unchanged. $samlRedirectUrl = "https://example_descope_app.com/saml"; // oauthRedirectUrl (?string): Optional redirect URL for OAuth SSO. Pass null to leave unchanged. $oauthRedirectUrl = "https://example_descope_app.com/oauth"; // ssoId (string): Optional SSO configuration ID (only needed if the tenant has multiple SSO configs). $ssoId = ""; try { $descopeSDK->management->sso->configureSSORedirectURL($tenantId, $samlRedirectUrl, $oauthRedirectUrl, $ssoId); echo "Successfully configured SSO redirect URL.\n"; } catch (\Descope\SDK\Exception\AuthException $e) { echo "Unable to configure SSO redirect URL: " . $e->getMessage() . "\n"; } ``` ### Load SSO Configuration Descope allows you to load the SSO config for a specified tenant. ```javascript // Args: // tenantId (str): The tenant ID to get the SSO configuration from. const tenantId = "xxxxxx" // ssoId (str): The SSO ID to get the SSO configuration for if the tenant has multiple SSO configs const ssoId = "xxxxxx" // You can pass ssoId in case you want to load a specific SSO configuration const ssoSettings = await descopeClient.management.sso.loadSettings('tenantId', 'ssoId'); // You can get all configured SSO settings for a specific tenant if the tenant has multiple SSO configs const allSSOSettings = await descopeClient.management.sso.loadAllSettings('tenantId'); ``` ```python # Args: # tenant_id (str): The tenant ID to get the SSO configuration from. tenant_id = "xxxxxxx" sso_settings_res = descope_client.mgmt.sso.load_settings("tenant-id") ``` ```go // Args: // tenantId (str): The tenant ID to get the SSO configuration from. const tenantId = "xxxxxx" // ssoId (str): The SSO ID to get the SSO configuration for if the tenant has multiple SSO configs const ssoId = "xxxxxx" // You can pass ssoId in case you want to load a specific SSO configuration ssoSettings, err := descopeClient.Management.SSO().LoadSettings(context.Background(), "tenantId", "ssoId") // You can get all configured SSO settings for a specific tenant if the tenant has multiple SSO configs allSSOSettings, err := descopeClient.Management.SSO().LoadAllSettings(context.Background(), "tenantId"); ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // You can get SSO settings for a specific tenant ID SSOSettingsResponse resp = ss.loadSettings("tenant-id"); ``` ### Delete SSO Configuration Descope allows you to delete the SSO config for a specified tenant. Use caution with this SDK call as it will remove the configuration and is irreversible. ```javascript // Args: // tenantId (str): The tenant ID to delete the SSO configuration from. const tenantId = "xxxxxx" // You can optionally pass in the ssoId if the tenant has multiple SSO configs let resp = await descopeClient.management.sso.deleteSettings(tenantId, ssoId) if (!resp.ok) { console.log("Unable to delete the sso configuration from tenant.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully deleted sso configuration from tenant.") } ``` ```python # Args: # tenant_id (str): The tenant ID to delete the SSO configuration from. try: descope_client.mgmt.sso.delete_settings(tenant_id="xxxxxxx") print ("Successfully deleted sso configuration from tenant.") except AuthException as error: print ("Unable to delete the sso configuration from tenant.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): The tenant ID to delete the SSO configuration from. tenantID := "xxxxxxx" // You can optionally pass in the ssoId if the tenant has multiple SSO configs err := descopeClient.Management.SSO().DeleteSettings(ctx, tenantID, ssoId) if (err != nil){ fmt.Println("Unable to delete the sso configuration from tenant: ", err) } else { fmt.Println("Successfully deleted sso configuration from tenant.") } ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // You can get SSO settings for a specific tenant ID try { SSOSettingsResponse resp = ss.deleteSettings("tenant-id"); } catch (DescopeException de) { // Handle the error } ``` ### SSO Mapping #### SSO User Attribute Mapping In this section of the console, you can setup mapping for user attributes. After you set up the mapping, each user that signs into your application will get these attributes assigned from the IdP. Descope also allows you to map attributes from your IdP to [custom user attributes](/management/user-management#custom-user-attributes) when configuring your attribute mapping. #### Groups Mapping In this part of SSO configuration, you can map SSO groups from your IdP to roles defined in Descope service. The group-to-role mapping will automatically populate the user's roles at the time of sign-in. The roles are included in the session token after successful authentication. It is important to note, this function overrides any previous mapping (even when empty). ```javascript // Args: // tenantId (str): The tenant ID to be configured const tenantId = "xxxxxx" // roleMappings (RoleMapping): A mapping between IDP groups and Descope roles. const roleMapping = { groups: ['IDP_ADMIN'], roleName: 'Tenant Admin'} // attributeMapping (AttributeMapping): A mapping between IDP user attributes and descope attributes. const attributeMapping = {name: "IDP_NAME", phoneNumber: "IDP_PHONE",} let resp = await descopeClient.management.sso.configureMapping(tenantId, roleMapping, attributeMapping) if (!resp.ok) { console.log("Unable to configured sso role and attribute mapping.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully configured sso role and attribute mapping.") } ``` ```python # Args: # tenant_id (str): The tenant ID to be configured # role_mappings (List[RoleMapping]): A mapping between IDP groups and Descope roles. # attribute_mapping (AttributeMapping): A mapping between IDP user attributes and descope attributes. try: descope_client.mgmt.sso.mapping(tenant_id="xxxxxxx", role_mappings=[RoleMapping(["IDP_ADMIN"], "Tenant Admin")], attribute_mapping=AttributeMapping(name="IDP_NAME", phone_number="IDP_PHONE")) print ("Successfully configured sso role and attribute mapping.") except AuthException as error: print ("Unable to configured sso role and attribute mapping.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): The tenant ID to be configured tenantID := "xxxxxxx" // roleMappings (List[RoleMapping]): A mapping between IDP groups and Descope roles. roleMappings := []*descope.RoleMapping{{Groups: []string{"IDP_ADMIN"}, Role: "Tenant Admin"}} // attributeMapping (AttributeMapping): A mapping between IDP user attributes and descope attributes. attributeMapping := &descope.AttributeMapping {Name: "IDP_NAME", PhoneNumber: "IDP_PHONE",} err := descopeClient.Management.SSO().ConfigureMapping(ctx, tenantID, roleMappings, attributeMapping) if (err != nil){ fmt.Println("Unable to configured sso role and attribute mapping: ", err) } else { fmt.Println("Successfully configured sso role and attribute mapping.") } ``` ```java SsoService ss = descopeClient.getManagementServices().getSsoService(); // Map IDP groups to Descope roles, or map user attributes. // This function overrides any previous mapping (even when empty). Use carefully. List rm = Arrays.asList(new RoleMapping(Arrays.asList("Groups"), "Tenant Role")); AttributeMapping am = new AttributeMapping("Tenant Name", "Tenant Email", "Tenant Phone Num", "Tenant Group"); try { ss.configureMapping(tenantId, rm, am); } catch (DescopeException de) { // Handle the error } ``` ## SSO Group Management The Descope SDKs make the SSO Groups available for being loaded. The below covers the available functions. ### Load All Groups Descopers can load all groups for a given tenant, the below covers examples of this. ```javascript // Args: // tenantId (str): Tenant ID to load groups from. const tenantId = "xxxxx" const resp = await descopeClient.management.group.loadAllGroups(tenantId) if (!resp.ok) { console.log(resp) console.log("Unable to load all groups.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded all groups.") console.log(resp.data) } ``` ```python # Args: # tenant_id (str): Tenant ID to load groups from. try: resp = descope_client.mgmt.group.load_all_groups(tenant_id="xxxxx") print ("Successfully loaded all groups.") print(resp) except AuthException as error: print ("Unable to load all groups.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): Tenant ID to load groups from. tenantID := "xxxx" resp, err := descopeClient.Management.Group().LoadAllGroups(ctx, tenantID) if (err != nil){ fmt.Println("Unable to load all groups: ", err) } else { fmt.Println("Successfully loaded all groups: ", resp) } ``` ```java // Load all groups for a given tenant id GroupService gs = descopeClient.getManagementServices().getGroupService(); try { List groups = gs.loadAllGroups("tenant-id"); for (Group g : groups) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ### Load All Groups for Members Descopers can load all groups for members based on login IDs and user IDs, the below covers examples of this. ```javascript // Args: // tenantId (str): Tenant ID to load groups from. const tenantId = "xxxxx" // userIDs (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT. const userIds = [] // loginIDs (List[str]): Optional List of login IDs, how the users identify when logging in. const loginIds = [] const resp = await descopeClient.management.group.loadAllGroupsForMembers(tenantId, userIds, loginIds) if (!resp.ok) { console.log(resp) console.log("Unable to load all groups for members.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded all groups for members.") console.log(resp.data) } ``` ```python # Args: # tenant_id (str): Tenant ID to load groups from. # login_ids (List[str]): Optional List of login IDs, how the users identify when logging in. # user_ids (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT. try: resp = descope_client.mgmt.group.load_all_groups_for_members(tenant_id="xxxxx", login_ids=["TestUser1","TestUser1"], user_ids=["U2J5ES9S8TkvCgOvcrkpzUgVTEBM","U2J5ES9S8TkvCgOvcrkpzUgVxtz"]) print ("Successfully loaded all groups for members.") print(resp) except AuthException as error: print ("Unable to load all groups for members.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): Tenant ID to load groups from. tenantID := "xxxx" // userIDs (List[str]): Optional List of user IDs, with the format of "U2J5ES9S8TkvCgOvcrkpzUgVTEBM" (example), which can be found on the user's JWT. userIDs := []string{"U2J5ES9S8TkvCgOvcrkpzUgVTEBM","U2J5ES9S8TkvCgOvcrkpzUgVxtz"} // loginIDs (List[str]): Optional List of login IDs, how the users identify when logging in. loginIDs := []string{"TestUser1","TestUser1"} resp, err := descopeClient.Management.Group().LoadAllGroupsForMembers(ctx, tenantID, userIDs, loginIDs) if (err != nil){ fmt.Println("Unable to load all groups for members: ", err) } else { fmt.Println("Successfully loaded all groups for members: ", resp) } ``` ```java // Load all groups for the given user/login IDs (can be found in the user's JWT, used for sign-in) try { List groups = gs.loadAllGroupsForMembers("tenant-id", Arrays.asList("user-id-1", "user-id-2"), Arrays.asList("login-id-1", "login-id-2")); for (Group g : groups) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ### Load All Group Members Descopers can load all groups members based on tenant ID and group ID, the below covers examples of this. ```javascript // Args: // tenantId (str): Tenant ID to load groups from. const tenantId = "xxxxx" // groupId (str): Group ID to load members for. const groupId = "xxxx" const res = await descopeClient.management.group.loadAllGroupMembers(tenantId, groupId) if (!resp.ok) { console.log(resp) console.log("Unable to load all group members.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully loaded all group members.") console.log(resp.data) } ``` ```python # Args: # tenant_id (str): Tenant ID to load groups from. # group_id (str): Group ID to load members for. try: resp = descope_client.mgmt.group.load_all_group_members(tenant_id="xxxxx", group_id="xxxx") print ("Successfully loaded all group members.") print(resp) except AuthException as error: print ("Unable to load all group members.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (str): Tenant ID to load groups from. tenantID := "xxxx" // groupID (str): Group ID to load members for. groupID := "xxxx" resp, err := descopeClient.Management.Group().LoadAllGroupMembers(ctx, tenantID, groupID) if (err != nil){ fmt.Println("Unable to load all group members: ", err) } else { fmt.Println("Successfully loaded all group members: ", resp) } ``` ```java // Load all group's members by the given group id try { List groups = gs.loadAllGroupMembers("tenant-id", "group-id"); for (Group g : groups) { // Do something } } catch (DescopeException de) { // Handle the error } ``` ## Generate SSO Configuration Link Generate an SSO admin link for a tenant that allows a tenant administrator to configure SSO settings. The generated link can be sent to tenant administrators via email or displayed in your application. Once accessed, the link allows the administrator to configure their tenant's SAML or OIDC SSO settings through a guided interface. The `expiration` parameter is a duration in seconds. Therefore, to retrieve a link valid for 6 hours, pass `21600`. ```javascript // Args: // tenantId (string): The ID of the tenant for which to generate the SSO configuration link. const tenantId = "my-tenant-id"; // expireDuration (number): Expiration duration in seconds. For a 6-hour link, use 21600. const expireDuration = 21600; // 6 hours // ssoId (string, optional): SSO identifier for the tenant. const ssoId = "my-sso-id"; // email (string, optional): Email address associated with the admin. const email = "admin@example.com"; // templateId (string, optional): Template ID to use for the configuration page. const templateId = "my-template-id"; // actorId (string, optional): When provided, this id is recorded as the audit actor for actions // performed inside the SSO Setup Suite (instead of the temporary user). Used as-is for // audit attribution and is not validated. const actorId = "my-admin-actor-id"; try { const resp = await descopeClient.management.tenant.generateSSOConfigurationLink( tenantId, expireDuration, ssoId, email, templateId, actorId ); if (resp.ok) { console.log("Successfully generated SSO configuration link:"); console.log(resp.data.adminSSOConfigurationLink); } else { console.log("Failed to generate SSO configuration link"); console.log("Status Code: " + resp.code); console.log("Error: " + resp.error.errorMessage); } } catch (error) { console.log("Failed to generate SSO configuration link: " + error); } ``` ```python # Args: # tenant_id (str): The ID of the tenant for which to generate the SSO configuration link. # expire_time (int, optional): Expiration duration in seconds. For a 6-hour link, use 21600. # email (str, optional): Email address associated with the admin. # sso_id (str, optional): SSO identifier for the tenant. # actor_id (str, optional): When provided, this is recorded as the audit actor for actions # performed inside the SSO Setup Suite (instead of the temporary user). It is used as-is for # audit attribution and is not validated. try: link = descope_client.mgmt.tenant.generate_sso_configuration_link( tenant_id="my-tenant-id", expire_time=21600, # 6 hours email="admin@example.com", sso_id="my-sso-id", actor_id="my-admin-actor-id" ) print("Successfully generated SSO configuration link:") print(link) except AuthException as error: print("Failed to generate SSO configuration link") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenantID (string): The ID of the tenant for which to generate the SSO configuration link. tenantID := "my-tenant-id" // expireDuration (int64): Expiration duration in seconds. For a 6-hour link, use 21600. expireDuration := int64(21600) // 6 hours // ssoID (string): SSO identifier for the tenant (optional, can be empty string). ssoID := "my-sso-id" // email (string): Email address associated with the admin (optional, can be empty string). email := "admin@example.com" // templateID (string): Template ID to use for the configuration page (optional, can be empty string). templateID := "" // actorID (string): Optional (pass "" for none). When provided, this id is recorded as the audit // actor for actions performed inside the SSO Setup Suite (instead of the temporary user). It is // used as-is for audit attribution and is not validated. actorID := "my-admin-actor-id" link, err := descopeClient.Management.Tenant().GenerateSSOConfigurationLink( ctx, tenantID, expireDuration, ssoID, email, templateID, actorID, ) if err != nil { fmt.Println("Failed to generate SSO configuration link: ", err) } else { fmt.Println("Successfully generated SSO configuration link: ", link) } ``` ```java // Import required classes import com.descope.model.tenant.request.GenerateTenantLinkRequest; TenantService ts = descopeClient.getManagementServices().getTenantService(); // Args: // tenantId (String): The ID of the tenant for which to generate the SSO configuration link. String tenantId = "my-tenant-id"; // expireDuration (long): Expiration duration in seconds. For a 6-hour link, use 21600. long expireDuration = 21600; // 6 hours // ssoId (String): SSO identifier for the tenant (optional). String ssoId = "my-sso-id"; // email (String): Email address associated with the admin (optional). String email = "admin@example.com"; // templateId (String): Template ID to use for the configuration page (optional). String templateId = ""; try { GenerateTenantLinkRequest request = GenerateTenantLinkRequest.builder() .tenantId(tenantId) .expireDuration(expireDuration) .ssoId(ssoId) .email(email) .templateId(templateId) .build(); String link = ts.generateSSOConfigurationLink(request); System.out.println("Successfully generated SSO configuration link: " + link); } catch (DescopeException de) { System.out.println("Failed to generate SSO configuration link: " + de.getMessage()); } ``` ```csharp // Args: // tenantID (string): The ID of the tenant for which to generate the SSO configuration link. var tenantID = "my-tenant-id"; // expireTime (string): Expiration duration in seconds. For a 6-hour link, use "21600". var expireTime = "21600"; // 6 hours // ssoId (string, optional): SSO identifier for the tenant. var ssoId = "my-sso-id"; // email (string, optional): Email address associated with the admin. var email = "admin@example.com"; // templateId (string, optional): Template ID to use for the configuration page. var templateId = ""; try { var resp = await descopeClient.Mgmt.V1.Tenant.Adminlinks.Sso.Generate.PostAsync(new GenerateTenantAdminLinkRequest { TenantId = tenantID, ExpireTime = expireTime, SsoId = ssoId, Email = email, TemplateId = templateId, }); var link = resp!.AdminSSOConfigurationLink; } catch (DescopeException ex) { // Handle the error } ``` # OIDC (/identity-federation/applications/oidc-apps) Configure Descope as an OpenID Connect Identity Provider for your applications. # OIDC Federated Applications Configuring additional federated applications (beyond the default) is a Pro+ feature. Configure a Federated Application to use Descope as an OpenID Connect (OIDC) Identity Provider. Your application redirects users to Descope for authentication, Descope runs your configured flow, and returns a validated identity token. Standard OIDC protocol throughout — no custom auth logic required. For a full list of Descope's OIDC endpoints and supported grant types, see the [OIDC Endpoints Guide](/identity-federation/applications/oidc-apps/oidc-endpoints). ## Creating an OIDC Application Navigate to [Applications](https://app.descope.com/applications) and click **+ Application**. Choose a template from the Application Library or create a Generic OIDC Application. Provide an **Application Name** and optionally an **Application ID** and **Description**. ![Create an OIDC Application within Descope](/assets/oidc-create.webp) ## Configuring the Application Once created, configure your application in the Descope Console: ![Configuring an OIDC Application within Descope](/assets/oidc-configure.webp) ### Application details | Setting | Details | | ------- | ------- | | **Application Name** | The display name for the application (can be updated). | | **Application ID** | Unique identifier (cannot be changed). Available in flows as the `ssoAppID` variable for rendering app-specific logic. | | **Application Description** | Optional description of the application's purpose. | ### Identity provider settings | Setting | Details | | ------- | ------- | | **Flow Hosting URL** | Where users are redirected for authentication. Defaults to `https://auth.descope.io/?flow=sign-up-or-in`. Configure using the gear icon — see [Auth Hosting](/identity-federation/auth-hosting). | | **Issuer URL** | Descope's identifier as the IdP: `https://api.descope.com/`. Use this for most OIDC configurations. | | **Discovery URL** | Returns the full OIDC configuration as JSON: `https://api.descope.com//.well-known/openid-configuration`. | | **Supported Claims** | Additional claims to include in the Well-Known Configuration. Default claims include `sub`, `name`, `email`, and others. | | **Force Authentication** | Forces flow execution even if the user is already signed in, equivalent to `prompt=login`. | If you're using a [custom domain](/how-to-deploy-to-production/custom-domain), replace `api.descope.com` with your custom domain in the Issuer and Discovery URLs. ## Configuring your OIDC client Configure your OIDC client (Service Provider) with the following: ### Required parameters - **Client ID**: Your Descope Project ID ([find it here](https://app.descope.com/settings/project)) - **Client Secret**: A Descope Access Key ([create one here](https://app.descope.com/m2m/accessKeys)) - **Issuer or Discovery URL**: Use one of these from your application configuration: - **Issuer URL** (recommended): `https://api.descope.com/` - **Discovery URL**: `https://api.descope.com//.well-known/openid-configuration` You can also configure endpoints individually: | Endpoint | URL | |----------|-----| | Authorization | `__BaseURL__/oauth2/v1/authorize` | | Token | `__BaseURL__/oauth2/v1/token` | | Logout | `__BaseURL__/oauth2/v1/logout` | ### Scopes | Scope | What it returns | |-------|-----------------| | `openid` | Required for OIDC authentication. | | `profile` | User's profile data (name, picture, etc.). | | `email` | User's email address. | | `phone` | User's phone number. | | `descope.custom_claims` | [Custom claims](#custom-claims) you've configured. | | `descope.claims` | User's [tenants, roles, and permissions](#tenant-roles-and-permissions). | ### Force Authentication By default, users already signed in to Descope are redirected back without re-authenticating (standard SSO behavior). To force re-authentication, either include `prompt=login` in your `/authorize` request or enable **Force Authentication** in the application settings. ![Force Authentication checkbox](/assets/oidc-force-authentication.webp) When a user already has a valid session and Force Authentication is disabled, Descope redirects the user back to the application without running the flow. In this case, [`lastAuth`](/flows/dynamic-keys#lastauth) context keys are not populated for that request, since the flow itself was skipped. ## Well-Known OIDC configuration The Discovery URL returns a JSON document with all OIDC configuration details, allowing clients to configure themselves automatically: ```json { "issuer": "__BaseURL__/__ProjectID__", "jwks_uri": "__BaseURL__/__ProjectID__/.well-known/jwks.json", "authorization_endpoint": "__BaseURL__/oauth2/v1/authorize", "token_endpoint": "__BaseURL__/oauth2/v1/token", "userinfo_endpoint": "__BaseURL__/oauth2/v1/userinfo", "end_session_endpoint": "__BaseURL__/oauth2/v1/logout", "revocation_endpoint": "__BaseURL__/oauth2/v1/revoke", "response_types_supported": ["code"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "profile", "email", "phone"], "claims_supported": [ "iss", "aud", "iat", "exp", "sub", "name", "email", "email_verified", "phone_number", "phone_number_verified", "picture" ], "token_endpoint_auth_methods_supported": [] } ``` ## Advanced configuration ### Custom claims Custom claims let you include application-specific user information in the ID token. 1. Add the `descope.custom_claims` scope to your OIDC client configuration. 2. Define the claims using one of these methods: - A [Custom Claims action](/flows/actions/custom-claims) in your flow - [JWT Templates](/management/token/jwt-templates) - The [Management SDK](/security-best-practices/custom-claims) or [API](/api/management/users/update-jwt) ![Custom claims configuration in OIDC](/assets/oidc-custom-claims.webp) Custom claim names in your JWT must match exactly how they're referenced in your application. ### Tenant, roles, and permissions Add the `descope.claims` scope to include authorization information in the ID token. This returns the user's tenants, roles, and permissions in a `tenants` claim: ```json { "sub": "U2XDs389PTVB8xtfRlixPb4luUD7", "email": "user@example.com", "tenants": { "": { "permissions": ["SSO Admin", "User Admin"], "roles": ["Tenant Admin"] } } } ``` See [Role-based access control](/authorization/role-based-access-control) for configuring roles and permissions. ### Adding Claims to the Well-Known Configuration Some OIDC clients (like Docebo) require specific claims to be listed in `claims_supported`. Add them in the **Supported Claims** setting of your application. The defaults are: ```json ["iss", "aud", "iat", "exp", "sub", "name", "email", "email_verified", "phone_number", "phone_number_verified", "picture", "family_name", "given_name"] ``` ![Supported claims configuration](/assets/oidc-claims.webp) ## Flow Hosting Configure the authentication page users see during the OIDC redirect using the gear icon next to **Flow Hosting URL** in your application settings. See [Auth Hosting](/identity-federation/auth-hosting) for all available options — custom domains, styling, background images, and self-hosted flows. # Using OIDC Endpoints (/identity-federation/applications/oidc-apps/oidc-endpoints) Get started with Descope's OIDC endpoints using Descope as an OIDC provider. # Descope OIDC Endpoints Quickstart Using Descope as an OpenID Connect (OIDC) provider is a common integration path—especially when your framework does not have a native Descope SDK (for example Expo, .NET Blazor, or custom OAuth clients). This guide explains how to use Descope's OIDC endpoints to authenticate users, obtain tokens, manage sessions, and perform secure logout and token revocation. ## Core OIDC Endpoints | Purpose | Endpoint | | --------------------------------------------------------------------- | ------------------------------------------------------------- | | **Authorization** - Start the OIDC login flow | `__BaseURL__/oauth2/v1/authorize` | | **Token** - Exchange authorization codes for tokens | `__BaseURL__/oauth2/v1/token` | | **UserInfo** - Retrieve user claims and profile information | `__BaseURL__/oauth2/v1/userinfo` | | **JWKs URI** - Retrieve public keys for verifying Descope-issued JWTs | `__BaseURL__/__ProjectID__/.well-known/jwks.json` | | **End Session** - Log the user out of their Descope session | `__BaseURL__/oauth2/v1/logout` | | **Revocation** - Revoke a token (access or refresh) | `__BaseURL__/oauth2/v1/revoke` | ## Diagram of OIDC Authorization Code Flow ## Supported Grant Types Descope supports the following OAuth 2.0 grant types: - **Authorization Code Flow with PKCE** - Recommended for public clients (SPAs, native mobile) - **Authorization Code Flow (without PKCE)** - For confidential clients that can safely store a client secret - **Client Credentials Flow** - For machine-to-machine (M2M) integrations - **Device Authorization Flow** - For devices without a browser or keyboard (e.g., smart TVs) The sections below walk through each flow in detail. ## Authorization Code Flow (with PKCE) Use this flow when building native mobile apps, SPAs, or any public client that cannot safely store a client secret. PKCE (Proof Key for Code Exchange) protects against code interception attacks. ### 1. Generate a Code Verifier and Code Challenge ```javascript function generateCodeVerifier() { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; return Array.from(crypto.getRandomValues(new Uint8Array(128))) .map(x => chars[x % chars.length]) .join(''); } async function generateCodeChallenge(verifier) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)); return btoa(String.fromCharCode(...new Uint8Array(digest))) .replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); } ``` ### 2. Redirect the User to the Authorization Endpoint ```javascript const authUrl = `__BaseURL__/oauth2/v1/authorize ?response_type=code &client_id=__ProjectID__ &redirect_uri=YOUR_REDIRECT_URI &scope=openid profile email &code_challenge=${codeChallenge} &code_challenge_method=S256 &state=YOUR_STATE`; window.location.href = authUrl; ``` After successful authentication, Descope redirects to your `redirect_uri` with an authorization code. ### 3. Exchange the Authorization Code for Tokens ```javascript const response = await fetch('__BaseURL__/oauth2/v1/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: 'AUTHORIZATION_CODE', redirect_uri: 'YOUR_REDIRECT_URI', client_id: '__ProjectID__', code_verifier: codeVerifier, }), }); const tokens = await response.json(); ``` ### 4. Retrieve User Information ```javascript const userResponse = await fetch('__BaseURL__/oauth2/v1/userinfo', { headers: { 'Authorization': `Bearer ${tokens.access_token}` }, }); const user = await userResponse.json(); ``` The `/userinfo` response includes user claims (for example `email`, `name`, `sub`) along with any [custom claims](#custom-claims) and role assignments you have configured in Descope. Store the `refresh_token` securely in your application so you can obtain new access tokens without forcing the user to log in again. ## Authorization Code Flow (without PKCE) Confidential clients (such as server-side web apps and trusted backend services) can perform the Authorization Code flow without PKCE by authenticating with their client secret. We still recommend enabling PKCE wherever possible. However, some enterprise OIDC clients expect the classic authorization code flow with a client secret and no PKCE. This section shows how to support that scenario. ### 1. Redirect the User to the Authorization Endpoint ```bash __BaseURL__/oauth2/v1/authorize? response_type=code& client_id=__ProjectID__& redirect_uri=https://your-app.com/callback& scope=openid%20profile%20email& state=YOUR_STATE ``` ### 2. Exchange the Authorization Code for Tokens Send the authorization code to the token endpoint. Include your client secret in either the `Authorization` header or the POST body. ```bash curl -X POST __BaseURL__/oauth2/v1/token \ -H 'Content-Type: application/x-www-form-urlencoded' \ -u '__ProjectID__:YOUR_CLIENT_SECRET' \ -d 'grant_type=authorization_code' \ -d 'code=AUTHORIZATION_CODE' \ -d 'redirect_uri=https://your-app.com/callback' ``` The response includes the `access_token`, `id_token`, and optionally a `refresh_token`. Use the `access_token` to call `/userinfo`, and verify the `id_token` signature before establishing a session. Only use this approach when you can safely store and protect the client secret. For SPAs or mobile apps, always use the PKCE variant. ## Client Credentials Flow (Machine-to-Machine) Use the Client Credentials flow to obtain tokens for backend services without user interaction. 1. Generate an [Access Key](https://app.descope.com/m2m/accessKeys) in the Descope Console. 2. Combine the `` and `` as `:`. 3. Base64-encode the string and send it in the `Authorization` header. ```bash curl -X POST __BaseURL__/oauth2/v1/token \ -H 'Authorization: Basic :)>' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials&scope=openid%20profile%20email' ``` Custom claims for machine-to-machine tokens can be configured using **Access Key JWT Templates** under Project Settings. ## Managing Sessions and Tokens ### Logout vs Token Revocation | Action | Affects | When to Use | | ----------- | ---------------------- | ---------------------------------- | | `/logout` | User session (browser) | Sign the user out from Descope and your app | | `/revoke` | Specific token | Invalidate tokens programmatically | **End Session (`/logout`)** ```javascript window.location.href = `__BaseURL__/oauth2/v1/logout ?id_token_hint=${id_token} &post_logout_redirect_uri=${YOUR_REDIRECT_URL}`; ``` **Revocation (`/revoke`)** ```javascript await fetch('__BaseURL__/oauth2/v1/revoke', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ token: 'TOKEN_TO_REVOKE', client_id: '__ProjectID__', }), }); ``` ### Silent Authentication Silent authentication lets you refresh sessions without prompting the user, as long as they still have an active Descope session. ``` __BaseURL__/oauth2/v1/authorize?...&prompt=none ``` Common scenarios include SPAs refreshing tokens or background session renewal. ### Passing Dynamic Values into Flows When redirecting to `/authorize`, you can include additional query parameters and consume them inside your Descope flow with a Scriptlet: ```javascript const query = new URL(startUrl).searchParams; return { age: query.get('age'), plan: query.get('plan'), }; ``` These values are available for conditions, screens, or custom logic inside the flow. ### JWT Verification To verify any JWT (such as `id_token` or `access_token`), retrieve Descope's public keys from the JWKs endpoint: ``` __BaseURL__/__ProjectID__/.well-known/jwks.json ``` Use these keys to validate signatures and confirm token authenticity in your backend. ### Testing the Endpoints You can experiment with Descope's OIDC endpoints using [OAuth Tools](https://oauth.tools) or your preferred API client. This is useful for validating flows, refreshing tokens, and checking error responses. With these endpoints and grant types, you can integrate Descope into any framework or custom OIDC client while maintaining the same level of security, observability, and user experience provided by Descope flows. # AWS Cognito (OIDC) (/identity-federation/applications/setup-guides/aws-cognito) Learn how to configure Descope as an OIDC provider with AWS Cognito to handle user authentication. # AWS Cognito (OIDC) With the power of [OpenID Connect](https://www.descope.com/learn/post/oidc), Descope acts as a federated identity provider to handle user authentication while Amazon Cognito acts as the primary identity provider and the user identity information store. With this integration, new users are automatically created in the user pool. An AWS Lambda function will then trigger to merge user identities in Amazon Cognito and retain all necessary roles and permissions. The simplified flow diagram below shows this process: ![Descope OIDC guide diagram of AWS Cognito as auth provider](/assets/descope-oidc-aws-cognito-auth-provider.webp) Follow the steps in this guide to configure your Amazon Cognito app to use Descope Flows. ## Setting up your Descope Flow If you want to use Passkeys, you can download the oidc-flow JSON from our [sample app repository](https://github.com/descope-sample-apps/aws-cognito-oidc-sample), which you can import into your own project. It is important to use this Flow, as it is designed to make sure the user and their email is always verified when using passkeys as an authentication method for security reasons. If your Android app renders this hosted flow in a WebView instead of a system browser, you may also need to register the app's signing fingerprint under **Android Fingerprints** in [Passkeys Settings](/auth-methods/passkeys/settings). ![Descope OIDC with AWS Cognito as auth provider flow configuration 1](/assets/descope-oidc-aws-cognito-auth-config-1.webp) Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). If you're using the `oidc-flow.json` provided above, edit the query parameter at the end of the Flow Hosting URL like so: `https://auth.descope.io/?flow=oidc-flow` ![Descope OIDC with AWS Cognito as auth provider flow configuration 2](/assets/descope-oidc-aws-cognito-auth-config-2.webp) You should keep this page open, as you're going to need this information for the next parts of this guide. If you would like to edit the UI of the login screen, you can do that in the Flow Editor. Once your flow is complete and your login redirect has been configured, you'll need to connect your Flow to Amazon Cognito by setting Descope up as an external provider. ## Descope as an external provider ### AWS Cloud Formation Script If you wish to use a pre-built [Cloud Formation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html) script to setup Descope as an external provider, rather than following the steps individually, you can use the following script: This script will perform the following actions: - **Conditional User Pool Creation** - The script can either use an existing Cognito User Pool or create a new one based on the input provided. - **Descope Integration** - It sets up Descope as an external identity provider, configuring it with necessary credentials and settings. - **Identity Federation Handling** Through the Lambda function, it handles scenarios where a user may have identities in both Descope and Cognito, merging these to maintain a consistent user identity across different login methods. - **IAM Role Configuration for Lambda** - Ensures that the Lambda function has the necessary permissions to execute its intended operations, particularly those involving interactions with the Cognito User Pool. Unless you're intimately familiar with Cloud Formation and understand how to use these scripts, it's recommended to perform the steps listed below manually one-by-one. That way you can control the configuration in a more granular fashion. Copy this lambda, and change the following values to use it: 1. **Parameters** - You will need to specify the following items: - **Descope Project ID** - Your Project ID from Project Settings - **Descope Access Key** - An Access Key secret generated under Access Keys - **Cognito User Pool ID (Optional)** - If you want to apply this configuration to a previously defined Cognito user pool, include the ID in your cloud formation script - **Issuer URL from Descope** - The issuer URL of Descope (as an OIDC provider). This can be found under Applications -> Your OIDC app -> Issuer ![Descope Issuer URL](/assets/descope-issuer-url.webp) 2. **User Pool and Client Properties** - You need to specify the properties for the Cognito User Pool and User Pool Client according to your requirements. 3. **Lambda Function Code** - The Lambda function code should be written in Python and embedded in the `ZipFile` property under the `OIDCUserMergeLambda` resource. Alternatively, you can upload the code to an S3 bucket and reference it in the template. ```yaml AWSTemplateFormatVersion: '2010-09-09' Description: CloudFormation template for configuring Cognito with Descope. Parameters: DescopeClientId: Type: String Description: Descope Project ID DescopeClientSecret: Type: String Description: Descope Access Key IssuerURL: Type: String Description: Issuer URL from Descope ExistingUserPoolId: Type: String Default: "" Description: Existing Cognito User Pool ID (leave blank to create a new one) Conditions: CreateNewUserPool: !Equals [!Ref ExistingUserPoolId, ""] Resources: UserPool: Type: "AWS::Cognito::UserPool" Condition: CreateNewUserPool Properties: # User Pool properties UserPoolClient: Type: "AWS::Cognito::UserPoolClient" Properties: # User Pool Client properties IdentityProvider: Type: "AWS::Cognito::UserPoolIdentityProvider" Properties: UserPoolId: !If [CreateNewUserPool, !Ref UserPool, !Ref ExistingUserPoolId] ProviderName: "Descope" ProviderType: "OIDC" ProviderDetails: client_id: !Ref DescopeClientId client_secret: !Ref DescopeClientSecret attributes_request_method: "GET" oidc_issuer: !Ref IssuerURL authorize_scopes: "openid profile email descope.custom_claims descope.claims" # Additional properties LambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: "lambda.amazonaws.com" Action: "sts:AssumeRole" Policies: - PolicyName: "CognitoLambdaExecutionPolicy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "cognito-idp:*" Resource: "*" OIDCUserMergeLambda: Type: "AWS::Lambda::Function" Properties: FunctionName: "OIDC_USER_MERGE" Runtime: "python3.8" Handler: "index.lambda_handler" Role: !GetAtt LambdaExecutionRole.Arn Code: ZipFile: | import boto3 client = boto3.client('cognito-idp') def lambda_handler(event, context): # Please include your custom lambda trigger code here (e.g. the example one shown below) Outputs: UserPoolId: Description: ID of the Cognito User Pool Value: !If [CreateNewUserPool, !Ref UserPool, !Ref ExistingUserPoolId] UserPoolClientId: Description: ID of the Cognito User Pool Client Value: !Ref UserPoolClient ``` #### Important Notes: - **IAM Role and Policies** - The `LambdaExecutionRole` is configured to allow the Lambda function to perform actions on Cognito User Pools. Adjust the permissions according to your security requirements. - **Testing and Security** - Test this template in a controlled environment before deploying it in production. Ensure that all sensitive information is handled securely. Once you've run this code, Descope should be configured as an external provider, and you should be good to go! If you're interested in viewing a sample application of Cognito working with Descope, you can skip to the bottom of this [doc](#sample-app) ### Creating / Using a User Pool In order to set up Descope as an external provider with Amazon Cognito, you'll first need to [create a user pool](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-as-user-directory.html) in Amazon Cognito, if you don't already have one. Just make sure that your user pool requires the email attribute and allows a sign in with email option. ![Descope OIDC with AWS Cognito as auth provider cognito configuration 1](/assets/descope-oidc-aws-cognito-config-1.webp) ![Descope OIDC with AWS Cognito as auth provider cognito configuration 2](/assets/descope-oidc-aws-cognito-config-2.webp) Once you have a configured user pool, you'll need to set up Descope as an external provider. ### What you'll need from the Descope Console In the Descope Console, you'll need a few things in order to configure the external provider. Fetch all of this information and put it in the respective fields in the configuration page: - **Provider Name**: Call it **Descope** - **Issuer URL**: This is the Issuer URL which is found under your **Authentication Methods -> SSO -> Identity Provider** configuration settings - **Client ID**: Your Descope Project ID, which can be found under [Project Settings](https://app.descope.com/settings/project) in the Descope Console - **Client Secret**: Access key generated under [Access Keys](https://app.descope.com/m2m/accessKeys) in the Descope Console ### Creating the External Provider in Cognito 1. Under **Authorized Scopes**, add the following scopes: ***openid profile email descope.custom_claims descope.claims***. The *descope.custom_claims* scope will allow us to include custom claims defined in the flow, and the *descope.claims* scope will return roles / permissions, tenants in the JWT to return back to Cognito. 2. Make sure that the **Attribute Request Method** is *GET*. There's no need to add an **Identifier**. If custom claims or roles are not passed in from Descope, but if they are configured to be accepted in your Cognito configuration, then the previous value will be saved in the Cognito user and not be replaced. As an example, if you're mapping roles but then the use in Descope originally has a role and then all role information is removed. In that case, the original role information will persist in Cognito. However, new values such as an updated Role name will replace the original value. At this point, your configuration screen should look something like this: ![Descope OIDC with AWS Cognito as auth provider cognito configuration 3](/assets/descope-oidc-aws-cognito-config-3.webp) 2. Under **Retrieve OIDC Endpoints**, select the Auto *fill through Issuer URL* option and paste in the Issuer URL you got from the Console in step 2 (e.g. `https://api.descope.com/`) 3. Finally, you'll need to map your attributes between Descope and Amazon Cognito in the following fashion: ![Descope OIDC with AWS Cognito as auth provider cognito configuration 4](/assets/descope-oidc-aws-cognito-config-4.webp) Here, you can edit the custom claims that come back to Amazon Cognito after Descope authentication is complete if you wish. You will need to map the corresponding key in the Custom Claim action (in the Flow) with the attributes mapped here. ![Descope OIDC with AWS Cognito as auth provider flow configuration 5](/assets/descope-oidc-aws-cognito-config-5.webp) If you're using the Cognito Hosted UI, you should now automatically see an option to sign in with Descope: ![Descope OIDC with AWS Cognito testing completed flow](/assets/descope-oidc-aws-cognito-completed.webp) However, if you're using a custom UI (as I suspect most of you are), then you'll need to add a way to start the OIDC flow. To start the OIDC flow, you'll need to navigate to the OAuth `/authorize` endpoint, as explained [here](https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html). Here is an example URL: ``` https://sample-app-prod.auth.us-west-2.amazoncognito.com/oauth2/authorize?identity_provider=Descope&redirect_uri=https://app.example.com/dashboard&response_type=CODE&client_id=cognito_app_client_id&scope=email%20openid%20phone ``` Once you have your external provider configured and your login screen working, you should be able to sign in with Descope, based on how you've configured your Flow. All of the OIDC logic will work in the background. Your users should be able to sign in using your personalized Descope Flow or continue using the traditional authentication methods defined by your user pool. So we're done right? Well, almost… ## Setting up an AWS Lambda trigger Since we're using Descope as a federated IdP, the users will not automatically merge together. This means that if a user logs in with Descope and logs in with Amazon Cognito, two separate users with differing permissions and roles will be configured in the user pool. We can handle this issue by using an AWS Lambda trigger to merge the user identities, so that the same user can use either method of login and gain access to the same account. To configure this user merge functionality follow the steps below: - Head to the **User Pool Properties** tab in your Amazon Cognito dashboard, and under **Lambda Triggers**, select *Add Lambda Trigger*. This will open up a new tab. - Configure your Lambda trigger to look like the screenshot below, and then select **Create a Lambda Trigger**: ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 1](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-1.webp) - Select **Create Function** in the top right corner, and configure the following items: - Function name: **OIDC_USER_MERGE** - Runtime: Select **Python** - Create the function, and then in the **Code** section, paste the code snippet shown below. This function will also print out the needed event and user information when merging identities, so that you can track it in your AWS CloudWatch logs. ```python import boto3 client = boto3.client('cognito-idp') def lambda_handler(event, context): print("Event: ", event) email = event['request']['userAttributes']['email'] # Find a user with the same email response = client.list_users( UserPoolId=event['userPoolId'], AttributesToGet=[ 'email', ], Filter='email = "{}"'.format(email) ) print('Users found: ', response['Users']) for user in response['Users']: provider = None provider_value = None # Check which provider it is using if event['userName'].startswith('descope_'): provider = 'Descope' provider_value = event['request']['userAttributes']['name'] print('Linking accounts from Email {} with provider {} provider_value {} '.format( email, provider, provider_value )) # If the signup is coming from a social provider, link the accounts # with admin_link_provider_for_user function if provider and provider_value: print('> Linking user: ', user) print('> Provider Id: ', provider_value) response = client.admin_link_provider_for_user( UserPoolId=event['userPoolId'], DestinationUser={ 'ProviderName': 'Cognito', 'ProviderAttributeValue': user['Username'] }, SourceUser={ 'ProviderName': provider, 'ProviderAttributeName': 'Cognito_Subject', 'ProviderAttributeValue': provider_value } ) # Return the event to continue the workflow return event ``` - Finally, go back to the original tab you had open, make sure that the Lambda trigger is selected, and add it to your user pool with the **Add Lambda trigger** button: ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 2](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-2.webp) We're almost there! Now all we have to do is make sure that the Lambda trigger has the correct permissions configured to be able to access the user identities through the SDK. ## Adding permissions for Lambda trigger In order to use this Lambda trigger to merge identities, the SDK being used will need to have access to your user pool and all of the identities stored in it. If you try logging in with Descope at this time, you'll notice that the user merging fails because of a permission issue. To resolve this, you'll need to create a new [identity permissions policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) in the [AWS IAM Console](https://aws.amazon.com/iam/), and make sure that Lambda trigger role is assigned to that new policy. You can do that by following the steps below: - Head to your IAM Console, select Policies, and click on the blue **Create New Policy** button in the top right hand corner. - Under *Select a Service*, select **Cognito User Pools**. - Give permission to all Cognito User Pool actions, and make sure you specify what Resource ARNs you will need for the user merging process. In the example below, I've selected all, but you will most likely only need to access to the **userpool** ARN. ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 3](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-3.webp) - After clicking Next, your final screen should look something like the screenshot below. Add a name and description to the policy and click on **Create Policy**. ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 4](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-4.webp) - Now head to the *Roles* section of the IAM Console, search for the **OIDC_PROD_MERGE** (or whatever you decided to name the Lambda trigger you created in the previous section) and select it. ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 5](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-5.webp) - Select **Attach policies** under *Add permissions*. ![Descope OIDC with AWS Cognito as auth provider setting up lambda trigger 6](/assets/descope-oidc-aws-cognito-auth-lambda-trigger-6.webp) - Search for the policy you created, and add it as a permission policy to this specific role. After that, your Lambda trigger will have full access to all of the user identity information and will be able to use the Amazon Cognito SDK to perform the merging tasks. Now, whenever you sign in with Descope, it will automatically check to see if the user identity already exists in the user pool based on the user's email coming from Descope. If a user already exists, all relevant properties will be merged. If you are using AWS Lambda triggers to merge the user identity, make sure in your Flow that the user has been verified by OTP, Magic Link, or by signing in with OAuth Providers like Google or Facebook. The reason for this, is to ensure that the Descope user account you're merging with the one in Cognito is indeed associate with that same user. In the `oidc-flow.json` Flow provided for Descope Passkeys [above](#setting-up-your-descope-flow), for example, the user's email is verified before the flow is completed. ## Sample App If you're interested in seeing how this is implemented in a sample React application, feel free to check out our [sample app](https://github.com/descope-sample-apps/aws-cognito-oidc-sample) on GitHub. If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Azure AD B2C (OIDC) (/identity-federation/applications/setup-guides/azure-ad-b2c-oidc) Configure Descope as a federated IdP with Azure AD B2C, enabling seamless integration of Descope Flows for enhanced authentication in your apps. # Integrating Descope with Azure AD B2C as a Federated Identity Provider In this guide, we'll walk through the steps to configure Descope as a federated Identity Provider (IdP) with Azure AD B2C. This setup allows you to use Descope Flows and authentication methods, such as passkeys and webauthn, while retaining the use of Azure B2C and the Active Directory in your application. ## Prerequisites - An active Azure AD B2C Tenant. - An active Descope Account. ## Step 1: Setting up your Descope Flow If you want to use Passkeys, make sure that you're verifing the user's email the first time you create a passkey for a specific user account and that you're checking to make sure user's without a verified email cannot login, for security purposes. ![Descope OIDC with Auth0 as auth provider flow configuration 1](/assets/descope-auth0-saml-configuration-flow-1.webp) Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). You can also host the flow yourself with any one of our client SDKs as well. ## Step 2: Configuring Descope as an OpenID Connect (OIDC) Identity Provider in Azure AD B2C ### Finding Your Azure AD B2C Tenant Domain 1. **Log in to Azure Portal**: Navigate to the [Azure Portal](https://portal.azure.com) and sign in. 2. **Access Azure AD B2C Service**: Search for and select the Azure AD B2C service. 3. **Tenant Overview**: Your B2C Tenant domain is displayed in the format `.onmicrosoft.com`. ![Tenant domain location in Azure B2C Portal](/assets/descope-tenant-azure-b2c-1.webp) For those who already have a working application with a Azure AD B2C tenant, you can also alter your pre-existing flow with the following steps. ### Setting Up a New User Flow in Azure AD B2C 1. **Create User Flow**: In Azure AD B2C, go to `User flows` and create a new sign-up and sign-in flow. ![Create new user flow in Azure B2C Portal](/assets/descope-tenant-azure-b2c-2.webp) ![Create new user flow in Azure B2C Portal](/assets/descope-tenant-azure-b2c-3.webp) 2. **Add Identity Provider**: In Azure AD B2C, navigate to `Identity providers` and select "New OpenID Connect provider". Here you'll need to gather the following information the Descope Console: | Information | Description | |-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Metadata URL | Found in the Descope Console under: `Applications` -> `Select your App` -> `App Discovery URL`. See picture below for location. | | Client ID | Your Descope Project ID, located in the Console under [Project Settings](https://app.descope.com/settings/project). | | Client Secret | Your Descope Access Key, generated in the Console under [Access Keys](https://app.descope.com/m2m/accessKeys). | | Scopes | Should be set to `openid profile email descope.custom_claims`. Optionally, `descope.claims` can be included for passing role/tenant information to Azure. | You'll then input it in the identity provider config in the Azure Portal like this: ![Azure identity provider config](/assets/descope-azure-identity-config.webp) This is where you can get the Metadata URL: ![Create new user flow in Azure B2C Portal](/assets/descope-azure-b2c-portal.webp) If you wish to use a different flow or want to host the flow yourself, you'll want to change the Flow Hosting URL to wherever the flow is located, and which one in the query parameter at the end of the URL string. 4. **Configure User Attributes**: Choose the user attributes you want to collect and return during the authentication process. You will need `User ID` at the very least, which is typically mapped to `sub`. This part you can configure exactly how you want, with you passing the claims in your flow with the `Custom Claims` action at the end. ![User attributes in azure identity provider config](/assets/descope-azure-identity-config-2.webp) This is how the attribute keys are mapped in the flow: ![Descope flow custom claims action](/assets/descope-flow-custom-action.webp) Once you've completed these steps, you can save your identity provider configuration and then proceed to configuring the rest of your [Azure User Flow](https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-user-flow). ## Step 3: Adjusting Azure AD B2C Settings 1. **Select Descope as Identity Provider**: Ensure Descope is selected as the Identity Provider in your User Flow. 2. **Disable Local Accounts and Other Identity Providers**: To enforce authentication via Descope, disable local account sign-ins and other identity providers in the User Flow. If you still want to use other auth methods as a backup, you can continue allowing `Local Accounts (with email)` however you won't be able to have a seamless redirect to your hosted flow page. ![Disabled local accounts in azure config](/assets/descope-disabled-local-azure-config.webp) By doing this, you'll make sure that you're automatically redirected to either the Hosted Auth page (`auth.descope.io`) or wherever else you're hosting the flow component, configured in the Descope Console under Applications. ## Step 4: Merging User Identities Between Descope and Azure B2C At this point, you're almost done with the setup process. The problem right now is that if you sign in as a pre-existing user with Descope, instead of merging the identities and logging you in as that same pre-existing user, a new user will be created. To avoid this duplication of users, you'll need to create a [Custom Policy](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policy-overview) in your Azure B2C configuration to handle this. Typically this can require a bit of complexity in setting up, however, you can use the Azure B2C Policy Setup Tool to help automatically add the necessary policy to your Azure instance. Follow the steps below to do this: If you want to look at other pre-built custom policies you can visit this [website](https://b2ciefsetupapp.azurewebsites.net/Home/Experimental), or if you want to download the Azure Custom Policy starter pack and modify them yourself you can get it from [GitHub](https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack) ### Instructions for Setting Up Auto-Linking Policy 1. Navigate to the [Setup Tool](https://b2ciefsetupapp.azurewebsites.net/), enter your Azure B2C domain, and deploy the custom starter pack: You'll need to give permission to the app in order for it to deploy the custom starter pack on your behalf. If you wish to do this manually, follow the instructions on Microsoft's [documentation page](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-policies-series-overview) ![Custom starter pack deployment](/assets/descope-starter-pack-deployment.webp) Then select the next few links shown below, to complete the setup and configuration of the starter pack. ![Second step in the setup process](/assets/second-step-setup-process.webp) 2. Once the starter pack has been deployed, navigate to the list of custom policies [here](https://b2ciefsetupapp.azurewebsites.net/Home/Experimental), input your `Azure Domain` and `auto-account-linking` under **Sample Folder Name**, and click `Deploy custom policy sample`. ![Installing auto-linking policy](/assets/install-auto-linking-policy.webp) This will install the custom policy that will automatically link user accounts together with the same email address. After this is complete, you should see this screen: ![Installation of auto-linking policy complete](/assets/installation-of-policy-complete.webp) 3. Next, you'll need to download your current user flow XML, for which you'll need to modify over the next few steps. This can be done in the Azure Portal here: ![Downloading the user flow XML](/assets/download-the-user-flow-xml.webp) 4. Then, you'll need to merge the that existing policy, with this account linking policy: ```xml .onmicrosoft.com B2C_1A_TrustFrameworkExtensions Azure Active Directory PolicyProfile ``` The way you merge these is by following the comments in the XML below and doing each of the instructions one by one: ```xml .onmicrosoft.com B2C_1A_TrustFrameworkExtensions Azure Active Directory ``` 5. Finally, you'll want to upload and test your new modified policy. You can do so under `Upload Custom Policies` in the Azure Portal: ![Uploading custom policy](/assets/upload-custom-policy-azure.webp) From this screen, once the policy is uploaded, you can test the user journey to make sure everything is working ok. For this, you can use the `Run now` feature in the Azure portal. You'll want to verify that users with the same email address in Descope and Azure AD B2C are merged correctly. Once you've done this, you should be all set to start using Descope in your apps! ## Understanding the Sample App There is a React and .NET sample app showcases how to use Descope with Azure B2C. These sample apps can help you understand how Descope integrates with Azure, and how the OIDC redirection works. These apps are also helpful if you're also interested in how you can get user information and display it in your frontend, from the attributes/claims you pass back from Azure to the application. These are controlled under [Application Claims](https://learn.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview#claims) in your Azure B2C User Flow configuration page. 1. **React** - [GitHub Link](https://github.com/descope-sample-apps/descope-azure-b2c-react-sample) 2. **ASP.NET** - [GitHub Link](https://github.com/descope-sample-apps/aspnetcore-b2c-descope-sample) # Document360 (OIDC) (/identity-federation/applications/setup-guides/document360-oidc) Learn how to set up Descope as a federated Identity Provider (IdP) to implement authentication for Document360. # Document360 (OIDC) In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) using OpenID Connect (OIDC) to implement authentication for Document360. ## Create Document360 Federated App You will first have to configure Document360 as a Federated App in Descope: 1. Navigate to [Federated Apps](https://app.descope.com/applications) in the Descope Console 2. Click on `+ Application` in the top right 3. Choose "Generic OIDC Application" 4. Type in your Application name (e.g., "Document360") 5. Optionally provide an Application ID and Description 6. Click on the "Create" button When users sign in to Document360, they will be redirected to Descope's hosted authentication flow. To modify the authentication flow, change the [Flow Hosting URL](/identity-federation/auth-hosting) in your Federated App Configuration. ## Document360 Configuration Now, configure the SSO settings in Document360: 1. Open Document360 in a separate tab or panel. 2. Navigate to **Settings > Users & security > SAML/OpenID** in Document360 (this menu covers both SAML and OpenID Connect protocols). 3. Click the **Create SSO** button. 4. Select **Others** as your Identity Provider (IdP) on the **Choose your Identity Provider (IdP)** page. 5. Choose **OpenID** as the protocol in the **Configure the Service Provider (SP)** page. 6. Enter the corresponding values from your Descope Federated Application: | Document360 | Descope | | ----------- | ---------------- | | Client ID | **Project ID** (found in [Project Settings](https://app.descope.com/settings/project)) | | Client Secret | **Access Key** (created in [Access Keys](https://app.descope.com/m2m/accessKeys)) | | Authority | **Authorization URL** (found in your Descope Federated App Configuration) | 7. On the **More Settings** page, configure the following: - **SSO name**: Enter a name for the SSO configuration. - **Customize login button**: Enter the text for the login button displayed to users. - **Auto assign reader group**: Toggle on/off as needed. - **Sign out idle SSO team account**: Toggle on/off based on your requirements. - **Convert existing team and reader accounts to SSO**: Choose users to send invite to. Click **Create** to complete the OIDC configuration. # Firebase (OIDC) (/identity-federation/applications/setup-guides/firebase-oidc) this guide covers how to set up Descope as a federated Identity Provider (IdP) to implement Descope Flows for applications that currently use Firebase. # Firebase (OIDC) With the power of [OpenID Connect](https://www.descope.com/learn/post/oidc), Descope acts as a federated identity provider to handle user authentication while Firebase acts as the primary identity provider and the user identity information store. We'll also show you how to handle user linking for accounts with identical emails logged in via an OIDC provider and another Firebase account that uses username and password. The simplified flow diagram below shows this process: ![Descope OIDC guide diagram of Firebase as auth provider](/assets/descope-oidc-firebase-auth-guide-diagram.webp) Follow the steps in this guide to configure your Firebase app to use Descope Flows. ## Setting up your Descope Flow If you want to use Passkeys, you can download the oidc-flow JSON from our [sample app repository](https://github.com/descope-sample-apps/firebase-oidc-sample), which you can import into your own project. It is important to use this Flow, as it is designed to make sure the user and their email is always verified when using passkeys as an authentication method for security reasons. ![Descope OIDC with Firebase as auth provider flow configuration 1](/assets/descope-firebase-oidc-auth-config-1.webp) Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). If you're using the `oidc-flow.json` provided above, edit the query parameter at the end of the Flow Hosting URL like so: `https://auth.descope.io/?flow=oidc-flow` ![Descope OIDC with Firebase as auth provider flow configuration 2](/assets/descope-firease-oidc-auth-config-2.webp) You should keep this page open, as you're going to need this information for the next parts of this guide. If you would like to edit the UI of the passkey login screen, you can do that in the Flow Editor. Once your flow is complete and your login redirect has been configured, you'll need to connect your flow to Firebase by setting Descope up as an OIDC provider. ## Setting up Descope as an OIDC Provider Let's jump into the setup process to get Descope added as an OIDC provider in your Firebase project: 1. Navigate to the [Firebase Console](https://console.firebase.google.com/u/0/), select Authentication under Build (in Product Categories), and go to Sign-in method. 2. Select Add New Provider in the top right corner, and then click on the OpenID Connect.3. Make sure that Code flow is selected and fill in the following necessary details: - **Name**: Call it Descope - **Client ID**: Your Descope Project ID, which can be found under [Project Settings](https://app.descope.com/settings/project) in the Descope Console - **Issuer URL**: This is the Issuer URL which is found under your SSO Configuration settings (see below) - **Client Secret**: Access key generated under [Access Keys](https://app.descope.com/m2m/accessKeys) in the Descope Console Once you've gathered all of this information, put it in the Firebase console as shown below: ![Descope OIDC with Firebase as auth provider Firebase configuration 1](/assets/descope-firease-oidc-auth-provider-config-1.webp) Once these steps have been completed, you're all set to implement the new login process and Descope Flow in your application. We'll use a sample React application and Firebase to demonstrate OIDC, which you can find on [GitHub](https://github.com/descope-sample-apps/firebase-oidc-sample). ## Firebase authentication SDK Firebase gives total flexibility on how to configure your login pages and define the overall process, so the instructions below may differ depending on your app implementation. We will use the [Firebase SDK](https://firebase.google.com/docs/auth/web/start#add-initialize-sdk) to handle the entire OIDC authentication process, which makes it very simple to implement. It will also help us easily handle user linking, which will be discussed later on. For the login screen UI, the easiest approach is to use FirebaseUI. If you've used Firebase for a while, you may be using the [legacy web namespaced API](https://firebase.google.com/docs/web/modular-upgrade) instead of the Web Modular API. For something simple like authentication, using the legacy Web namespaced API might be simpler as you won't need to refactor older code using the compat libraries. ## Building out the login page with Firebase When designing your login page with Firebase, most developers either choose to use FirebaseUI (a library for customizing your login UI, recommended) or build out the authentication process themselves. In this blog, we'll show you how to do it with FirebaseUI, and then explain how it can be implemented manually. If you already have a functioning login page with Firebase, you can skip to the [Embedding Descope in your app](#embedding-descope-in-your-app) section of the tutorial. ### Using FirebaseUI FirebaseUI is a library provided by Firebase that you can use to quickly implement authentication functionality in your app. [FirebaseUI](https://firebase.google.com/docs/auth/web/firebaseui) provides pre-made user interface components for authentication and supports multiple authentication methods, including email/password, social login, and most importantly, OIDC. If you're already using FirebaseUI, you'll need to simply add your OIDC provider configuration as a sign-in option in your [uiConfig](https://firebase.google.com/docs/auth/web/firebaseui#sign_in): ```javascript signInOptions: [ firebase.auth.EmailAuthProvider.PROVIDER_ID, "oidc.descope", // "oidc." ], ``` This will allow FirebaseUI to display a button that will start the OIDC flow and allow the user to log in with Descope instead of Firebase. Under **callbacks**, you can also add an arrow function in your uiConfig to navigate to a different protected page like a dashboard. After these additions, your uiConfig will look something like this: ```javascript const uiConfig = { signInFlow: "redirect", signInOptions: [ firebase.auth.EmailAuthProvider.PROVIDER_ID, "oidc.descope", ], callbacks: { signInSuccessWithAuthResult: () => { navigate("/dashboard"); return false; }, }, }; ``` Finally, you'll need to embed the **StyledFirebaseAuth react component** in your page. Once embedded, you should see two buttons, one for email and password, and another for OIDC login with Descope. If a user selects **Sign in with Descope**, the OIDC login flow will start and the user will be automatically redirected to the **Flow Hosting URL** that you previously configured in the Descope Console. If you would like to style your buttons and change the text, you can include a fullLabel, buttonColor, and iconUrl field for each of the sign in options in your **uiConfig**, like so: ```javascript signInOptions: [ { provider: firebase.auth.EmailAuthProvider.PROVIDER_ID, fullLabel: "Sign in with Email", // Optional }, { provider: "oidc.descope", fullLabel: "Sign in with Passkeys", // Optional buttonColor: "#000000", // Optional iconUrl: "https://images.ctfassets.net/...", // Optional }, ] ``` With the configuration above, my login page looks like this: ![Descope OIDC with Firebase as auth example login page](/assets/descope-oidc-firebase-auth-example-login.webp) With FirebaseUI, that's pretty much it in regards to building out the login page with a **Sign in with Descope** button. If you're building out your login page with the SDK, you can follow the instructions below. Otherwise, skip to the [Embedding Descope in your app](#embedding-descope-in-your-app) section. ### Using the SDK without FirebaseUI If you wish to design your own login page without FirebaseUI, you'll first need to initialize Descope as the provider using the following line of code: ```javascript const provider = new firebase.auth.OAuthProvider("oidc.descope"); ``` Then, you'll need to create a function that can invoke the OIDC authentication flow. Depending on how you want to customize your login screen, you can invoke this function with a button or some other custom UI component: ```javascript const signInWithDescope = async () => { try { await firebase.auth().signInWithRedirect(provider); } catch (error) { console.error("Descope Sign-in Error", error); } }; ``` All of the provider settings come from the configuration we set previously in the Firebase Console, so this simple **async** function is all we should need to log the user in using OIDC. After following all of these steps, if you invoke the **SignInWithDescope** function, you should automatically be redirected to where your flow is. ## Embedding Descope in your app We still don't have our Descope Flow embedded anywhere in our application, as we've just been setting up the Firebase side of things so far. In order to login with Descope Flows, you'll need to use the Descope React Component in your app. You would typically do this by creating a page in the location of the Flow Hosting URL you previously configured. In my example, I configured it to be `http://localhost:3000/login` for testing: ![Descope OIDC with Firebase as auth provider flow configuration 3](/assets/descope-firebase-oidc-auth-config-3.webp) Therefore, I will create a new page with the /login route to contain the Descope React Component like this: ```javascript return (
console.log(e.detail.user)} onError={(e) => console.log("Could not log in!")} theme="light" />
); ``` When the user is redirected to this page, they should see the screen to enable passkeys that is included in the **oidc-flow.json** file that's part of the GitHub repository of the sample application. After a user successfully logs in with Descope and verifies their email, Firebase will finish the OIDC flow and generate the JWT tokens that the rest of your application and APIs already use. For the end user, the experience will be very similar to before - it will seem as if they used Firebase to login, but they used Descope with passkeys instead of using a username and password. After following all of the steps so far, you now have full-fledged Descope powered Flows embedded in your Firebase app, enabling you to have passkeys as an alternative authentication method for your users. ## Handling user linking with Firebase We are almost done! The only remaining thing is to configure proper user linking in your app so that you don't have duplicate user records. If you test out the login right now, you may notice that user accounts don't link together. Instead, depending on the current configuration, you'll either see that two user accounts are created with the same email but different identity providers, or that one user account is created with just the last used login provider. To ensure that both login methods (email/password and passkeys) can be used to sign in to one particular user account, you'll need to link them using the Firebase SDK. Firebase provides the functionality to link user accounts sharing the same email address. This way, if a user logs in with an OIDC provider and another user account already exists in Firebase with the same email, Firebase can link these two accounts. To start out, you'll want to make sure that your Firebase application supports user account linking. To do this, head back to the Firebase Console -> Authentication -> Settings and make sure that **Link accounts that use the same email** is selected: ![Descope OIDC with Firebase as auth user linking 1](/assets/descope-oidc-auth-user-linking-1.webp) If this is not enabled, whenever you log in via email/password or log in via passkeys, you'll have two separate accounts with different identity providers, which is not what we want! Next, depending on how you've configured your login page, you'll either use FirebaseUI (**which will handle the linking for you**) or develop logic in your Firebase project to check for existing accounts and use the **linkWithCredential** function provided by Firebase to link the two accounts. ### User linking with FirebaseUI If you're using FirebaseUI, you can try to sign in with either Descope or your email/password right now. Only one user with two identity providers should exist with that email in the Firebase Console. The Firebase user ID stays the same regardless of the identity provider used during sign in. Since we verified the user's identity in our Descope Flow by validating the same email that was used in the account linking, the user can safely continue to sign in with passkeys in the future and gain full access to their account. It's important to understand that Firebase treats all OIDC providers as federated IdPs, meaning that during the account linking, the original user's credentials in Firebase will be replaced with the credentials from Descope. If the user attempts to sign in with their email and password again, after creating their passkey with Descope, then this screen will appear instead of a place to enter their password: ![Descope OIDC with Firebase as auth user linking 2](/assets/descope-oidc-auth-user-linking-2.webp) Therefore, if you're using FirebaseUI, you're all set and ready to deploy passkeys in your application for all users! However, if you've built out a custom login page with the SDK functions, read on. ### User linking with the SDK To prevent the replacement of the email/password-based account when signing in with Descope, you will have to manually manage the account linking process, which would look something like this: 1. User tries to sign in with Descope. 2. If an account already exists with the same email, Firebase will throw an **auth/account-exists-with-different-credential** error. 3. You catch this error, get the pending Descope credential from the error object, and save it somewhere (**e.g., in your app's state or localStorage**). 4. You then prompt the user to sign in with their email/password (**e.g., showing a form or redirecting to FirebaseUI**). 5. Once the user has signed in with their email/password, you get the current user's UserCredential object. 6. Then, you use the linkWithCredential method to link the Descope credential (**which you saved earlier**) with the existing account. The implementation of the merging logic in your application will looks similar to what I have done below in my Login.js: ```javascript import React, { useState } from 'react'; import firebase from 'firebase/app'; import 'firebase/auth'; import { useNavigate } from 'react-router-dom'; import EmailPasswordSignIn from './EmailPasswordSignIn'; const Login = () => { const [pendingCred, setPendingCred] = useState(null); const navigate = useNavigate(); const signInWithDescope = async () => { const provider = new firebase.auth.OAuthProvider('oidc.descope'); try { await firebase.auth().signInWithRedirect(provider); } catch (error) { if (error.code === 'auth/account-exists-with-different-credential') { setPendingCred(error.credential); } } }; const signInWithEmailPassword = async (email, password) => { try { const userCred = await firebase.auth().signInWithEmailAndPassword(email, password); if (pendingCred) { await userCred.user.linkWithCredential(pendingCred); setPendingCred(null); } navigate('/dashboard'); } catch (error) { console.error(error); // Handle error } }; // EmailPasswordSignIn is a custom login component like FirebaseUI return (
{pendingCred ? ( ) : ( )}
); } ``` This code also includes the previously mentioned **signInWithDescope** function, and stores the pending credential information in a React state hook. Once you've implemented this login page successfully, your users should be able to use multiple identity providers to log in and access their accounts. And that's it! If you're curious, you can read more about linking user accounts with different authentication methods on the [Firebase](https://firebase.google.com/docs/auth/web/account-linking#web-namespaced-api_1) docs. If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Setup Guides (/identity-federation/applications/setup-guides) List of all setup guides for Descope as an IdP # Federated Application Setup Guides You can configure Federated Applications on the [Federated Apps](https://app.descope.com/applications) page of the Descope console. While you can connect any application using OIDC or SAML, we provide templates for some popular applications in our Federated Apps Library, which contain step-by-step instructions on how to configure the connection to the specific Service Provider. ![Federated Apps Library](/assets/federated-apps-library.webp) We also provide the following guides to walk through the setup of some other common Federated Apps: ## Guides } title="AWS Cognito (OIDC)" description="Use this guide to enable OIDC SSO for Cognito using Descope as the IdP" /> } title="Auth0" description="Use this guide to enable SSO for Auth0 using OIDC and SAML with Descope as the IdP" /> } title="Azure AD B2C (OIDC)" description="Use this guide to enable SSO for Azure AD B2C using OIDC with Descope as the IdP" /> } title="Document360 (OIDC)" description="Use this guide to enable SSO for Document360 using OIDC with Descope as the IdP" /> } title="Firebase (OIDC)" description="Use this guide to enable SSO for Firebase using OIDC with Descope as the IdP" /> } title="Keycloak" description="Use this guide to enable SSO for Keycloak using OIDC and SAML with Descope as the IdP" /> } title="Metabase (SAML)" description="Use this guide to enable SSO for Metabase using OIDC and SAML with Descope as the IdP" /> } title="Ping Identity (OIDC)" description="Use this guide to enable SSO for Ping Identity using OIDC with Descope as the IdP" /> } title="Retool (OIDC)" description="Use this guide to enable SSO for Retool using OIDC with Descope as the IdP" /> } title="Salesforce (OIDC)" description="Use this guide to enable SSO for Salesforce using OIDC with Descope as the IdP" /> } title="Zoho (SAML)" description="Use this guide to enable SSO for Zoho using SAML with Descope as the IdP" /> # Metabase (SAML) (/identity-federation/applications/setup-guides/metabase-saml) This guide provides a comprehensive walkthrough for setting up Metabase as your Identity Provider (IdP) for SSO with your Descope project # Metabase (SAML) This guide details the steps required to configure Descope to work with Metabase as a federated identity provider, using SAML. You can refer to the Metabase SAML documentation for more information on how this works and how to configure the Metabase side: [Metabase SAML Authentication](https://www.metabase.com/docs/latest/people-and-groups/authenticating-with-saml). Keep in mind that only the Pro and Enterprise tiers of Metabase work with SAML authentication. ## Table of Contents 1. [Descope Configuration](#descope-configuration) 2. [Metabase Configuration](#metabase-configuration) These steps will be taken in parallel, so it's a good idea to have the Descope Console and your Metabase Settings Dashboard both open before you begin. ## Descope Configuration 1. **Create a New SAML Application**: In your Descope console, go to the [Applications](https://app.descope.com/applications) section in the Descope Console and add a new SAML application. ![App new Application in Descope](/assets/descope-new-application.webp) 2. **Configure Descope's SAML Settings in Metabase**: Copy the `Entity ID` and `XML URL` from Descope and paste them into the respective fields in Metabase's SSO Configuration. ![SAML IdP settings to copy over to Metabase](/assets/saml-idp-settings.webp) You can skip to the [Metabase Configuration](#metabase-configuration) section to get instructions on where to put these in Metabase. Once you've completed the Metabase Configuration steps, return here to complete the setup process. 3. **Enter Metabase's Metadata**: You'll need to select `Enter the connection details manually`, and from there input the `ACS URL`, `Entity ID` and `Certificate` that you retrieved from Metabase in Step 3 under [Metabase Configuration](#metabase-configuration) below. 4. **SSO Mapping**: This is found in the settings for your new SAML application. You'll need to map at least email to each of your Descope Users. It's also a good idea to map first and last name as well, along with SAML groups that are already configured in Metabase. You can do so by configuring the SSO mapping like this: | Descope Field | SAML Claim | |-----------------|----------------------------------------------------------------------------| | Email | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` | | Given Name | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname` | | Family Name | `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname` | | Descope Roles | `http://schemas.xmlsoap.org/claims/Group` Here is an example of what this looks like: ![SSO Mapping in Descope](/assets/ssomapping.webp) ## Metabase Configuration 1. **Set up SAML in Metabase**: Navigate to the Admin Panel in Metabase, then to the Authentication tab. Select 'SAML' and enable it. 2. **Configure SAML Settings**: Fill in the necessary details such as `SAML Identity Provider URL`, `Entity ID`, etc., as required by Metabase. 3. **Copy Over IdP Configuration Settings**: You'll need to gather the `URL the IdP should redirect to`, as well as the `Entity ID` and `Certificate` from Metabase for use in the Descope Console. For more information, visit the Metabase [docs page](https://www.metabase.com/docs/latest/people-and-groups/authenticating-with-saml#enabling-saml-authentication-in-metabase). ![Configuring SAML in Metabase](/assets/configuring-saml-in-metabase.webp) Now return to the Descope Console and put in the necessary information under **SP Configuration**. These steps are detailed above, under Step 3 of the [Descope Configuration](#descope-configuration) section. # Ping Identity (OIDC) (/identity-federation/applications/setup-guides/ping-identity) This guide covers how to set up Descope as a federated Identity Provider (IdP) to implement Descope Flows for applications that currently use Ping Identity # Ping Identity (OIDC) Application Setup Guide Descope's integration as an OpenID Connect (OIDC) provider enables seamless authentication using federated identity with Ping Identity. This capability allows you to leverage Descope's passwordless methods and passkeys while maintaining Ping Identity as your primary identity provider. The process ensures that user identities are efficiently managed and consolidated. Follow these steps to configure Ping Identity to use Descope as a federated OIDC provider. ## Setting up Descope as OIDC Provider You will need to either create an [OIDC Application](/identity-federation/applications/oidc-apps) or use the default one to use Descope as an federated OIDC provider. You can find these in the Descope Console, under `Applications`. For most applications, you'll just use the Default OIDC application found [here](https://app.descope.com/applications/descope-default-oidc). Descope Flows are hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). Learn more about our hosted app [here](/identity-federation/auth-hosting). ![Descope OIDC with Ping Identity provider flow configuration 2](/assets/descope-oidc-ping-auth-config-2.webp) Keep this page open as you'll need this information for subsequent steps. You can customize the UI of the login screen in the Flow Editor. ## Configuring Ping Identity to Use Descope ### Configuring the OIDC Application in Ping Identity 1. First, you'll want to create a new `External Identity Provider` in Ping, as shown here: ![Create new external IdP in Ping](/assets/descope-create-idp-ping.webp) 2. Next, you'll need to select `OpenID Connect` as the Provider Type, and give your new external IdP a name. In this case, it's called `Descope`: ![Descope name for external IdP](/assets/descope-name-ext-idp-ping.webp) 3. Then, you should input the following items from Descope: - **Client ID**: Your Descope Project ID from [Project Settings](https://app.descope.com/settings/project) in Descope - **Client Secret**: An Access Key from [Access Keys](https://app.descope.com/m2m/accessKeys) in Descope - **Discovery Document URI**: Found under your IdP app configuration: **Applications -> Default/Custom App -> Discovery URL** in the Descope Console. Your configuration screen in Ping Identity should mirror the following setup: ![Descope OIDC with Ping Identity provider configuration](/assets/descope-ping-identity-oidc-config.webp) You can click on `Use Discovery Document` after pasting in the URL, and all the remaining fields should automatically populate. 4. Afterwards, you can make sure that your attributes are mapped properly. You can adjust custom claim mappings in Ping Identity post-authentication by matching keys from Descope's `Custom Claims` action with Ping Identity's attributes. ![Descope OIDC with Ping Identity attribute mapping](/assets/descope-ping-identity-attribute-mapping.webp) You can make the `Username` mapped to any identifier you want by simply changing the `providerAttributes.sub` value. Once you've configured this, you're almost all set to use your new external IdP with Ping. The logo and name of the button can be adjusted on the main configuration screen here: ![Descope OIDC with Ping Identity logo and name](/assets/descope-ping-idp-logo-name.webp) The only thing left to handle is the user linking, to make sure that users that sign in via Descope, as also the same users as what is provisioned in Ping. By default, this is something that is not enabled. This is typically not an issue for self-registered new users, but for will cause issues for existing users when they sign in with the same user identifier as their user that already exists in the Ping Directory. The next section will go into the specifics of how you properly handle this. ## Handling User Linking with Ping Identity As we approach the completion of integrating Descope as a federated OIDC provider with Ping Identity, it's crucial to ensure that user accounts are correctly linked. This step is vital to prevent the creation of duplicate user accounts and to provide a seamless login experience for users who may have existing accounts. ### Automatic Account Linking 1. **Navigate to Authentication Policies** - In your PingOne dashboard, go to `Authentication` > `Authentication`. 2. **Add a New Policy** - Click on the `+ Add Policy` button to create a new authentication policy. 3. **Specify the Policy Name** - Enter a unique name for the policy that will help you identify it later. 4. **Select the Step Type** - From the `Step Type` dropdown, choose `External Identity Provider`. 5. **Choose Your External IdP** - In the `External Identity Provider` dropdown, select the external identity provider that you have set up, in this case, `Descope`. 6. **Set the Required Authentication Level** (Optional) - You can specify an authentication context level that you wish to request from the identity provider. - This step is optional and can be used for more granular control over the authentication process, such as using selectors on incoming contexts to determine policy flows. 7. **Save the Policy** - Click `Save and Continue` to store the settings of your new policy. This is an example of what your policy should look like: ![Ping Authentication Policy](/assets/descope-ping-auth-policy.webp) ### Post-External Authentication After the user is redirected back from the external identity provider: - **For New Users**: If the user does not have an existing account in PingOne, a new user account will be created. - **For Existing Users**: If a user account already exists, PingOne will prompt the user to link their new external identity to their existing account. This linking process ensures that users have a single, unified account, reducing confusion and streamlining the sign-in process across different authentication methods. By following these steps, you will have effectively enabled user linking within your application, utilizing Ping Identity's built-in capabilities. This will allow users to authenticate with Descope and link their identity to their existing accounts seamlessly. To learn more about how to specifically use and sign in your new external IdP, you can also read about external IdPs in Ping's documentation [here](https://docs.pingidentity.com/pingone/integrations/p1_external_idps.html). If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Retool (OIDC) (/identity-federation/applications/setup-guides/retool-oidc) How you can configure SSO with OIDC and Descope to work with your Retool resources and apps. # Using SSO With Retool (OpenID Connect) This document outlines the process to configure Descope SSO Authentication with Retool. This configuration will help you securely access your Retool apps and protect your resources and APIs. This guide will show you how to configure SSO with the [Cloud](https://retool.com/) and [Self-Hosted](https://retool.com/self-hosted/) versions of Retool. Retool supports both OIDC and SAML SSO, however because of the additional features OIDC allots (such as being able to use information from ID tokens in your Retool apps), it is the preferred method of SSO with Descope. If you've already configured SAML, but would like to switch to using OIDC, you can follow the guide [here](https://docs.retool.com/sso/guides/saml-to-oidc). Custom Authentication only works with [Enterprise](https://retool.com/in/pricing) level subscriptions to Retool. Make sure you have the right type of Retool account before continuing with this guide. ## Table of Contents - [**Retool OIDC Configuration**](#retool-oidc-configuration) - [Protecting Retool Apps and Workflows](#protecting-retool-applications-and-workflows) - [Protecting APIs](#protecting-apis) - [**Role Mapping**](#role-mapping) - [**Referencing JWT claims**](#referencing-jwt-claims) ## Retool OIDC Configuration You'll need the following items to configure integrate Descope with OIDC: - **Client ID**: Your Descope Project ID, which can be found under [Project Settings](https://app.descope.com/settings/project) in the Descope Console - **Client Secret**: Access key generated under [Access Keys](https://app.descope.com/m2m/accessKeys) in the Descope Console - **Scopes**: The scopes that are defined for authorization of Descope resources, such as - `openid email profile` - **Authorization URL**: `__BaseURL__/oauth2/v1/authorize` - **Token URL**: `__BaseURL__/oauth2/v1/token` ### Protecting Retool Applications and Workflows For Cloud-Hosted versions of Retool - use the **Retool SSO Settings** page in your Retool dashboard to configure SSO. For Self-Hosted versions of Retool - create [environment variables](https://docs.retool.com/self-hosted/reference/environment-variables#authentication) to configure SSO. If self-hosting, make sure that the **BASE_DOMAIN** environment variable is set as well. This will ensure links using your domain (e.g. new user invitations and forgotten password resets) are correct. This image is an example of the setup procedure in a cloud-hosted deployment of Retool: ![Descope OIDC with Retool Configuration](/assets/descope-retool-oidc-config.webp) If you wish to use role-based authorization with Retool, you can map the corresponding roles defined in Descope, with roles you've already defined in Retool. You can read about this in the [Role Mapping](#role-mapping) section below. Finally, you can consider enabling a few of the other options you see at the bottom of the configuration page, such as [**JIT - Just in Time Provisioning**](https://docs.retool.com/sso/guides/jit-provisioning). ![JIT User Provisioning](/assets/jit-user-provisioning.webp) Once you've configured SSO, hit **Save** in the top right corner, and the next time you login with Retool you should see your login page look like this: ![Descope OIDC with Retool Configuration](/assets/descope-retool-oidc-login.webp) Now you can use authentication methods not supported by Retool, such as Passkeys, and also Descope as an IdP for handling all of your user identities. ### Protecting APIs Whether you're using Self-Hosted or Cloud-Hosted Retool, the process for building a Custom Auth workflow to authenticate with protected APIs is the same and fairly straightforward. The steps below will show you how to create a [Resource](https://docs.retool.com/data-sources/quickstarts/resources) and utilize Descope JWTs for authentication: 1. Visit the Retool Resources page in your dashboard, and create a new resource in the top right corner 2. Select **REST API** from the list, and then select **Custom Auth** underneath _Authentication_ 3. Click **Add a new step to the auth workflow**, and then fill in the necessary OIDC information listed [above](#retool-oidc-configuration) 4. Add another step to the auth flow, and then select **Define a Variable** The access token that Descope will provide (which contains all of your custom claims), will be exported as `{{oauth1.accessToken}}`. In order to access it, to include in your HTTP header, you will need to define it as another variable like this: ![Defining a Variable in OIDC REST API Config](/assets/define-variable-oidc-rest-api.webp) 5. Once you're variable has been defined, you can include the variable as an `Authorization Bearer` token in the HTTP header above: ![Defining a Variable in OIDC REST API Config](/assets/define-variable-oidc-rest-api-2.webp) ## Role Mapping You can also map the roles returned from the OpenID response to your Retool permission groups. Any Retool groups that are not specified in the role mapping are overwritten. The following example maps teacher and student roles to specific Retool permission groups. | Setting | Environment Variable | Example | | ------- | -------- | -------- | | Roles key | CUSTOM_OAUTH2_SSO_JWT_ROLES_KEY | accessToken.groups | | Role mapping | CUSTOM_OAUTH2_SSO_ROLE_MAPPING | teacher -> admin, student -> viewer | In this example above, `teacher` is the role defined in Descope and `admin` is the role defined in Retool. Here is an example of how this is configured in both Retool and Descope: ![Role Mapping in Retool SSO Configuration Page](/assets/role-mapping-retool.webp) ![Role Mapping in Retool SSO Configuration Page](/assets/role-mapping-descope.webp) There are more detailed instructions you can read about in the [Role Mapping](https://docs.retool.com/sso/guides/group-sync/) section of the Retool documentation. Once you've configured mapped the roles, you'll need to re-authenticate with OIDC or SAML in order to sync the group and user roles across Descope and Retool. Since the roles are mapped, you can use them to define [specific permissions](https://docs.retool.com/org-users/guides/users/user-permissions) for each user role. Custom permissions logic you can also integrate in your platform, using the `permissions` claim on the JWT, as described in the next section. ## Referencing JWT claims You can use custom claims included in the JWT to personalize Retool apps or control component permissions. Retool automatically includes these claims as the values of the `{{current_user.metadata.accessToken}}`. You can access them using curly braces anywhere in your Retool app, such as: `{{current_user.metadata.accessToken.picture}}` ![Role Mapping in Retool SSO Configuration Page](/assets/role-mapping-descope-claims.webp) All role and permission information relating to a specific user should automatically be included in the JWT returned by Descope. As the picture above shows, you should be able to see what information you can access from the JWT if you start to type after `current_user.metadata.` If you have any other questions about using Descope with Retool, feel reach to reach out to [us](/support)! # Zoho (SAML) (/identity-federation/applications/setup-guides/zoho-saml) Learn how to set up Descope as a federated Identity Provider (IdP) to implement authentication for Zoho. # Zoho (SAML) In this guide, we will cover how to set up Descope as an Identity Provider (IdP) for SAML Single Sign-on (SSO) with Zoho Help Center (the Service Provider). To do this, all you will need is: - A Descope account (you can [sign up](https://www.descope.com/sign-up) for a “Free Forever” account) - Access to a Zoho Help Center account Once you have the above, simply follow along with this guide to learn how to add Descope Flows to your application. ## Configuring Zoho Help Center Refer to the Zoho article on [Setting up SAML Single Sign-on for Help Center](https://help.zoho.com/portal/en/kb/desk/user-management-and-security/data-security/articles/setting-up-saml-single-signon-for-help-center#Setting_up_SAML_SSO) to understand how to set up Zoho SAML SSO from the Zoho Help Center side. It covers enabling and disabling SAML SSO for Zoho Help Center from the Descope side. ## Configuring Descope as the IdP ### Setting up your Hosted Auth Page Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). ### Configuring an federated application 1. Log into your Descope account 2. Navigate to Dashboard -> Applications 3. Click “+Application” 4. Input a name (e.g., “Zoho Help Center”) and choose SAML 5. Click “Create” ### In the Zoho SAML Help Center Configuration, do the following: - Copy the SSO URL from Descope into the Remote Login URL field - Copy the Logout URL from Descope to the Remote Logout URL field - Download the “public certificate” from Descope by clicking “Descope Certificate” and then “public certificate.” Upload this in Zoho as the Public Key - Click “Save” in Zoho Help Center ![Zoho Help Center](/assets/zoho-auth-page.webp) ### In the Descope SAML Application you just created, do the following: 1. Under the “Service Provider” section in Descope, select the “Enter the Connection Details Manually” option. 2. Copy the Help Center SAML Response URL from Zoho into the ACS URL field 3. Copy the Entity ID from Zoho into the Entity ID field 4. Leave the certificate input box empty 5. Add your ACS URL to the Allowed ACS Callback URLs (e.g., `https://*.zohoportal.com/*`) 6. Set the SAML Assertion Subject Type to Email 7. Set the SAML Assertion NameID Format to Email 8. Copy the Default Relay State from Zoho to the Default Relay State in Descope 9. Click “Save” in Descope ![Zoho Descope Config](/assets/zoho-descope-config.webp) That’s it! Your end users will now be redirected to the Descope’s sign-in page when signing in to the Help Center. # Sign-In with Auto Sign-up (/api/magic-link/email/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending a magic link via email Initiate a process that implements both sign-in and sign-up using a single endpoint. Descope will generate and deliver a clickable magic link to the email address specified. If the email address is already registered (the end user has already registered) the user will be signed in. If the email address is not registered (the end user is not yet registered) the user will be signed up. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link/with-sdks/client#introduction) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/magic-link/email/sign-up) endpoint if you want a sign-up flow that will fail if the end user is already registered. - Use the [Sign-In](/api/magic-link/email/sign-in) endpoint if you want a sign-in flow that will fail if the end user isn't yet registered. # Sign-In (/api/magic-link/email/sign-in) ### Sign-in existing end user by sending a magic link via email Initiate a sign-in process by sending a magic link to an existing end user. Descope will generate and deliver a clickable magic link to the email address specified. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. The endpoint will return a failure code if the email address is not registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link/with-sdks/client#introduction) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/magic-link/email/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/magic-link/email/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/magic-link/email/sign-up) ### Sign-up new end user by sending a magic link via email Initiate a sign-up process by sending a magic link to a new end user. Descope will generate and deliver a clickable magic link to the email address specified. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate magic links that have already been sent. The endpoint will return a failure code if the email address is already registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link/with-sdks/client#introduction) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-In](/api/magic-link/email/sign-in) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/magic-link/email/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Email (/api/magic-link/email/update-email) ### Update email of end user by sending magic link via email Update the email address of an existing end user by sending a magic link to the new email address. Descope will generate and deliver a clickable magic link to the new email address specified. After successfully verifying the magic link token the new email address will be used to deliver new magic links via email. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Generate Embedded Link (/api/magic-link/embedded-link/generate) ### Generate an embedded link for an existing user Initiate a sign-in process by generating an embdedded link for an existing user utilizing a management key. The endpoint will return a token which can then be verified using the Magic Link [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### Next Steps Verify the embedded link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Embedded link Authentication](/customize/auth/embeddedlink/) for details about implementing embedded links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Sign-In with Auto Sign-up (/api/magic-link/sms/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending a magic link via SMS Initiate a process that implements both sign-in and sign-up using a single endpoint. Descope will generate and deliver a clickable magic link as an SMS to the phone number specified. If the phone number is already registered (the end user has already registered) the user will be signed in. If the email address is not registered (the end user is not yet registered) the user will be signed up. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/magic-link/sms/sign-up) endpoint if you want a sign-up flow that will fail if the end user is already registered. - Use the [Sign-In](/api/magic-link/sms/sign-in) endpoint if you want a sign-in flow that will fail if the end user isn't yet registered. # Sign-In (/api/magic-link/sms/sign-in) ### Sign-in existing end user by sending a magic link via SMS Initiate a sign-in process by sending a magic link to an existing end user. Descope will generate and deliver a clickable magic link as an SMS to the phone number specified. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. The endpoint will return a failure code if the email address is not registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/magic-link/sms/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/magic-link/sms/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/magic-link/sms/sign-up) ### Sign-up new end user by sending a magic link via SMS Initiate a sign-up process by sending a magic link to a new end user. Descope will generate and deliver a clickable magic link to the phone number specified. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate magic links that have already been sent. The endpoint will return a failure code if the email address is already registered. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. ### Next Steps Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-In](/api/magic-link/sms/sign-up) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/magic-link/sms/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Phone Number (/api/magic-link/sms/update-phone) ### Update phone number of end user by sending magic link via SMS Update the phone number of an existing end user by sending a magic link to the new phone number. Descope will generate and deliver a clickable magic link as an SMS to the new phone number specified. After successfully verifying the magic link token the new phone number will be used to deliver new magic links via SMS. The clickable magic link is made up of two parts - the URI you provide in the `URI` field and the magic link token generated by Descope. For example, if `URI=https://app.mycompany.com/magiclink/verify`, the clickable magic link will be `https://app.mycompany.com/magiclink/verify?t=magic-link-token.` Magic links expire in the time frame configured in the [Descope console](https://app.descope.com/settings/authentication/magiclink), so sending multiple magic links (for example, when an end user tries to sign-up a second or third time) does not invalidate prior magic links that have already been sent. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. Note that `URI` is an optional parameter. If omitted - the project setting will apply. If provided - it should to be part of the allowed `Approved Domains` configured in the project settings. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Step Verify the magic link token using the [Verify Token](/api/magic-link/verification/verify-token) endpoint. ### See Also - See [Magic link Authentication](/auth-methods/magic-link) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Verify Token (/api/magic-link/verification/verify-token) ### Verify the magic link token from the end user Verify that the magic link token in the URL clicked by the end user matches and has not expired. This endpoint completes the magic link flow for: * sign up * [Sign-Up via email](/api/magic-link/email/sign-up) * [Sign-Up via SMS](/api/magic-link/sms/sign-up) * sign-in * [Sign-In via email](/api/magic-link/email/sign-in) * [Sign-In via SMS](/api/magic-link/sms/sign-in) * sign-in with auto sign-up * [Sign-In with Auto Sign-up via email](/api/magic-link/email/sign-in-auto-sign-up) * [Sign-In with Auto Sign-up via SMS](/api/magic-link/sms/sign-in-auto-sign-up) * update data * [update email](/api/magic-link/email/sign-up) * [update phone number](/api/magic-link/email/sign-up) ### Next Steps The response object will contain the user's details including the session and refresh JWTs. ### See Also - See [Magic link Authentication](/auth-methods/magic-link/with-sdks/client#introduction) for details about implementing magic links. - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. # Delete access key attribute (/api/management/access-key-management/delete-access-key-attribute) Delete a single access key custom attribute definition by machine name (no-op if absent), using a valid management key. # Get access key attribute (/api/management/access-key-management/get-access-key-attribute) Get a single access key custom attribute definition by machine name, using a valid management key. # Rotate Access Key (/api/management/access-key-management/rotate-access-key) Rotate an access key — regenerates the secret for an existing access key while preserving the same key ID, name, roles, tenants, expiry and metadata. The new cleartext is returned exactly once and the previous secret stops working immediately. # Set access key attribute (/api/management/access-key-management/set-access-key-attribute) Create or update a single access key custom attribute definition (upsert by machine name), using a valid management key. # Revoke agentic identities (/api/management/agentic-identity-hub/revoke-agentic-identities) Revoke agentic identities and invalidate their consents — the same operation as [revoking access in the Console](/agentic-identity-hub/core-components/agents#revoking-access). Combine filters to narrow scope: - `clientId` + `userId` — one user's grant to a specific agent - `consentId` + `userId` — a specific consent for a user - `resourceId` + `userId` — all of a user's grants on one MCP server [Resource](/resources) - `userId` alone — all agentic identities for that user across every MCP server Returns the number of identities revoked in `revoked`. # Search agentic identities (/api/management/agentic-identity-hub/search-agentic-identities) List [agentic identities](/agentic-identity-hub/core-components/agents) — authorization records that bind an OAuth client to a user, tenant, or autonomous client. Filter by `clientId`, `resourceId`, `userId`, or `consentId`. Returns consent details, agent name, resource ID, and client ID for each identity. Use `page` (zero-based) and `limit` for pagination. # Create Audit Event (/api/management/audit/create-audit-event) ### Create an audit log event, using a valid management key. This API endpoint allows you to create an audit log utilizing various parameters and returns the results in JSON format. # Search Analytics (/api/management/audit/search-analytics) Search analytics (summarized) data grouped by time periods, using a valid management key. # Search Audit (/api/management/audit/search-audit) ### Search the audit log, using a valid management key. This API endpoint allows you to search the audit log utilizing various search parameters and returns the results in JSON format. # Create connector (/api/management/connector-management/create-connector) Create a connector, using a valid management key. A single connector resource maps to either a connectorservice connector or a messaging provider, routed by type. # Delete connector (/api/management/connector-management/delete-connector) Delete a single connector by id, using a valid management key. # Get connector (/api/management/connector-management/get-connector) Get a single connector by id, using a valid management key. # Update connector (/api/management/connector-management/update-connector) Update a single connector, using a valid management key. # Activate Access Key (/api/management/access-keys/activate-access-key) ### Activate an existing access key, using a valid management key. This API endpoint allows administrators to activate an existing access key. ### Next Steps Once you have reactivated the access key, you can utilize it to configure external items such as [SCIM](/api/scimmanagement/), or use it to [exchange for a JWT](/api/access-keys/exchange-key). ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Batch Activate Access Keys (/api/management/access-keys/batch-activate-access-keys) ### Activate existing access keys in batch, using a valid management key. This API endpoint allows administrators to activate existing access keys in batch. ### Next Steps Once you have reactivated the access key, you can utilize it to configure external items such as [SCIM](/api/scimmanagement/), or use it to [exchange for a JWT](/api/access-keys/exchange-key). ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Batch Deactivate Access Keys (/api/management/access-keys/batch-deactivate-access-keys) ### Deactivate existing access keys in batch, using a valid management key. This API endpoint allows administrators to deactivate existing access keys in batch. Once the access keys have been deactivated, their access will be revoked until reactivated. ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Batch Delete Access Keys (/api/management/access-keys/batch-delete-access-keys) ### Delete existing access keys in batch, using a valid management key. This API endpoint allows administrators to delete existing access keys in batch. Once the access keys have been deleted, their access will be revoked. ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Create Access Key (/api/management/access-keys/create-access-key) ### Create an access key, using a valid management key. This API endpoint allows administrators to create an access key. During the creation of the access key, you can set the name, expiration time, roles and tenant:role pairs to associated with the key. ### Next Steps Once you have the access key, you can utilize it to configure external items such as [SCIM](/api/scimmanagement/), or use it to [exchange for a JWT](/api/access-keys/exchange-key). ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Deactivate Access Key (/api/management/access-keys/deactivate-access-key) ### Deactivate an existing access key, using a valid management key. This API endpoint allows administrators to deactivate an existing access key. Once the access key has been deactivated, it's access will be revoked until reactivated. ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Delete Access Key (/api/management/access-keys/delete-access-key) ### Delete an existing access key, using a valid management key. This API endpoint allows administrators to delete an existing access key. Once the access key has been deleted, it's access will be revoked. ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Import Access Key (/api/management/access-keys/import-access-keys) Import an access key by providing its plaintext value, using a valid management key. # Access Key Management API Overview (/api/management/access-keys) Use the Descope API to manage your access keys with a management key. # Access Key Management ## Overview Access keys are similar to API keys — they let administrators authenticate into an application using the Descope authentication service. Access keys can be exchanged for a session JWT. The Access Key Management APIs provide endpoints for managing these keys. ## Use Cases 1. [Load an Access Key](/api/management/access-keys/load-access-key) 2. [Search Access Keys](/api/management/access-keys/search-access-keys) 3. [Create Access Key](/api/management/access-keys/create-access-key) 4. [Update Access Key](/api/management/access-keys/update-access-key) 5. [Activate Access Key](/api/management/access-keys/activate-access-key) 6. [Deactivate Access Key](/api/management/access-keys/deactivate-access-key) 7. [Activate Access Keys in Batch](/api/management/access-keys/batch-activate-access-keys) 8. [Deactivate Access Keys in Batch](/api/management/access-keys/batch-deactivate-access-keys) 9. [Delete Access Keys in Batch](/api/management/access-keys/batch-delete-access-keys) 10. [Delete Access Key](/api/management/access-keys/delete-access-key) ## Examples ### Example - create an access key and exchange it for a JWT 1. Call the [Create Access Key](/api/management/access-keys/create-access-key) endpoint and capture the generated key from the `cleartext` field in the response. 2. Pass the key to the [Exchange Access Key](/api/access-keys/exchange-key) endpoint to exchange it for a JWT. ### Example - search and deactivate an access key 1. Call the [Search Access Keys](/api/management/access-keys/search-access-keys) endpoint to find the key you want to deactivate. 2. Call the [Deactivate Access Key](/api/management/access-keys/deactivate-access-key) endpoint using the access key ID from step 1. # Load An Access Key (/api/management/access-keys/load-access-key) ### Load an access key, using a valid management key. This API endpoint allows administrators to load the details of an existing access key. The response contains details of the access key including associated roles and tenants as well as details of the key's creation, status, and expiration. ### Next Steps Once you have this data, you can utilize the response to [Update an access key](/api/management/access-keys/update-access-key), [Activate an access key](/api/management/access-keys/activate-access-key), [Deactivate an access key](/api/management/access-keys/deactivate-access-key), or [Delete an access key](/api/management/access-keys/delete-access-key). ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Search Access Keys (/api/management/access-keys/search-access-keys) ### Search access keys, using a valid management key. This API endpoint allows administrators to search for details of existing access keys for a given array of tenants. The response contains an array of details for the access keys returned by the search including associated roles and tenants as well as details of the key's creation, status, and expiration. ### Next Steps Once you have this data, you can utilize the response to [Update an access key](/api/management/access-keys/update-access-key), [Activate an access key](/api/management/access-keys/activate-access-key), [Deactivate an access key](/api/management/access-keys/deactivate-access-key), or [Delete an access key](/api/management/access-keys/delete-access-key). ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Update Access Key (/api/management/access-keys/update-access-key) ### Update an existing access key, using a valid management key. This API endpoint allows administrators to update an existing access key. With this endpoint, you can only update the access key's name. ### See also - See [Access Key Management](/access-keys) for further details on managing access keys. # Create a list of authz relations (/api/management/authz/create-relations) Create a list of authz relations. # Delete an authz namespace (/api/management/authz/delete-namespace) Delete an authz namespace and remove all related relations. # Delete an authz relation definition (/api/management/authz/delete-relation-definition) Delete an authz relation definition for your project and remove all related relations. # Delete all relations for a list of resources (/api/management/authz/delete-relations-for-resources) Delete all relations for the given list of resources. # Delete a list of authz relations (/api/management/authz/delete-relations) Delete a list of authz relations. # Delete all relations with matching resourceIds for a list of resources (/api/management/authz/delete-resource-relations-for-resources) Delete all relations for the given list of resources in which they are the resource. # Delete an authz schema (/api/management/authz/delete-schema) Delete an authz schema thus removing authz from your project. # Validate an authz schema and preview the changes (/api/management/authz/dry-run-schema) Preview schema diff between existing and new schema, without saving it. # Return the list of targets and resources changed since the given date (/api/management/authz/get-modified) Return the list of targets and resources changed since the given date. Should be used to invalidate local caches. # Check a list of relation queries (/api/management/authz/has-relations) Check a list of relation queries. # FGA Management API Overview (/api/management/authz) Use the Descope API to manage fine-grained authorization in your application. # Fine-Grained Authorization (FGA) Management ## Overview The Descope FGA/Authz API endpoints let developers implement Relationship-based Access Control (ReBAC) and Attribute-based Access Control (ABAC) in their applications. These endpoints are part of the Descope Management API and require a Project ID and Management Key. Management keys are generated from **Company > Management Keys**. Project IDs are found in the console at **Project > Project ID**. Include both in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Save Schema](/api/management/authz/save-schema) — define a ReBAC or ABAC schema 2. [Create Relations](/api/management/authz/create-relations) — create relations between entities 3. [Has Relations](/api/management/authz/has-relations) — check if relations exist ## Example - ReBAC 1. [Create and save a ReBAC schema](/api/management/authz/save-schema) 2. [Create relations between entities](/api/management/authz/create-relations) 3. [Check to see if relations exist](/api/management/authz/has-relations) # Load an authz schema (/api/management/authz/load-schema) Load an authz schema for your project. # Load a list of defined relations for the given resource (/api/management/authz/resource-relations) Load a list of defined relations for the given resource. # Save an authz namespace (/api/management/authz/save-namespace) Save (create or update) an authz namespace for your project. # Save an authz relation definition (/api/management/authz/save-relation-definition) Save (create or update) an authz relation definition for your project. # Save an authz schema (/api/management/authz/save-schema) Save (create or update) an authz schema for your project. # Load a list of defined relations for the given list of targets (/api/management/authz/targets-relations) Load a list of defined relations for the given list of targets without recursively traversing the relation tree. # Load the resources that the target has the given relation to including all derived relationss (/api/management/authz/what-can-target-access-with-relation) Load the resources that the target has the given relation to including all derived relations # Load a list of relations for the given target including all derived relations (/api/management/authz/what-can-target-access) Load a list of relations for the given target including all derived relations. # Query who can access resource with relation (/api/management/authz/who-can-access) Return a list of users who can access a given resource with given relation. # Create Descoper (/api/management/descopers/create-descopers) Create a descoper # Delete Descoper (/api/management/descopers/delete-descoper) Delete a descoper # Get Descoper (/api/management/descopers/get-descoper) Get a descoper # List Descopers (/api/management/descopers/list-descopers) List descopers # Update Descoper (/api/management/descopers/update-descoper) Update a descoper # Get embedded link settings (/api/management/embedded-link-management/get-embedded-link-settings) Get the project's embedded link settings, using a valid management key. # Set embedded link settings (/api/management/embedded-link-management/set-embedded-link-settings) Set the project's embedded link settings, using a valid management key. # Generate a token for user sign up, later can be verified with magiclink (/api/management/embedded-link/embedded-link-signup) Generate a token for user sign up # Create dynamic registration template (/api/management/dynamic-registration-templates-management/create-dynamic-registration-template) Create a new dynamic registration template, using a valid management key. # Delete dynamic registration template (/api/management/dynamic-registration-templates-management/delete-dynamic-registration-template) Delete a dynamic registration template by id, using a valid management key. # Delete dynamic registration templates (/api/management/dynamic-registration-templates-management/delete-dynamic-registration-templates) Delete multiple dynamic registration templates by id, using a valid management key. # Load all dynamic registration templates (/api/management/dynamic-registration-templates-management/load-all-dynamic-registration-templates) Load all dynamic registration templates, using a valid management key. # Load dynamic registration template (/api/management/dynamic-registration-templates-management/load-dynamic-registration-template) Load a dynamic registration template by id, using a valid management key. # Update dynamic registration template (/api/management/dynamic-registration-templates-management/update-dynamic-registration-template) Update an existing dynamic registration template, using a valid management key. # Get enchanted link settings (/api/management/enchanted-link-management/get-enchanted-link-settings) Get the project's enchanted link settings, using a valid management key. # Set enchanted link settings (/api/management/enchanted-link-management/set-enchanted-link-settings) Set the project's enchanted link settings, using a valid management key. # Create engine (/api/management/engines-management/create-engine) Create a new engine, returning its ID and secret. Requires a valid management key. # Delete engine (/api/management/engines-management/delete-engine) Delete an engine, using a valid management key. # Load engine by ID (/api/management/engines-management/load-engine) Load an engine by ID, using a valid management key. # Load all engines (/api/management/engines-management/load-engines) Load all engines for the project, using a valid management key. # Rotate engine secret (/api/management/engines-management/rotate-engine-secret) Rotate an engine's secret, returning the new secret. The previous secret is immediately invalidated. Requires a valid management key. # Update engine (/api/management/engines-management/update-engine) Update an existing engine, using a valid management key. # Delete flow (/api/management/flow-management/delete-flow) Delete a single flow by id, using a valid management key. # Delete widget (/api/management/flow-management/delete-widget) Delete a single widget by id, using a valid management key. # Get flow (/api/management/flow-management/get-flow) Get a single flow by id in its exported representation, using a valid management key. # Get widget (/api/management/flow-management/get-widget) Get a single widget by id in its exported representation, using a valid management key. # Set flow (/api/management/flow-management/set-flow) Create or update a single flow from its exported representation, remapping any connector and role references to matching entities in the project, using a valid management key. # Set widget (/api/management/flow-management/set-widget) Create or update a single widget and its embedded flows from the exported representation, remapping any connector and role references to matching entities in the project, using a valid management key. # AuthZEN Action Search (/api/management/fga/auth-zen-action-search) Return the actions the subject may perform on the given resource (OpenID AuthZEN Action Search API). # AuthZEN Access Evaluation (/api/management/fga/auth-zen-evaluation) Evaluate a single OpenID AuthZEN Access Evaluation request. # AuthZEN Access Evaluations (/api/management/fga/auth-zen-evaluations) Evaluate a batch of OpenID AuthZEN Access Evaluation requests, with optional default values and combining semantics (execute_all, deny_on_first_deny, permit_on_first_permit). # AuthZEN Resource Search (/api/management/fga/auth-zen-resource-search) Return the resources of the given type the subject may access with the given action (OpenID AuthZEN Resource Search API). # AuthZEN Subject Search (/api/management/fga/auth-zen-subject-search) Return the subjects permitted to perform the given action on the given resource (OpenID AuthZEN Subject Search API). # Check FGA Permission (/api/management/fga/check-permission) ### Check FGA permission This endpoint allows you to check if a target has a specific relation to a resource using Fine-Grained Authorization. # Create FGA Backup (/api/management/fga/create-fga-backup) Create a new FGA backup. # Get FGA Relations (/api/management/fga/create-relations) ### Get FGA relations This endpoint allows you to retrieve relations for a given target or resource using Fine-Grained Authorization. # Delete All FGA Relations (/api/management/fga/delete-all-fga-relations) Delete all project FGA relations # Delete FGA Backup (/api/management/fga/delete-fga-backup) Delete an FGA backup. # Delete FGA Relations (/api/management/fga/delete-relations) ### Delete FGA relations This endpoint allows you to delete relations using Fine-Grained Authorization. # Get FGA Backup (/api/management/fga/get-fga-backup) Get an FGA backup by ID. # Get Mappable Resources (/api/management/fga/get-mappable-resources) ### Get mappable resources This endpoint allows you to retrieve mappable resources for Fine-Grained Authorization. # Get Mappable Schema (/api/management/fga/get-mappable-schema) ### Get mappable schema This endpoint allows you to retrieve the mappable schema for Fine-Grained Authorization. # Get FGA Schema (/api/management/fga/get-schema) ### Get FGA schema This endpoint allows you to retrieve the current Fine-Grained Authorization schema for your project. # Fine-Grained Authorization (FGA) API Overview (/api/management/fga) Use the Descope API to manage Fine-Grained Authorization (FGA) with a management key. # Fine-Grained Authorization (FGA) API Overview ## Overview Fine-Grained Authorization (FGA) lets you manage complex authorization scenarios in your application. The FGA APIs let administrators manage authorization schemas and relations using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Check Permission](/api/management/fga/check-permission) — verify if a target has a specific relation to a resource 2. [Create Relations](/api/management/fga/create-relations) — create authorization relations between targets and resources 3. [Delete Relations](/api/management/fga/delete-relations) — remove authorization relations 4. [Get Schema](/api/management/fga/get-schema) — retrieve the current FGA schema 5. [Save Schema](/api/management/fga/save-schema) — create or update the FGA schema ## Examples ### Example - check permission Use the [Check Permission](/api/management/fga/check-permission) API endpoint to verify if a user has access to a specific resource. ### Example - manage relations 1. Use [Create Relations](/api/management/fga/create-relations) to establish authorization relationships. 2. Use [Delete Relations](/api/management/fga/delete-relations) to remove authorization relationships. ### Example - manage schema 1. Use [Get Schema](/api/management/fga/get-schema) to retrieve the current FGA schema. 2. Use [Save Schema](/api/management/fga/save-schema) to create or update the FGA schema. # List FGA Backups (/api/management/fga/list-fga-backups) List all FGA backups. # Load FGA Resources (/api/management/fga/load-resources-details) ### Load FGA resources This endpoint allows you to load resources for Fine-Grained Authorization. # Restore FGA Backup (/api/management/fga/restore-fga-backup) Restore from an FGA backup. # Save FGA Resources (/api/management/fga/save-resources-details) ### Save FGA resources This endpoint allows you to save resources for Fine-Grained Authorization. # Save FGA Schema (/api/management/fga/save-schema) ### Save FGA schema This endpoint allows you to save (create or update) the Fine-Grained Authorization schema for your project. # Search for FGA mappable resources (/api/management/fga/search-mappable-resources) Search for FGA mappable resources. # Generate JWT for Sign-In (/api/management/generic-auth/generate-jwt-sign-in) Generate a JWT for an existing user, using a valid management key. # Generate JWT for Sign-Up or Sign-In (/api/management/generic-auth/generate-jwt-sign-up-or-in) Create a new user and generate a JWT for them, or just generate a JWT if the user already exists. Uses a valid management key. # Generate JWT for Sign-Up (/api/management/generic-auth/generate-jwt-sign-up) Create a new user and generate a JWT for them, using a valid management key. # Complete External Authentication (/api/management/flows/complete-external-auth-flow) Complete an external authentication flow step. Called by the customer's backend after authenticating the user on their own page. Requires a valid management key. # Delete Flow (/api/management/flows/delete-flows) ### Delete a flow within a project utilizing a management key. This endpoint is used to delete a flow from a project by giving an existing flow ID to be deleted. ### See Also - See [Flow Overview](/customize/flows/) for more information on flows. - See [Manage Flows](/customize/manage_flows/) for more information on managing (export, import, delete, disable, enable) flows. # Export Flow Localization (/api/management/flows/export-flow-localization) Export flow localization, using a valid management key. # Export Flow (/api/management/flows/export-flow) ### Export an existing flow from a project utilizing a management key. This endpoint is used to export an existing flow from a project. The response is the JSON which includes the flow and associated screens. ### See Also - See [Flow Overview](/customize/flows/) for more information on flows. - See [Manage Flows](/customize/manage_flows/) for more information on managing (export, import, delete, disable, enable) flows. # Export Theme (/api/management/flows/export-theme) ### Export a theme from a project utilizing a management key. This endpoint is used to export a theme from a project. The response is the JSON of the theme. ### See Also - See [Styles Overview](/management/project-settings/styles) for more information on styles and themes # Get Management Flow async result (/api/management/flows/get-management-flow-async-result) Get the result from an async management flow execution (if any), using a valid management key. # Import Flow Localization (/api/management/flows/import-flow-localization) Import flow localization, using a valid management key. # Import Flow (/api/management/flows/import-flow) ### Import a flow within a project utilizing a management key. This endpoint is used to import a flow to a project. The request items for the `flow` and `screen` this endpoint can be received from the export flow endpoint. ### See Also - See [Flow Overview](/customize/flows/) for more information on flows. - See [Manage Flows](/customize/manage_flows/) for more information on managing (export, import, delete, disable, enable) flows. # Import Theme (/api/management/flows/import-theme) ### Import a theme to a project utilizing a management key. This endpoint is used to import a theme from a project. The request body for this endpoint can be received from the export theme endpoint. ### See Also - See [Styles Overview](/management/project-settings/styles) for more information on styles and themes # Flow and Style Management API Overview (/api/management/flows) Use the Descope API to manage your project's flows and styles with a management key. # Flow and Styles Management ## Overview The Flow and Styles Management APIs let you programmatically manage flows and themes using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [List/Search Flows](/api/management/flows/list-flows) 2. [Export a Flow](/api/management/flows/export-flow) 3. [Import a Flow](/api/management/flows/import-flow) 4. [Delete a Flow](/api/management/flows/delete-flow) 5. [Export a Theme](/api/management/flows/export-theme) 6. [Import a Theme](/api/management/flows/import-theme) ## Examples ### Example - migrate a flow to another project 1. [List/Search Flows](/api/management/flows/list-flows) to find the flow ID for the flow you want to export. 2. [Export Flow](/api/management/flows/export-flow) using the flow ID from step 1. 3. [Import Flow](/api/management/flows/import-flow) using the `flow` and `screen` from step 2 to import the flow into a different project. ### Example - migrate a theme to another project 1. [Export Theme](/api/management/flows/export-theme) to export the theme. 2. [Import Theme](/api/management/flows/import-theme) using the response from step 1 as the request body to import the theme into a different project. # List Flow Templates (/api/management/flows/list-flow-templates) List all available flow templates # List/Search Flows (/api/management/flows/list-flows) ### List or search flows within a project utilizing a management key. This endpoint is used to list or search flows within a project. To list all flows, send an empty body such as: `{ }` or `{ "ids": [] }`. To search for a flow or several flows, send a body with the flowIds you want to search such as `{ "ids": ["sign-in"] }` or `{ "ids": ["sign-in", "sign-up"] }`. ### See Also - See [Flow Overview](/customize/flows/) for more information on flows. - See [Manage Flows](/customize/manage_flows/) for more information on managing (export, import, delete, disable, enable) flows. # List all widgets (/api/management/flows/list-widgets) List all widgets in project # Run Management Flow asynchronously (/api/management/flows/run-management-flow-async) Run a management flow asynchronously, using a valid management key. # Run Management Flow (/api/management/flows/run-management-flow) Run a management flow, using a valid management key. # Update SSO Provider IDs (/api/management/internal/update-sso-provider-i-ds) Update SSO provider IDs and SCIM provider IDs for tenant SSO settings. This endpoint is not publicly documented. # Apply JWT Template From Library (/api/management/jwt-templates/apply-jwt-template-from-library) Materialise a library entry as a new project JWT template. Optional overrides let the caller pick a different name, swap tags, or amend the claim body before save. Strict validation runs as if it were a create. # Create JWT Template (/api/management/jwt-templates/create-jwt-template) Create a new JWT template. Strict validation runs first — if it fails, the response carries a list of `ValidationIssue`s with stable codes (RESERVED_CLAIM_KEY, NAME_MISSING, …) and the template is not saved. type must be "user" or "key". authSchema in {default,tenantOnly,none}. issuerType in {legacy,inbound,federated}. emptyClaimPolicy in {none,nil,delete}. The `template` field is the JSON object whose keys are claim names. # Delete JWT Template (/api/management/jwt-templates/delete-jwt-template) Delete a JWT template by id. The project's default templates are restored where this one was referenced. # List JWT Template Library (/api/management/jwt-templates/list-jwt-template-library) List the curated JWT template library Descope ships — starter templates with documented use cases, optional logos, and `experimental` flags. # List JWT Templates (/api/management/jwt-templates/list-jwt-templates) List every JWT template defined on the current project. Returns full field detail for each — name, description, type (key|user), tags, claim body, authSchema, issuerType, etc. # Load JWT Template Library Entry (/api/management/jwt-templates/load-jwt-template-library-entry) Load a single library entry by id, including the full claim body — required before applying. # Load JWT Template (/api/management/jwt-templates/load-jwt-template) Load a single JWT template by id. # Update JWT Template (/api/management/jwt-templates/update-jwt-template) Update an existing JWT template by id. Same strict validation as CreateJwtTemplate runs first; on failure the existing template is unchanged. # Validate JWT Template (/api/management/jwt-templates/validate-jwt-template) Dry-run validate a JWT template without saving. Pass either an inline `template` payload (to validate before create/update) or an existing `id` (to lint a saved template). Returns a list of `ValidationIssue`s — empty list means valid. # Get magic link settings (/api/management/magic-link-management/get-magic-link-settings) Get the project's magic link settings, using a valid management key. # Set magic link settings (/api/management/magic-link-management/set-magic-link-settings) Set the project's magic link settings, using a valid management key. # Add IPs to List (/api/management/lists/add-i-ps-to-list) Add one or more IPs to an existing IP list # Add Texts to List (/api/management/lists/add-texts-to-list) Add one or more text items to an existing text list # Check IP in List (/api/management/lists/check-ip-in-list) Check if a specific IP exists in a list # Check Text in List (/api/management/lists/check-text-in-list) Check if a specific text exists in a list # Clear List (/api/management/lists/clear-list) Clear all IPs from a list # Create List (/api/management/lists/create-list) Create a new list # Delete List (/api/management/lists/delete-list) Delete a list by ID # Get All Lists (/api/management/lists/get-all-lists) Get all lists # Get List By Name (/api/management/lists/get-list-by-name) Get a list by name # Get List (/api/management/lists/get-list) Get a list by ID # Import Lists (/api/management/lists/import-lists) Import multiple lists # Remove IPs from List (/api/management/lists/remove-i-ps-from-list) Remove one or more IPs from an existing IP list # Remove Texts from List (/api/management/lists/remove-texts-from-list) Remove one or more text items from an existing text list # Update List (/api/management/lists/update-list) Update an existing list # Create Management Key (/api/management/management-keys/create-management-key) Create a management key using another management key. # Delete Management Key (/api/management/management-keys/delete-management-keys) Delete a management key using another management key. # Get Management Key (/api/management/management-keys/get-management-key) Get a management key using another management key. # Search Management Keys (/api/management/management-keys/search-management-keys) Search management keys using another management key. # Update Management Key (/api/management/management-keys/update-management-key) Update a management key using another management key. All supported fields will be reset if not provided. # Create MCP Server Client (/api/management/mcp-server-client-management/create-mcp-server-client) Create an MCP Server Client, using a valid management key. # Delete MCP Server Client (/api/management/mcp-server-client-management/delete-mcp-server-client) Delete an MCP Server Client by ID, using a valid management key. # Delete MCP Server Clients (/api/management/mcp-server-client-management/delete-mcp-server-clients) Delete multiple MCP Server Clients by IDs, using a valid management key. # Get MCP Server Client Secret (/api/management/mcp-server-client-management/get-mcp-server-client-secret) Get MCP Server Client secret, using a valid management key. # Load MCP Server Client (/api/management/mcp-server-client-management/load-mcp-server-client) Load an MCP Server Client by ID, using a valid management key. # Rotate MCP Server Client Secret (/api/management/mcp-server-client-management/rotate-mcp-server-client-secret) Rotate MCP Server Client secret, using a valid management key. # Search MCP Server Clients (/api/management/mcp-server-client-management/search-mcp-server-clients) Search MCP Server Clients for a specific MCP Server, using a valid management key. # Update MCP Server Client (/api/management/mcp-server-client-management/update-mcp-server-client) Update an MCP Server Client, using a valid management key. # Delete OAuth provider (/api/management/oauth-management/delete-o-auth-provider) Delete a single OAuth provider by id (system providers are reset to disabled), using a valid management key. # Get OAuth provider (/api/management/oauth-management/get-o-auth-provider) Get a single OAuth provider by id, using a valid management key. # Get OAuth settings (/api/management/oauth-management/get-o-auth-settings) Get the project's general OAuth settings, using a valid management key. # Set OAuth provider (/api/management/oauth-management/set-o-auth-provider) Create or update a single OAuth provider, using a valid management key. # Set OAuth settings (/api/management/oauth-management/set-o-auth-settings) Set the project's general OAuth settings, using a valid management key. # Create MCP Server (/api/management/mcp-server-management/create-mcp-server) Create an MCP Server, using a valid management key. # Delete MCP Server (/api/management/mcp-server-management/delete-mcp-server) Delete an MCP Server by ID, using a valid management key. # Delete MCP Servers (/api/management/mcp-server-management/delete-mcp-servers) Delete multiple MCP Servers by IDs, using a valid management key. # Load All MCP Servers (/api/management/mcp-server-management/load-all-mcp-servers) Load all MCP Servers for a project, using a valid management key. # Load MCP Server (/api/management/mcp-server-management/load-mcp-server) Load an MCP Server by ID, using a valid management key. # Update MCP Server (/api/management/mcp-server-management/update-mcp-server) Update an MCP Server, using a valid management key. # Get OTP settings (/api/management/otp-management/get-otp-settings) Get the project's one-time passcode settings, using a valid management key. # Get OTP Settings for a tenant (/api/management/otp-management/get-tenant-otp-settings) Get OTP Settings for a tenant, using a valid management key. # Set OTP settings (/api/management/otp-management/set-otp-settings) Set the project's one-time passcode settings, using a valid management key. # Set OTP Settings for a tenant (/api/management/otp-management/set-tenant-otp-settings) Set OTP Settings for a tenant, using a valid management key. # Get passkey settings (/api/management/passkey-management/get-passkey-settings) Get the project's passkey (WebAuthn) settings, using a valid management key. # Set passkey settings (/api/management/passkey-management/set-passkey-settings) Set the project's passkey (WebAuthn) settings, using a valid management key. # Create outbound SCIM configuration (/api/management/outbound-scim-management/create-outbound-scim-configuration) Create a new outbound SCIM configuration, using a valid management key. Configures a SCIM connector that provisions users from a federated application to an external SCIM 2.0 service provider. # Delete outbound SCIM configuration (/api/management/outbound-scim-management/delete-outbound-scim-configuration) Delete an outbound SCIM configuration, using a valid management key. # Load outbound SCIM configuration (/api/management/outbound-scim-management/load-outbound-scim-configuration) Load an outbound SCIM configuration by id. Response merges connector config with webhook status (enabled flag, last export/processing times, failure counter). # Enable or disable an outbound SCIM configuration (/api/management/outbound-scim-management/set-outbound-scim-enabled) Enable or disable a specific outbound SCIM configuration. Separate from Update because enable/disable is a high-frequency toggle. # Update outbound SCIM configuration (/api/management/outbound-scim-management/update-outbound-scim-configuration) Update an existing outbound SCIM configuration, using a valid management key. # Get password settings (/api/management/password-management/get-password-settings-v-2) Get the project's password settings, using a valid management key. # Set password settings (/api/management/password-management/set-password-settings-v-2) Set the project's password settings, using a valid management key. # Batch Upload Outbound App Tenant OAuth Tokens (/api/management/outbound-apps/batch-upload-outbound-app-tenant-oauth-tokens) ### Batch upload tenant OAuth tokens for an outbound application Import pre-existing tenant-scoped OAuth tokens in a single request. This operation is all-or-nothing: if any token fails validation, the entire batch is rejected and no tokens are committed. Fix the reported failures and retry the full batch. Requires a management key. # Batch Upload Outbound App User OAuth Tokens (/api/management/outbound-apps/batch-upload-outbound-app-user-oauth-tokens) ### Batch upload user OAuth tokens for an outbound application Import pre-existing user-scoped OAuth tokens in a single request. This operation is all-or-nothing: if any token fails validation, the entire batch is rejected and no tokens are committed. Fix the reported failures and retry the full batch. Requires a management key. # Connect to outbound application (/api/management/outbound-apps/connect-outbound-app) Connect to outbound application, using a valid JWT. # Create outbound application according to existing dcr preset (/api/management/outbound-apps/create-outbound-app-by-dcr-preset) Create a new outbound application according to existing dcr preset, using a valid management key. # Create outbound application by existing template (/api/management/outbound-apps/create-outbound-app-by-template) Create a new outbound application by existing template using a valid management key. # Create Outbound App (/api/management/outbound-apps/create-outbound-app) ### Create outbound application This endpoint allows you to create a new outbound application. # Delete outbound application token by id (/api/management/outbound-apps/delete-outbound-app-token-by-id) Delete outbound application token by id, using a valid management key. # Delete outbound application tokens by appId or userId (/api/management/outbound-apps/delete-outbound-app-user-tokens) Delete outbound application tokens by appId or userId, using a valid management key. # Delete Outbound App (/api/management/outbound-apps/delete-outbound-app) ### Delete outbound application This endpoint allows you to delete an outbound application. # Fetch Latest Outbound App Tenant Token (/api/management/outbound-apps/fetch-latest-outbound-app-tenant-token) ### Fetch latest outbound application tenant token This endpoint allows you to fetch the latest tenant token for an outbound application. # Fetch Latest Outbound App User Token (/api/management/outbound-apps/fetch-latest-outbound-app-user-token) ### Fetch latest outbound application user token This endpoint allows you to fetch the latest user token for an outbound application. # Fetch Outbound App Tenant Token (/api/management/outbound-apps/fetch-outbound-app-tenant-token) ### Fetch outbound application tenant token This endpoint allows you to fetch the tenant token for an outbound application. # Fetch Outbound App User Token (/api/management/outbound-apps/fetch-outbound-app-user-token) ### Fetch outbound application user token This endpoint allows you to fetch the user token for an outbound application. # Get Outbound App by ID (/api/management/outbound-apps/get-outbound-app-by-id) ### Get outbound application by ID This endpoint allows you to retrieve a specific outbound application by its ID. # Outbound Apps (/api/management/outbound-apps) Manage outbound applications that allow your users to authenticate with external services. # Outbound Apps Outbound Apps allow you to configure external OAuth/OIDC providers that your application can use to authenticate users with third-party services. This section covers the Management API endpoints for managing outbound applications, including creating, updating, deleting, and retrieving tokens for outbound apps. # List All Outbound Apps (/api/management/outbound-apps/list-all-outbound-apps) ### List all outbound applications This endpoint allows you to retrieve all outbound applications configured in your project. # List Outbound Apps with User Token (/api/management/outbound-apps/list-outbound-apps-with-user-token) ### List outbound applications with user token This endpoint allows you to retrieve outbound applications that have a user token available. # Update Outbound App (/api/management/outbound-apps/update-outbound-app) ### Update outbound application This endpoint allows you to update an existing outbound application. # Upload tenant API key for outbound app (/api/management/outbound-apps/upload-outbound-app-tenant-api-key) Upload/set a static API key for a tenant on an apikey-type outbound application, using a valid management key. An optional externalIdentifier lets you store multiple keys for the same tenant and application, each addressable by that identifier when fetching the token. Uploading again with the same externalIdentifier (or with none) replaces the existing key. # Upload Outbound App Tenant OAuth Token (/api/management/outbound-apps/upload-outbound-app-tenant-oauth-token) ### Upload a tenant OAuth token for an outbound application Import a pre-existing tenant-scoped OAuth token into an outbound application without requiring the user to re-run the OAuth flow. Requires a management key. # Upload user API key for outbound app (/api/management/outbound-apps/upload-outbound-app-user-api-key) Upload/set a static API key for a user on an apikey-type outbound application, using a valid management key. An optional externalIdentifier lets you store multiple keys for the same user and application, each addressable by that identifier when fetching the token. Uploading again with the same externalIdentifier (or with none) replaces the existing key. # Upload Outbound App User OAuth Token (/api/management/outbound-apps/upload-outbound-app-user-oauth-token) ### Upload a user OAuth token for an outbound application Import a pre-existing user-scoped OAuth token into an outbound application without requiring the user to re-run the OAuth flow. Requires a management key. # Create Permission (/api/management/permissions/create-permission) ### Create a permission, using a valid management key. This API endpoint allows administrators to create a new permission. The endpoint takes the following two parameters: - name (required) - description (optional) ### Next Steps Once you have this data, you can utilize the newly created role to [Create Roles](/api/management/roles/create-role) or [Update Roles](/api/management/roles/update-role) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Bulk Create Permissions (/api/management/permissions/create-permissions) Bulk create Permissions, using a valid management key. # Delete Permission (/api/management/permissions/delete-permission) ### Delete a permission, using a valid management key. This API endpoint allows administrators to delete an existing permission. The endpoint takes the following one parameter: - name (required) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Bulk Delete Permissions (/api/management/permissions/delete-permissions) Bulk delete Permissions, using a valid management key. # Permissions Management API Overview (/api/management/permissions) Use the Descope API to create, update, and delete permissions. # Permission Management ## Overview The Permissions Management APIs let you programmatically manage permissions using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Load All Permissions](/api/management/permissions/load-all-permissions) 2. [Create Permission](/api/management/permissions/create-permission) 3. [Update Permission](/api/management/permissions/update-permission) 4. [Delete Permission](/api/management/permissions/delete-permission) ## Examples ### Example - create a permission and apply it to a role 1. Call the [Create Permission](/api/management/permissions/create-permission) API endpoint to create a new permission. 2. Use the [Create Role](/api/management/roles/create-role) or [Update Role](/api/management/roles/update-role) API endpoints to apply the permission to a role. # Load All Permission (/api/management/permissions/load-all-permissions) ### Load all permissions, using a valid management key. This API endpoint returns details all permissions configured within the Descope instance. The response includes an array of permissions and these details of each permission: - name - description - systemDefault ### Next Steps Once you have this data, you can utilize the response to [Create Roles](/api/management/roles/create-role) or [Update Roles](/api/management/roles/update-role) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Update Permission (/api/management/permissions/update-permission) ### Update a permission, using a valid management key. This API endpoint allows administrators to update an existing permission. The endpoint takes the following two parameters: - name (required) - description (optional - though if not provided, it will be removed from the permission) ### Next Steps Once you have this data, you can utilize the newly created role to [Create Roles](/api/management/roles/create-role) or [Update Roles](/api/management/roles/update-role) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Bulk Update Permissions (/api/management/permissions/update-permissions) Bulk update Permissions, using a valid management key. # Create policy rule (/api/management/policy-rules-management/create-policy-rule) Create a new policy rule row using a valid management key. The id is server-generated. # Delete policy rule (/api/management/policy-rules-management/delete-policy-rule) Delete a policy rule row matched by id. Idempotent. # Load policy rule settings (/api/management/policy-rules-management/load-policy-rule-settings) Load the project-level policy rule settings (default action). # Load policy rule (/api/management/policy-rules-management/load-policy-rule) Load a single policy rule row by id. # Reorder a policy rule (/api/management/policy-rules-management/reorder-policy-rule) Move one policy rule to a 1-based position in the evaluation order; rules in between shift accordingly. Rules are evaluated first-match in this order. # Search policy rules (/api/management/policy-rules-management/search-policy-rules) Search policy rule rows by filters (text, action_kind, effect, principal_type, resource_type, enabled). Paged and sorted. # Update policy rule settings (/api/management/policy-rules-management/update-policy-rule-settings) Update the project-level policy rule settings. defaultAction is allow or block; the choice is one-way and cannot be reset to the unconfigured state. # Update policy rule (/api/management/policy-rules-management/update-policy-rule) Replace an existing policy rule row matched by id. # Create Project (/api/management/project-management/create-project) Create a new project in the management key's company, using a valid company-level management key. # Get admin portal settings (/api/management/project-management/get-admin-portal-settings) Get the project's admin portal settings, using a valid management key. # Get project settings (/api/management/project-management/get-project-settings) Get the project's settings, using a valid management key. # Load Project (/api/management/project-management/load-project) Load the project's name, environment and tags, using a valid management key. # Set admin portal settings (/api/management/project-management/set-admin-portal-settings) Update the project's admin portal settings, using a valid management key. # Set project settings (/api/management/project-management/set-project-settings) Update the project's settings, using a valid management key. # Update Project (/api/management/project-management/update-project) Update the project's name, environment or tags, leaving any fields that aren't set in the request unchanged, using a valid management key. # Clone Project (Async) (/api/management/projects/clone-project-async) Clone a project, including its settings and configurations. Users, tenants and access keys are not cloned. This API is asynchronous and will return a unique ID that can be used to track the progress of the clone operation. # Clone Project (/api/management/projects/clone-project) ### Clone a project utilizing a management key. This endpoint allows you to clone the current project, including its settings and configurations. _Note: This requires a pro or enterprise tier licenses. Users, tenants and access keys are not cloned._ ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Delete Project (/api/management/projects/delete-project) ### Delete a project utilizing a management key. This endpoint allows you to delete a project. This action is irreversible, use with caution. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Export Messaging Localization (/api/management/projects/export-messaging-template-localization) Export messaging localization, using a valid management key. # Export Project (/api/management/projects/export-project) ### Export a project utilizing a management key. This endpoint is used to export a project. The response is the JSON of the project items. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Export Project Snapshot (/api/management/projects/export-snapshot) ### Export a project snapshot utilizing a management key. This endpoint allows you to export a snapshot of the current project state. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Get Clone Project Process (/api/management/projects/get-clone-project-process) Get the status of an asynchronous clone project process. This returns an object describing the new project details or an error if the process failed, using a valid management key. # Import Messaging Localization (/api/management/projects/import-messaging-localization) Import messaging localization, using a valid management key. # Import Project (/api/management/projects/import-project) ### Import a project utilizing a management key. This endpoint is used to import a project. The argument of `files` should be the output of the [export project endpoint](/api/management/projects/export-project) You can also exclude items from the export when importing by utilizing the flags below within the `exclude` array. ``` The entire project: project Project specific items: project.domain project.trustedDomains project.tokenResponseMethod project.selfProvisioning project.rotateJwt project.cookiepolicy project.refreshTokenExpiration project.stepupTokenExpiration project.sessionTokenExpiration project.keySessionTokenExpiration project.inviteUrl project.inviteEmail project.inviteSms project.inviteMagicLink project.conformanceJwt project.inactivity Auth Methods, Flows, styles, etc: magicLink enchantedLink embeddedLink otp totp sso oauth webauthn password styles flows connectors authorization attributes ssoApps ``` You can also import secrets for connectors and OAuth Providers using the `inputSecrets` argument. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Import Project Snapshot (/api/management/projects/import-snapshot) ### Import a project snapshot utilizing a management key. This endpoint allows you to import a previously exported project snapshot. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Project Management API Overview (/api/management/projects) Use the Descope API to manage your projects with a management key. # Project Management ## Overview The Project Management APIs let you programmatically manage your Descope projects using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Rename a Project](/api/management/projects/rename-project) 2. [Export a Project](/api/management/projects/export-project) 3. [Import a Project](/api/management/projects/import-project) 4. [Clone a Project](/api/management/projects/clone-project) 5. [Delete a Project](/api/management/projects/delete-project) ## Examples ### Example - export and import projects 1. Call the [Export Project](/api/management/projects/export-project) API endpoint to export a project. 2. Pass the output to the [Import Project](/api/management/projects/import-project) API endpoint. If you are importing to a different project, update the Project ID in the bearer token for the import request. # List Projects (/api/management/projects/list-projects) ### List all projects utilizing a management key. This endpoint allows you to list all projects in your account. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Rename Project (/api/management/projects/rename-project) ### Rename a project utilizing a management key. This endpoint allows you to update the name of a project. The body only requires the `name` argument. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Update Project Tags (/api/management/projects/update-tags) ### Update project tags utilizing a management key. This endpoint allows you to update the tags associated with a project. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Validate Project Snapshot (/api/management/projects/validate-snapshot) ### Validate a project snapshot utilizing a management key. This endpoint allows you to validate a project snapshot before importing it. ### See Also - See [Managing Environments](/customize/environments/) for details about managing environments. # Create resource (/api/management/resources-management/create-resource) Create a new resource, using a valid management key. # Delete resource (/api/management/resources-management/delete-resource) Delete a resource, using a valid management key. # Delete resources batch (/api/management/resources-management/delete-resources) Delete multiple resources, using a valid management key. # Load all resources (/api/management/resources-management/load-all-resources) Load all resources, using a valid management key. # Load resource by URI (/api/management/resources-management/load-resource-by-uri) Load a resource by URI, using a valid management key. # Load resource by ID (/api/management/resources-management/load-resource) Load a resource by ID, using a valid management key. # Update resource (/api/management/resources-management/update-resource) Update a resource, using a valid management key. # Create Role (/api/management/roles/create-role) ### Create a role, using a valid management key. This API endpoint allows administrators to create a new role. The endpoint takes the following three parameters: - name (required) - description (optional) - permissionNames (optional) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Bulk Create Roles (/api/management/roles/create-roles) Bulk create Roles, using a valid management key. # Delete Role (/api/management/roles/delete-role) ### Delete a role, using a valid management key. This API endpoint allows administrators to delete an existing role. The endpoint takes the following one parameter: - name (required) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions._override/App.tsx # Batch Delete Roles (/api/management/roles/delete-roles) ### Delete roles in batch, using a valid management key. This API endpoint allows administrators to delete roles in batch. The endpoint takes the following one parameter: - roleNames (required) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Role Management API Overview (/api/management/roles) Use the Descope API to create, manage, and delete roles using a management key. # Role Management ## Overview The Role Management APIs let you programmatically manage roles using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Load All Roles](/api/management/roles/load-all-roles) 2. [Search Roles](/api/management/roles/search-roles) 3. [Create Role](/api/management/roles/create-role) 4. [Update Role](/api/management/roles/update-role) 5. [Delete Role](/api/management/roles/delete-role) 6. [Batch Delete Roles](/api/management/roles/delete-roles) ## Examples ### Example - create a role and apply it to SSO mapping 1. Create a new role using the [Create Role](/api/management/roles/create-role) API endpoint. ### Example - create a role and apply it to a user 1. Create a new role using the [Create Role](/api/management/roles/create-role) API endpoint. 2. Apply the role to a user with [Create User](/api/management/users/create-user), [Update User](/api/management/users/update-user), or [Update User Add Roles](/api/management/users/update-user-add-roles). # Load All Roles (/api/management/roles/load-all-roles) ### Load all roles, using a valid management key. This API endpoint allows administrators to load all existing roles. This endpoint returns an array of roles including their name, description, and permissionsNames. ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Search Roles (/api/management/roles/search-roles) ### Search roles, using a valid management key. This API endpoint allows administrators to search against existing roles. This endpoint returns an array of roles including their name, description, and permissionsNames that match the search parameters. ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Update Role (/api/management/roles/update-role) ### Update an existing role, using a valid management key. This API endpoint allows administrators to update an existing role. The endpoint takes the following four parameters: - name (required) - newName (required) - description (optional - though if not provided, it will be removed from the role)) - permissionNames (optional - though if not provided, it will be removed from the role)) ### See also - See [User Authorization](/manage/roles/) for further details on managing roles and permissions. # Bulk Update Roles (/api/management/roles/update-roles) Bulk update Roles, using a valid management key. # Delete Scope Claim Mapping (/api/management/scope-claim-mapping/delete-scope-claim-mapping) Remove the project-wide scope-to-claims mapping. # Get Scope Claim Mapping (/api/management/scope-claim-mapping/get-scope-claim-mapping) Get the project-wide mapping of OIDC scopes to JWT claims. Returns an empty mapping when none has been configured. # Set Scope Claim Mapping (/api/management/scope-claim-mapping/set-scope-claim-mapping) Replace the project-wide mapping of OIDC scopes to JWT claims. Each value may be a static string or a {{...}} template resolved at token-generation time. # Add a named SSO application secret (/api/management/sso-apps/add-sso-application-secret) Add a new named OIDC client secret to an SSO application, using a valid management key. Returns the new secret metadata and its cleartext value (shown only once). # Create SSO application custom attribute definitions (/api/management/sso-apps/create-sso-application-custom-attribute) Create or update SSO application custom attribute definitions, using a valid management key. # Create SSO application permission (/api/management/sso-apps/create-sso-application-permission) Create a permission scoped to an SSO application, using a valid management key. # Create SSO application role (/api/management/sso-apps/create-sso-application-role) Create a role scoped to an SSO application, using a valid management key. Role permissionIds must reference permissions of the same application. # Create OIDC Application (/api/management/sso-apps/create-sso-oidc-application) ### Create OIDC Application within a project This endpoint creates an OIDC Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Create SAML Application (/api/management/sso-apps/create-ssosaml-application) ### Create SAML Application within a project This endpoint creates a SAML Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Create SSO WS-Fed IDP application (/api/management/sso-apps/create-ssows-fed-application) Create a new SSO WS-Fed IDP application, using a valid management key. # Delete SSO application custom attribute definitions (/api/management/sso-apps/delete-sso-application-custom-attribute) Delete SSO application custom attribute definitions, using a valid management key. # Delete SSO application permission (/api/management/sso-apps/delete-sso-application-permission) Delete an SSO application permission by application id and permission id, using a valid management key. The permission is detached from any roles that reference it. # Delete SSO application role (/api/management/sso-apps/delete-sso-application-role) Delete an SSO application role by application id and role id, using a valid management key. Any user or service account assignments of the role are removed. # Delete Application (/api/management/sso-apps/delete-sso-application) ### Delete an Application within a project This endpoint deletes an Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Get SSO application permission (/api/management/sso-apps/get-sso-application-permission) Get a single SSO application permission by application id and permission id, using a valid management key. # Get SSO application role (/api/management/sso-apps/get-sso-application-role) Get a single SSO application role by application id and role id, using a valid management key. # Get SSO application secret by application ID (/api/management/sso-apps/get-sso-application-secret) Get the dedicated OIDC client secret of an SSO application by its ID, using a valid management key. # Application Management API Overview (/api/management/sso-apps) Use the Descope REST API to manage Applications within Descope # Application Management ## Overview The Application Management APIs let you programmatically manage the applications within your project using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Load All Applications](/api/management/sso-apps/load-all-applications) 2. [Load Application by ID](/api/management/sso-apps/load-sso-application-by-id) 3. [Create OIDC Application](/api/management/sso-apps/create-sso-oidc-application) 4. [Update OIDC Application](/api/management/sso-apps/update-sso-oidc-application) 5. [Create SAML Application](/api/management/sso-apps/create-sso-saml-application) 6. [Update SAML Application](/api/management/sso-apps/update-sso-saml-application) 7. [Delete Application](/api/management/sso-apps/delete-sso-application) ## See Also Review our [documentation](/manage/idpapplications/) on Applications within Descope. # Load All Applications (/api/management/sso-apps/load-all-sso-applications) ### Load all Applications within a project This endpoint returns details of all Applications within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Load Application by ID (/api/management/sso-apps/load-sso-application-by-id) ### Load Application by ID within a project This endpoint returns details of a specific Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Revoke a named SSO application secret (/api/management/sso-apps/revoke-sso-application-secret) Revoke a named OIDC client secret of an SSO application, using a valid management key. Returns the remaining secret metadata. # Rotate SSO application secret by application ID (/api/management/sso-apps/rotate-sso-application-secret) Rotate the dedicated OIDC client secret of an SSO application by its ID, using a valid management key. # Get SSO application custom attribute definitions (/api/management/sso-apps/sso-application-custom-attributes) Get SSO application custom attribute definitions, using a valid management key. # Update SSO application permission (/api/management/sso-apps/update-sso-application-permission) Update an SSO application permission by application id and permission id, using a valid management key. # Update SSO application role (/api/management/sso-apps/update-sso-application-role) Update an SSO application role by application id and role id, using a valid management key. The full role is replaced with the given values, including permissionIds and roleMappings. # Update OIDC Application (/api/management/sso-apps/update-sso-oidc-application) ### Update OIDC Application within a project This endpoint updates an OIDC Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Update SAML Application (/api/management/sso-apps/update-ssosaml-application) ### Update SAML Application within a project This endpoint updates a SAML Application within your Descope project. ### See Also - Review our [documentation](/manage/idpapplications/) around Applications within Descope. # Update SSO WS-Fed IDP application (/api/management/sso-apps/update-ssows-fed-application) Update a SSO WS-Fed IDP application, using a valid management key. # Create template (/api/management/template-management/create-messaging-template) Create a message template for an auth method and delivery channel, using a valid management key. The created template is returned with its server-generated id. # Delete template (/api/management/template-management/delete-messaging-template) Delete a single message template by type, method and id, using a valid management key. Deleting the active template resets the auth method's selection to the built-in System template. # Get template (/api/management/template-management/get-messaging-template) Get a single message template by type, method and id, using a valid management key. # Update template (/api/management/template-management/update-messaging-template) Update a single message template by type, method and id, using a valid management key. # Get TOTP settings (/api/management/totp-management/get-totp-settings) Get the project's TOTP settings, using a valid management key. # Set TOTP settings (/api/management/totp-management/set-totp-settings) Set the project's TOTP settings, using a valid management key. # Add a named third party application secret (/api/management/third-party-apps/add-third-party-application-secret) Add a new named client secret to a third party application, using a valid management key. Returns the new secret metadata and its cleartext value (shown only once). # Batch delete third party applications (/api/management/third-party-apps/batch-delete-third-party-applications) Delete multiple third party applications in batch, using a valid management key. # Create third party application (v2) (/api/management/third-party-apps/create-third-party-application-v-2) Create a new third party application using the structured scope-claim mapping model, using a valid management key. The legacy attributesScopes field is ignored. # Create third party application (/api/management/third-party-apps/create-third-party-application) Create a new third party application, using a valid management key. # Delete third party application consents (/api/management/third-party-apps/delete-third-party-application-consents) Delete OAuth user consents for [Inbound Apps](/identity-federation/inbound-apps). Provide `consentIds`, or filter by `appId`, `userIds`, and optional `tenantId`. To revoke [agentic identities](/agentic-identity-hub/core-components/agents), use [Revoke agentic identities](/api/management/agentic-identity-hub/revoke-agentic-identities) instead — it scopes deletion to agentic consents and supports `clientId` and `resourceId` filters. # Delete third party application consents by tenant (/api/management/third-party-apps/delete-third-party-application-tenant-consents) Delete all OAuth user consents for an [Inbound App](/identity-federation/inbound-apps) within a tenant. Provide `tenantId` and optional `appId` or `consentIds`. # Delete third party application (/api/management/third-party-apps/delete-third-party-application) Delete a third party application, using a valid management key. # Get third party application secret (/api/management/third-party-apps/get-third-party-application-secret) Get a third party application secret, using a valid management key. # Load all third party applications (v2) (/api/management/third-party-apps/load-all-third-party-applications-v-2) Loads all third party applications in the structured scope-claim mapping model, using a valid management key. Legacy attributesScopes are converted to scopeClaimMapping and returned empty. # Load All third party applications (/api/management/third-party-apps/load-all-third-party-applications) Loads all project third party applications, using a valid management key. # Load third party application by ID (v2) (/api/management/third-party-apps/load-third-party-application-v-2) Loads a third party application by id in the structured scope-claim mapping model, using a valid management key. Legacy attributesScopes are converted to scopeClaimMapping and returned empty. # Load third party application by ID (/api/management/third-party-apps/load-third-party-application) Loads project third party application by id, using a valid management key. # Patch third party application (v2) (/api/management/third-party-apps/patch-third-party-application-v-2) Patch a third party application using the structured scope-claim mapping model, using a valid management key. Only the fields present in the request change; the legacy attributesScopes field is cleared. # Patch third party application (/api/management/third-party-apps/patch-third-party-application) Patch a third party application, using a valid management key. # Revoke a named third party application secret (/api/management/third-party-apps/revoke-third-party-application-secret) Revoke a named client secret of a third party application, using a valid management key. Returns the remaining secret metadata. # Rotate third party application secret by application ID (/api/management/third-party-apps/rotate-third-party-application-secret) Rotate the project third party application secret by the application id, using a valid management key. # Search third party application consents (/api/management/third-party-apps/search-third-party-application-consents) Search OAuth user consents for [Inbound Apps](/identity-federation/inbound-apps). Filter by `appId`, `userId`, `consentId`, or `tenantId`. For [agentic identities](/agentic-identity-hub/core-components/agents) (MCP server authorizations), use [Search agentic identities](/api/management/agentic-identity-hub/search-agentic-identities) instead — it returns agent name, resource ID, and client ID alongside each consent. # Update third party application (v2) (/api/management/third-party-apps/update-third-party-application-v-2) Update a third party application using the structured scope-claim mapping model, using a valid management key. The legacy attributesScopes field is ignored and cleared. # Update third party application (/api/management/third-party-apps/update-third-party-application) Update a third party application, using a valid management key. # Create Tenant (/api/management/tenants/create-tenant) ### Create a new tenant, using a valid management key. This API endpoint will create a new tenant utilizing a valid management key. Creation of a new tenant can set the name, id, and selfProvisioningDomains. The id and selfProvisioningDomains are not mandatory. The id will be autogenerated if not provided. The response will always include the tenantId. ### Next Steps - You can then add users to the tenant via [Update User](/api/management/users/update-user) or [Create User](/api/management/users/create-user) - You can also apply sso configurations to the tenant via the [SSO Management API](/api/ssomanagement/) ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Create Tenants Batch (/api/management/tenants/create-tenants-batch) Create multiple tenants in a single batch, using a valid management key. Each tenant is created independently; per-tenant failures are returned in the response body. # Delete tenant attribute (/api/management/tenants/delete-tenant-attribute) Delete a single tenant custom attribute definition by machine name (no-op if absent), using a valid management key. # Delete Tenant (/api/management/tenants/delete-tenant) ### Delete a tenant, using a valid management key. This API endpoint will delete a tenant utilizing a valid management key based on the provided user tenandId. ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Delete Tenants Batch (/api/management/tenants/delete-tenants-batch) Delete multiple tenants in a single batch, using a valid management key. Each tenant is deleted independently; per-tenant failures are returned in the response body. # Get SSO Admin Link for Authenticated Users (/api/management/tenants/get-tenant-admin-link-sso-for-authenticated-users) Get SSO admin link, that will work for authenticated users only, using a valid management key # Generate tenant admin SSO configuration links (batch) (/api/management/tenants/get-tenant-admin-links-sso-for-authenticated-users) Get default SSO admin link plus per-ssoId admin links for authenticated users only, using a valid management key # Get tenant attribute (/api/management/tenants/get-tenant-attribute) Get a single tenant custom attribute definition by machine name, using a valid management key. # Tenant Management API Overview (/api/management/tenants) Use the Descope API to manage your tenants with a management key. # Tenant Management API Overview ## Overview The Tenant Management APIs let you programmatically manage tenants using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Load All Tenants](/api/management/tenants/load-all-tenants) 2. [Load Tenant By ID](/api/management/tenants/load-tenant-by-id) 3. [Search Tenants](/api/management/tenants/search-tenants) 4. [Create Tenant](/api/management/tenants/create-tenant) 5. [Update Tenant](/api/management/tenants/update-tenant) 6. [Delete Tenant](/api/management/tenants/delete-tenant) ## Examples ### Example - create a tenant 1. Call the [Create Tenant](/api/management/tenants/create-tenant) API endpoint to create the tenant with the desired configuration. 2. Add users to the tenant with [Update User Add Tenant](/api/management/users/update-user-add-tenant), [Update User](/api/management/users/update-user), or [Create User](/api/management/users/create-user). 3. Optionally, configure SSO for the tenant via the [SSO Management API](/api/management/tenants/sso). ### Example - update a tenant's settings Use the [Update Tenant](/api/management/tenants/update-tenant) API endpoint to update the tenant's name or `selfProvisioningDomains`. # Load All Tenants (/api/management/tenants/load-all-tenants) ### Load all tenants, using a valid management key. This API endpoint returns details of all configured tenants within the Descope instance. The response includes an array of the tenants and these details for each tenant: - id - name - selfProvisioningDomains ### Next Steps - Once you have this data, you can utilize the response to add users to the tenant via [Update User](/api/management/users/update-user) or [Create User](/api/management/users/create-user) - You can also apply sso configurations to the tenant via the [SSO Management API](/api/ssomanagement/) ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Load Tenant By ID (/api/management/tenants/load-tenant-by-id) ### Load tenant by ID, using a valid management key. This API endpoint returns details of the tenant within the Descope instance that matches the ID provided. The response includes an array of the tenants and these details for each tenant: - id - name - selfProvisioningDomains ### Next Steps - Once you have this data, you can utilize the response to add users to the tenant via [Update User](/api/management/users/update-user) or [Create User](/api/management/users/create-user) - You can also apply sso configurations to the tenant via the [SSO Management API](/api/ssomanagement/) ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Patch Tenant (/api/management/tenants/patch-tenant) Patch a tenant, using a valid management key. Only the fields that are provided in the request are updated, any other field keeps its current value. # Remove SSO User from Tenant (/api/management/tenants/remove-sso-user) ### Remove an SSO user from a tenant, using a valid management key. This API endpoint removes an SSO user's association with a tenant. ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Search Tenants (/api/management/tenants/search-tenants) ### Search all tenants, using a valid management key. This API endpoint returns details of configured tenants within the Descope instance that match the search parameters. The response includes an array of the tenants and these details for each tenant: - id - name - selfProvisioningDomains ### Next Steps - Once you have this data, you can utilize the response to add users to the tenant via [Update User](/api/management/users/update-user) or [Create User](/api/management/users/create-user) - You can also apply sso configurations to the tenant via the [SSO Management API](/api/ssomanagement/) ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Set tenant attribute (/api/management/tenants/set-tenant-attribute) Create or update a single tenant custom attribute definition (upsert by machine name), using a valid management key. Tenant attributes do not support edit permissions. # Update Tenant Default Roles (/api/management/tenants/update-tenant-default-roles) Update tenant default roles, using a valid management key. # Update Tenant (/api/management/tenants/update-tenant) ### Update a tenant, using a valid management key. This API endpoint will update a tenant utilizing a valid management key. Utilizing this API endpoint will allow you to update the name or selfProvisioningDomains settings of the tenant. ### Next Steps - You can then add users to the tenant via [Update User](/api/management/users/update-user) or [Create User](/api/management/users/create-user) - You can also apply sso configurations to the tenant via the [SSO Management API](/api/ssomanagement/) ### See also - See [Tenant Management](/management/tenant-management) for further details on managing tenants. # Update Tenants Batch (/api/management/tenants/update-tenants-batch) Update multiple tenants in a single batch, using a valid management key. Each tenant is updated independently; per-tenant failures are returned in the response body. # Anonymous User (/api/management/users/anonymous) ### Anonymous User Anonymous Users are identified with a unique Descope JWT type. Eventually, create a token that we can use as the defined anonymous identity. Signed by Descope. For more info, please refer to our anonymous users documentation. # Batch Create Users (/api/management/users/batch-create-users) ### Batch Create Users, using a valid management key. This API endpoint will batch create new users utilizing a valid management key. This API endpoint allows you to configure all aspects of a user: - loginId - email - phone - verified settings (phone, email) - one must be set to true - displayName - roleNames - Tenant configurations - which tenantIds, which roleNames. The userTenants can include multiple items Ex: ``` "userTenants": [ { "tenantId": "T2IMjmRfYTQHlbaastz3im59ERS3", "roleNames": [ "Test" ] }, { "tenantId": "T2Igau6dX1R6SkomtFCdBLrc3r67", "roleNames": [ "Test" ] } ``` Additionally, you can create a user with multiple login IDs by passing an array of loginIds in string format within the `additionalIdentifiers` key. You can also decide whether to invite the users, configure the inviteUrl, and whether to send invites via email or SMS. When importing with hashed passwords, see [this guide](/migrate/custom#importing-passwords) for further detailed configuration of password hash formats. ### Next Steps Once the user is created, the user can then login utilizing any sign-in api supported. This will then switch the user from invited to active. ### See also - See [Manage User](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Batch Delete Users (/api/management/users/batch-delete-users) ### Delete users, using a valid management key. This API endpoint will delete users utilizing a valid management key. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Create Family Custom Attribute (/api/management/users/create-family-custom-attribute) Create one or more custom attribute definitions on the family entity, using a valid management key. # Create Family Dependent (/api/management/users/create-family-dependent) Create a dependent (shadow profile) user in a family # Create Family (/api/management/users/create-family) Create a family, using a valid management key. # Create Test User (/api/management/users/create-test-user) Create a test user, using a valid management key. # Create Family-Scoped User Custom Attribute (/api/management/users/create-user-family-scoped-custom-attribute) Create one or more family-scoped user custom attribute definitions, using a valid management key. Values for these attributes are held per family membership rather than on the user. # Create User (/api/management/users/create-user) ### Create a new user, using a valid management key. This API endpoint will create a new user utilizing a valid management key. This API endpoint allows you to configure all aspects of a user: - loginId - email - phone - verified settings (phone, email) - one must be set to true - displayName - roleNames - Tenant configurations - which tenantIds, which roleNames. The userTenants can include multiple items Ex: ``` "userTenants": [ { "tenantId": "T2IMjmRfYTQHlbaastz3im59ERS3", "roleNames": [ "Test" ] }, { "tenantId": "T2Igau6dX1R6SkomtFCdBLrc3r67", "roleNames": [ "Test" ] } ``` Additionally, you can create a user with multiple login IDs by passing an array of loginIds in string format within the `additionalIdentifiers` key. ### Next Steps Once the user is created, the user can then login utilizing any sign-in api supported. This will then switch the user from invited to active. ### See also - See [Manage User](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Delete Family Custom Attribute (/api/management/users/delete-family-custom-attribute) Delete one or more custom attribute definitions from the family entity by name, using a valid management key. # Delete Family Dependent (/api/management/users/delete-family-dependent) Delete a dependent (shadow profile) user from a family # Delete Family (/api/management/users/delete-family) Delete a family, using a valid management key. # Delete user attribute (/api/management/users/delete-user-attribute) Delete a single user custom attribute definition by machine name (no-op if absent), using a valid management key. # Delete Family-Scoped User Custom Attribute (/api/management/users/delete-user-family-scoped-custom-attribute) Delete one or more family-scoped user custom attribute definitions by name, using a valid management key. # Delete User's TOTP Seed (/api/management/users/delete-user-totp-seed) ### Delete an existing user's TOTP, using a valid management key. This API endpoint allows you to delete an existing user's TOTP seed. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Delete User (/api/management/users/delete-user) ### Delete a user, using a valid management key. This API endpoint will delete a user utilizing a valid management key based on the provided user loginId. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Expire User Passwsord (/api/management/users/expire-user-password) ### Expire an existing user's password, using a valid management key. This API endpoint allows you to expire an existing user's password. Upon next login, the user will need to follow the reset password flow. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. - See [Reset Password](/api/passwords/email/password-reset) for sending the password reset email. # Get Family Custom Attributes (/api/management/users/get-family-custom-attributes) Get the family entity's custom attribute definitions, using a valid management key. # Get Family Settings (/api/management/users/get-family-settings) Get the project's family account settings, using a valid management key. # Get user attribute (/api/management/users/get-user-attribute) Get a single user custom attribute definition by machine name, using a valid management key. # Get Family-Scoped User Custom Attributes (/api/management/users/get-user-family-scoped-custom-attributes) Get the project's family-scoped user custom attribute definitions, using a valid management key. # Get User Provider Token (/api/management/users/get-user-provider-token) ### Get an existing user's provider token, using a valid management key. This API endpoint will loads the user's access token generated by the OAuth/OIDC provider, using a valid management key. When querying for OAuth providers, this only applies when utilizing your own account with the provider and have selected `Manage tokens from provider` selected under the [social auth methods](https://app.descope.com/settings/authentication/social). ### Query Params - `loginId` - The loginId of the user you want to get the provider token for. - `provider` - The provider you want to get the token for. - `withRefreshToken (optional)` - set to true to also return the refresh token. - `forceRefresh (optional)` - set to true to force a refresh of the token. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Provider Options](/auth-methods/oauth#social-login-oauth-providers) for a the out of the box list of providers. # Impersonate Family Dependent (/api/management/users/impersonate-family-dependent) Impersonate a family dependent. The impersonator (by user ID) must be a member of the dependent's family and hold the family impersonate-dependents permission there. # Impersonate Stepup (/api/management/users/impersonate-stepup) Impersonate as a different user with step-up claim # Impersonate User (/api/management/users/impersonate) ### Impersonate a user, using a valid management key. This API endpoint will allow you to impersonate a user using a login ID. The impersonator user must have the impersonation permission in order for this request to work. The response would be a refresh JWT of the impersonated user # Import User Passkeys (/api/management/users/import-user-passkeys) Import passkey credentials for a user, using a valid management key. # User Management API Overview (/api/management/users) Use the Descope API to manage your application users with a management key. # User Management API Overview ## Overview The User Management APIs let you programmatically create, update, search, and delete users using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Endpoints These are the available User Management API endpoints: 1. [Load User](/api/management/users/load-user) 2. [Get User Provider Token](/api/management/users/get-user-provider-token) 3. [Search Users](/api/management/users/search-users) 4. [Get User's Login History](/api/management/users/users-auth-history) 5. [Create User](/api/management/users/create-user) 6. [Batch Create Users](/api/management/users/batch-create-users) 7. [Update User](/api/management/users/update-user) 8. [Update User Status](/api/management/users/update-user-status) 9. [Update User Email](/api/management/users/update-user-email) 10. [Update User Login ID](/api/management/users/update-user-login-id) 11. [Update User Phone](/api/management/users/update-user-phone) 12. [Update User Display Name](/api/management/users/update-user-display-name) 13. [Update User Picture](/api/management/users/update-user-picture) 14. [Update User Custom Attributes](/api/management/users/update-user-custom-attribute) 15. [Update JWT](/api/management/users/update-jwt) 16. [Expire User Password](/api/management/users/expire-user-password) 17. [Set Active Password for User](/api/management/users/set-user-active-password) 18. [Set Temporary Password for User](/api/management/users/set-user-temp-password) 19. [Update User Add Tenant](/api/management/users/update-user-add-tenant) 20. [Update User Remove Tenant](/api/management/users/update-user-remove-tenant) 21. [Update User Add Role](/api/management/users/update-user-add-roles) 22. [Set User's Roles](/api/management/users/update-user-set-roles) 23. [Update User Remove Role](/api/management/users/update-user-remove-roles) 24. [Add Application to User](/api/management/users/update-user-add-sso-apps) 25. [Set Applications for User](/api/management/users/update-user-set-sso-apps) 26. [Remove Application from User](/api/management/users/update-user-remove-sso-apps) 27. [Log User Out of All Sessions](/api/management/users/logout-all-user-devices) 28. [Delete User's Passkeys](/api/management/users/remove-user-passkeys) 29. [Delete User](/api/management/users/delete-user) 30. [Batch Delete Users](/api/management/users/batch-delete-users) ## Examples ### Loading a user Use the [Load User](/api/management/users/load-user) API endpoint to retrieve user information. Do not call Load User in a frequently invoked function such as authentication middleware. Instead, use [custom claims](/manage/customclaims/) to include the data you need directly in the session token. ### Creating a user 1. Call the [Create User](/api/management/users/create-user) API endpoint with the desired user configuration. 2. The user can then log in using any supported sign-in method, which changes their status from `invited` to `active`. ### Updating a user [Update User](/api/management/users/update-user) performs a full overwrite — any field not included in the request body will be removed from the user. For example, if a user has both an email and a phone number but the update only includes email, the phone number will be cleared. To modify individual fields without affecting other settings, use one of the specific update endpoints listed above (Update User Email, Update User Phone, etc.). When a user's details change (for example, a role is added), their JWT is automatically refreshed within their current session. # List Trusted Devices (/api/management/users/list-trusted-devices-for-users) List trusted devices for one or more users. # List User Passkeys (/api/management/users/list-user-passkeys) List all passkeys for a user, using a valid management key. # Load User (/api/management/users/load-user) ### Load a user's data, using a valid management key. This API endpoint takes the user's loginId and then returns details of a user utilizing a valid management key. The response includes the following; however, there are additional items in the response that you can see below by expanding the response 200 OK. - loginIds - userId - name - email - phone - verified settings (phone, email) - Tenant configurations - which tenantIds, which roleNames _Note: Suppose you frequently load a user for a specific user detail, such as their email address or a particular custom attribute. In that case, you can save execution time and additional API/SDK calls to load the user by adding the items to the custom claim. For details on adding items to the custom claims, see [this documentation](/security-best-practices/custom-claims#using-custom-claims-within-descope-flows)._ _Note: If you have access to all federated applications, the list will return as an empty array. Descope allows you to restrict which apps each user has access to, but by default gives access to all applications._ ### Next Steps Once you have this data, you can utilize the response to prepare the payload to perform an [Update](/api/management/users/update-user) on the user. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Load Users (/api/management/users/load-users) Load users by their IDs, using a valid management key. # Log user out of all sessions (/api/management/users/logout-all-user-devices) ### Log a user out of all sessions, using a valid management key. This API endpoint allows you to log a user out of all active sessions. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Roles](/manage/roles/) for further details on managing roles. # Logout Users from All Devices (/api/management/users/logout-all-users-devices) Log multiple users out from all of their active sessions, using a valid management key. # Patch Users Batch (/api/management/users/patch-user-batch) Patch users in batch, using a valid management key. # Patch User (/api/management/users/patch-user) ### Patch a user's details, using a valid management key. This API endpoint will patch a user's details of a user utilizing a valid management key. Additionally, you can patch a user with multiple login IDs by passing an array of loginIds in string format within the `additionalIdentifiers` key. This allows you to add additional login identifiers to an existing user without performing a full user update. When adding additional identifiers to a user who has an SSO login ID, the user may be able to authenticate outside of SSO whenever SSO is not enforced and non-SSO methods (such as magic link or password) are available. See [Risks in Merging SSO and Non-SSO Identities](/sso/merging-sso-identities-risk) before using this field on SSO users. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Remove User Passkey (/api/management/users/remove-user-passkey) Remove a specific passkey for a user by credential ID, using a valid management key. # Delete User's Passkeys (/api/management/users/remove-user-passkeys) ### Delete a user's Passkeys, using a valid management key. This API endpoint will delete all existing passkeys for the user ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Remove User Recovery Codes (/api/management/users/remove-user-recovery-codes) Remove (revoke) all recovery codes for a user, using a valid management key. # Search Families (/api/management/users/search-families) Search families, using a valid management key. # Search test Users (/api/management/users/search-test-users) Search test users, using a valid management key. # Search Users (/api/management/users/search-users) ### Search for users, using a valid management key. This API endpoint will search for users utilizing a valid management key. Searches can be defined with any combination of roles or tenants. You can also only send the request with an empty payload to return all users. The response will include the following details on all users within an array of objects: - loginIds - userId - name - email - phone - verified settings (phone, email) - Tenant configurations (tenantIds, roleNames) ### Next Steps You can then parse through the response in order to find any users which you may need to delete, update, etc. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Set Family Settings (/api/management/users/set-family-settings) Set the project's family account settings, using a valid management key. # Set Active Password for User (/api/management/users/set-user-active-password) ### Set an active password for an existing user, using a valid management key. This API endpoint allows you to set an active password for an existing user. This will allow the user to authenticate with this password without changing it. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Set user attribute (/api/management/users/set-user-attribute) Create or update a single user custom attribute definition (upsert by machine name), using a valid management key. # Set Temporary Password for User (/api/management/users/set-user-temp-password) ### Set a temporary password for an existing user, using a valid management key. This API endpoint allows you to set a temporary password for an existing user. This will require the user to change their password on next authentication. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Stop Family Impersonation (/api/management/users/stop-family-impersonation) Stop impersonating a family dependent and return to the acting admin's own session # Stop Impersonation (/api/management/users/stop-impersonation) Stop impersonation as a different user # Update Family (/api/management/users/update-family) Update a family's name, custom attributes, photo, or disabled state, using a valid management key. Omitted fields are left unchanged. # Update JWT (/api/management/users/update-jwt) ### Updates a JWT with custom claims, using a valid management key. This API endpoint will update a JWT with custom claims. This endpoint takes the JWT as well as the `customClaims` json. # Update User Add Families (/api/management/users/update-user-add-families) Add a user to one or more families, using a valid management key. Each family entry may also set the user's roles and family-scoped attributes for that family in the same call; omitting roleNames/familyScopedAttributes on a family the user already belongs to leaves them unchanged. # Update User Add Roles (/api/management/users/update-user-add-roles) ### Add roles to an existing user, using a valid management key. This API endpoint allows you to add roles to a user granularly without updating all user details. `roleNames` is an array of the role names in string format. The `tenantId` is optional; if provided, the user must be a member of that tenant The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Roles](/manage/roles/) for further details on managing roles. # Add Application to User (/api/management/users/update-user-add-sso-apps) ### Add Applications to an existing user, using a valid management key. This API endpoint allows you to add Applications to a user granularly without updating all user details. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Applications](/manage/idpapplications/) for further details on Applications. # Update User Add Tenant (/api/management/users/update-user-add-tenant) ### Add a tenant to an existing user, using a valid management key. This API endpoint allows you to add a user to a tenant granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Update User Custom Attribute (/api/management/users/update-user-custom-attribute) ### Update an existing user's custom attributes, using a valid management key. This API endpoint allows you to update a user's custom attributes granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Update User Display Name (/api/management/users/update-user-display-name) ### Updates an existing user's display name, using a valid management key. This API endpoint allows you to update the user's display name granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Update User Email (/api/management/users/update-user-email) ### Updates an existing user's email, using a valid management key. This API endpoint allows you to update the user's email granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Update User Impersonation Consent (/api/management/users/update-user-impersonation-consent) Update user impersonation consent, using a valid management key. This allows granting impersonation consent without requiring the user-facing consent flow. # Update User Login ID (/api/management/users/update-user-login-id) ### Updates an existing user's login ID, using a valid management key. This API endpoint allows you to update a user's Login ID. If you'd like to remove a login ID, provide an empty string for the new login ID. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Update User Phone (/api/management/users/update-user-phone) ### Updates an existing user's phone number, using a valid management key. This API endpoint allows you to update the user's phone number granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Update User Picture (/api/management/users/update-user-picture) ### Update an existing user's profile picture, using a valid management key. This API endpoint allows you to update a user's profile picture granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Update User Recovery Email (/api/management/users/update-user-recovery-email) Update user recovery email, using a valid management key. # Update User Recovery Phone (/api/management/users/update-user-recovery-phone) Update user recovery phone, using a valid management key. # Update User Remove Families (/api/management/users/update-user-remove-families) Remove a user from one or more families, using a valid management key. # Update User Remove Roles (/api/management/users/update-user-remove-roles) ### Remove roles from an existing user, using a valid management key. This API endpoint allows you to remove roles from a user granularly without updating all user details. `roleNames` is an array of the role names in string format. The `tenantId` is optional; if provided, the user must be a member of that tenant The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Roles](/manage/roles/) for further details on managing roles. # Remove Application to User (/api/management/users/update-user-remove-sso-apps) ### Remove Applications from an existing user, using a valid management key. This API endpoint allows you to remove Applications from a user granularly without updating all user details. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Applications](/manage/idpapplications/) for further details on Applications. # Update User Remove Tenant (/api/management/users/update-user-remove-tenant) ### Removes a tenant from an existing user, using a valid management key. This API endpoint allows you to remove a user from a tenant granularly without updating all user details. The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Tenants](/management/tenant-management) for further details on managing tenants. # Delete Trusted Devices (/api/management/users/update-user-remove-trusted-devices) Delete user trusted devices by IDs. # Set User's Roles (/api/management/users/update-user-set-roles) ### Set an existing user's roles, using a valid management key. This API endpoint allows you to set a user's roles. This will override the current roles associated to the user and will set all passed roles. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Manage Roles](/manage/roles/) for further details on managing roles. # Set Applications to User (/api/management/users/update-user-set-sso-apps) ### Set Applications for an existing user, using a valid management key. This API endpoint allows you to set the associated Applications for a user. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. - See [Applications](/manage/idpapplications/) for further details on Applications. # Update User Status (/api/management/users/update-user-status) ### Updates an existing user's status, using a valid management key. This API endpoint allows you to update the user's status granularly without updating all user details. Available statuses to utilize: - invited - enabled - disabled The response returns the user's details in json format. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Update User (/api/management/users/update-user) ### Updates a user's details, using a valid management key. This API endpoint will update a user's details of a user utilizing a valid management key. It is important to understand the update will take the configurations for the user provided and will overwrite all user settings. This means that if the user currently has email and phone, but the update only includes email, the phone and other non-provided configurations will be removed. This API endpoint will remove any details that are not provided. It is preferred to use other updates supported by the API, such as the following options: - [Update User Status](/api/management/users/update-user-status) - [Update User Email](/api/management/users/update-user-email) - [Update User Phone](/api/management/users/update-user-phone) - [Update User Display Name](/api/management/users/update-user-display-name) - [Update User Add Tenant](/api/management/users/update-user-add-tenant) - [Update User Remove Tenant](/api/management/users/update-user-remove-tenant) - [Update User Add Role](/api/management/users/update-user-add-roles) - [Update User Remove Role](/api/management/users/update-user-remove-roles) Additionally, you can update a user with multiple login IDs by passing an array of loginIds in string format within the `additionalIdentifiers` key. It is suggested to gather the current user configurations via [Load User](/api/management/users/load-user) in order to assist you in building the payload for this api endpoint. ### See also - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Users Authentication History V2 (/api/management/users/users-auth-history-v-2) Load users' authentication history by user IDs, using a valid management key. V2 endpoint with improved request body handling. # Sign-In with Auto Sign-up (/api/otp/email/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending an OTP code via email Initiate a process that implements both sign-in and sign-up using a single endpoint. Descope will generate and deliver the One-Time Password (OTP) to the end user via email. If the email address is already registered (the end user exists) the user will be signed in. If the email address is not registered (the end user is not yet registered) the user will be signed up. Sending multiple OTP codes (for example, when an end user tries to sign-up/sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/email/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/email/sign-up) endpoint if you want a sign-up flow that will fail if the end user is already registered. - Use the [Sign-In](/api/otp/email/sign-in) endpoint if you want a sign-in flow that will fail if the end user isn't yet registered. # Sign-In (/api/otp/email/sign-in) ### Sign-in existing end user by sending an OTP code via email Initiate a sign-in process by sending a One-Time Password (OTP) to an existing end user. Descope will generate and deliver the OTP code to the email address specified. Sending multiple OTP codes (for example, when an end user tries to sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the email address is not yet registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/email/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/email/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/otp/email/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/otp/email/sign-up) ### Sign-up new end user by sending an OTP code via email Initiate a sign-up process by sending a One-Time Password (OTP) to a new end user. Descope will generate and deliver the OTP code to the email address specified. Sending multiple OTP codes (for example, when an end user tries to sign-up a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the email address is already registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/email/verify-otp) endpoint to complete the user sign-up process. After successfully verifying OTP code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - Use the [Sign-In](/api/otp/email/sign-in) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/otp/email/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Email (/api/otp/email/update-email) ### Update Email Address of Existing User Update the email of an existing end user by sending an OTP code to the new email address. After successfully verifying the code the new email address will be used to deliver new OTP messages via email. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. This endpoint will return an empty response object when it completes successfully. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/email/verify-otp) endpoint to complete the update process. After successfully verifying the code the new email address will replace the original email address. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See the [Verify OTP Code](/api/otp/email/verify-otp) endpoint, which will return the Refresh Jwt needed. # Verify OTP Code (/api/otp/email/verify-otp) ### Verify the validity of an OTP code sent via email Verify that the OTP code entered by the end user matches the OTP code that was sent. The Verify OTP code endpoint completes the OTP via email flow for: - [Sign-Up](/api/otp/email/sign-up) - [Sign-In](/api/otp/email/sign-in) - [Sign-In with Auto Sign-up](/api/otp/email/sign-in-auto-sign-up) - [Update Email](/api/otp/email/update-email) The response object includes the session JWT `sessionJwt` and refresh JWT `refreshJwt` when the endpoint completes successfully, and the end user will be signed in. For an update email flow, the new email address will replace the original email address. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. # Sign-In with Auto Sign-up (/api/otp/phone/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending an OTP code via Voice Initiate a process that implements both sign-in and sign-up using a single endpoint. Descope will generate and deliver the One-Time Password (OTP) to the end user via Voice. If the phone number is already registered (the end user exists) the user will be signed in. If the phone number is not registered (the end user is not yet registered) the user will be signed up. Sending multiple OTP codes (for example, when an end user tries to sign-up/sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/phone/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/phone/sign-up) endpoint if you want a sign-up flow that will fail if the end user is already registered. - Use the [Sign-In](/api/otp/phone/sign-in-auto-sign-up) endpoint if you want a sign-in flow that will fail if the end user isn't yet registered. # Sign-In (/api/otp/phone/sign-in) ### Sign-in existing end user by sending an OTP code via Voice Initiate a sign-in process by sending a One-Time Password (OTP) to an existing end user. Descope will generate and deliver the OTP code to the phone number specified. Sending multiple OTP codes (for example, when an end user tries to sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the phone number is not yet registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/phone/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/phone/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/otp/phone/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/otp/phone/sign-up) ### Sign-up new end user by sending an OTP code via Voice Initiate a sign-up process by sending a One-Time Password (OTP) to a new end user. Descope will generate and deliver the OTP code via Voice to the phone number specified. Sending multiple OTP codes (for example, when an end user tries to sign-up a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the phone number is already registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/phone/verify-otp) endpoint to complete the user sign-up process. After successfully verifying the OTP code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - Use the [Sign-In](/api/otp/phone/sign-in) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/otp/phone/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Phone (/api/otp/phone/update-phone) ### Update phone number of Existing User Update the phone number of an existing end user by sending an OTP code to the new phone number. After successfully verifying the code the new phone number will be used to deliver new OTP messages via Voice. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. This endpoint will return an empty response object when it completes successfully. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/phone/verify-otp) endpoint to complete the update process. After successfully verifying the code the newphone number will replace the original phone number. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - Successful execution will return an empty body - To try this endpoint - need to provide `Project ID:Refresh JWT` as bearer. You can acquire the Session JWT by signing in the user and collecting it from the response. # Verify OTP Code (/api/otp/phone/verify-otp) ### Verify the validity of an OTP code via Voice Verify that the OTP code entered by the end user matches the OTP code that was sent. The Verify OTP code endpoint completes the OTP via Voice flow for: - [Sign-Up](/api/otp/phone/sign-up) - [Sign-In](/api/otp/phone/sign-in) - [Sign-In with Auto Sign-up](/api/otp/phone/sign-in-auto-sign-up) - [Update Phone](/api/otp/phone/update-phone) The response object includes the session JWT `sessionJwt` and refresh JWT `refreshJwt` when it completes successfully, and the end user will be signed in. For an update phone number flow, the new phone number will replace the original phone number. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. # Sign-In with Auto Sign-up (/api/otp/sms/sign-in-auto-sign-up) ### Sign-in end user (with automatic sign-up) by sending an OTP code via SMS Initiate a process that implements both sign-in and sign-up using a single endpoint. Descope will generate and deliver the One-Time Password (OTP) to the end user via SMS. If the phone number is already registered (the end user exists) the user will be signed in. If the phone number is not registered (the end user is not yet registered) the user will be signed up. Sending multiple OTP codes (for example, when an end user tries to sign-up/sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/sms/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/sms/sign-up) endpoint if you want a sign-up flow that will fail if the end user is already registered. - Use the [Sign-In](/api/otp/sms/sign-in-auto-sign-up) endpoint if you want a sign-in flow that will fail if the end user isn't yet registered. # Sign-In (/api/otp/sms/sign-in) ### Sign-in existing end user by sending an OTP code via SMS Initiate a sign-in process by sending a One-Time Password (OTP) to an existing end user. Descope will generate and deliver the OTP code to the phone number specified. Sending multiple OTP codes (for example, when an end user tries to sign-in a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the phone number is not yet registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/sms/verify-otp) endpoint to complete the user sign-in process. After successfully verifying the code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - See [User Login Options](/api/overview#user-login-options) for further details on loginOptions. - Use the [Sign-Up](/api/otp/sms/sign-up) endpoint to sign-up a new end user. - Use the [Sign-In with Auto Sign-up](/api/otp/sms/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Sign-Up (/api/otp/sms/sign-up) ### Sign-up new end user by sending an OTP code via SMS Initiate a sign-up process by sending a One-Time Password (OTP) to a new end user. Descope will generate and deliver the OTP code via SMS to the phone number specified. Sending multiple OTP codes (for example, when an end user tries to sign-up a second or third time) will invalidate any OTP code that has already been sent. This endpoint will return an empty response object when it completes successfully. The endpoint will return a failure code if the phone number is already registered. ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/sms/verify-otp) endpoint to complete the user sign-up process. After successfully verifying the OTP code the end user will be signed-in. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - Use the [Sign-In](/api/otp/sms/sign-in) endpoint to sign-in an existing end user. - Use the [Sign-In with Auto Sign-up](/api/otp/sms/sign-in-auto-sign-up) endpoint to create a single sign-up and sign-in flow, which will create a new end user if they are not already registered. # Update Phone (/api/otp/sms/update-phone) ### Update phone number of Existing User Update the phone number of an existing end user by sending an OTP code to the new phone number. After successfully verifying the code the new phone number will be used to deliver new OTP messages via SMS. The bearer token requires both the ProjectId and refresh JWT in the format `:`, and can therefore only be run for end users who are currently signed-in. This endpoint will return an empty response object when it completes successfully. Descope allows you to associating multiple login IDs for a user during API update calls. For details on how this feature works, please review the details [here](/manage/users#associating-multiple-login-ids-for-a-user). ### Next Steps Verify the OTP code using the [Verify OTP Code](/api/otp/sms/verify-otp) endpoint to complete the update process. After successfully verifying the code the newphone number will replace the original phone number. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. - Successful execution will return an empty body - To try this endpoint - need to provide `Project ID:Refresh JWT` as bearer. You can acquire the Session JWT by signing in the user and collecting it from the response. # Verify OTP Code (/api/otp/sms/verify-otp) ### Verify the validity of an OTP code via SMS Verify that the OTP code entered by the end user matches the OTP code that was sent. The Verify OTP code endpoint completes the OTP via SMS flow for: - [Sign-Up](/api/otp/sms/sign-up) - [Sign-In](/api/otp/sms/sign-in) - [Sign-In with Auto Sign-up](/api/otp/sms/sign-in-auto-sign-up) - [Update Email](/api/otp/sms/update-phone) The response object includes the session JWT `sessionJwt` and refresh JWT `refreshJwt` when it completes successfully, and the end user will be signed in. For an update phone number flow, the new phone number will replace the original phone number. ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email address and phone number. # Reset Password (/api/passwords/email/password-reset) ### Sent a password reset email to an existing user utilizing the password API. ### Next Steps You will then need to verify the user after the password reset is sent via email, this would need to be done via [Verify Magic Link](/api/magic-link/verification/verify-token). ### See Also - See [The User Object](/api/overview#the-user-object) for further details on how to identify users and their contact information such as email addresses and phone number. - You can also utilize [Update Password](/api/passwords/update-password) or [Replace Password](/api/passwords/replace-password) as alternatives to change a user's password. # Generate recovery codes for a user (/api/passwords/recovery-codes/generate-user-recovery-codes) Generate recovery codes for a user # Sign in a user using a recovery code (/api/passwords/recovery-codes/sign-in-recovery-code) Sign in a user using a recovery code # Get the security questions for a user to verify (/api/passwords/security-questions/get-user-security-verify-questions) Get the security questions for a user to verify # Sets up security questions for a user (/api/passwords/security-questions/setup-user-security-questions) Sets up security questions for a user # Verifies the security questions for a user (/api/passwords/security-questions/verify-user-security-questions) Verifies the security questions for a user # Backend (/sessions/validation/backend) Learn how Descope Backend SDK can validate session for secure session management. # Backend Session Validation This page focuses on backend session validation. Backend session validation is an integral part of secure session management, especially when dealing with APIs or more intricate use cases. This approach validates the session token at the server-side, thereby ensuring that the token has not been tampered with and is valid. It helps to verify that the incoming requests are indeed from authenticated users and not from potential attackers. If you're looking to set up web or mobile session handling, see [Session management](/sessions/management). For an overview of how Descope sessions work, see [Sessions](/sessions). For validating the session, you need to integrate the Descope Backend SDK with your application server. To use the backend SDK, first install the SDK using your package manager. After installing use the code below to add the session validation. ## Validate Session The session validation code below should be added to your application middleware (if you are using application middleware) for validating the session on all required routes. If you are not using a middleware, then you can add the validation code to all the routes which serve protected resource. Before validation, you need to extract the session token from the request authorization header. Note that you can customize certain properties like refresh token timeout in [Settings>Projects](https://app.descope.com/settings/project). Learn more [here](/management/project-settings). You can optionally validate the `aud` claim by passing an `audience` parameter. This ensures the token was issued for your specific application and prevents token reuse across different applications. The `audience` parameter accepts either a string or an array of strings. ```javascript // Args: // sessionToken (str): The session token, which contains the signature that will be validated const sessionToken = "xxxx"; // extract from request authorization header //refreshToken (str): The refresh token const refreshToken = "xxxx"; // extract from request authorization header try { // Basic validation without audience checking const authInfo = await descopeSdk.validateSession(sessionToken); // Or validate with a single audience (string) const authInfoWithAudience = await descopeSdk.validateSession(sessionToken, { audience: '__ProjectID__' }); // Or validate with multiple audiences (array) const authInfoWithMultipleAudiences = await descopeSdk.validateSession(sessionToken, { audience: ['__ProjectID__', 'my-custom-audience'] }); console.log("Successfully validated user session:"); console.log(authInfo); } catch (error) { console.log("Could not validate user session " + error); } // If validateSession throws an exception, you will need to refresh the session using const authInfo = await descopeClient.refreshSession(refreshToken); // For session migration, use refresh() with an externalToken instead of refreshSession(). // This exchanges a legacy provider token for Descope session/refresh JWTs. // The user must already exist in Descope before migration. const externalToken = "xxxx"; // token from your legacy auth provider const migrationResponse = await descopeClient.refresh(undefined, externalToken); // Alternatively, you could combine the two and // have the session validated and automatically refreshed when expired const authInfo = await descopeClient.validateAndRefreshSession(sessionToken, refreshToken); ``` ```python # Args: # session_token (str): The session token, which contains the signature that will be validated session_token = "xxxx" # extract from request authorization header # refreshToken (str): The refresh token refreshToken = "xxxx" # extract from request authorization header try: # Basic validation without audience checking jwt_response = descope_client.validate_session(session_token=session_token) # Or validate with a single audience (string) jwt_response_with_audience = descope_client.validate_session( session_token=session_token, audience="__ProjectID__" ) # Or validate with multiple audiences (list) jwt_response_with_multiple_audiences = descope_client.validate_session( session_token=session_token, audience=["__ProjectID__", "my-custom-audience"] ) print("Successfully validated user session:") print(jwt_response) except Exception as error: print("Could not validate user session. Error:") print(error) # If validate_session raises an exception, you will need to refresh the session using jwt_response = descope_client.refresh_session(refresh_token) # Alternatively, you could combine the two and # have the session validated and automatically refreshed when expired jwt_response = descope_client.validate_and_refresh_session(session_token, refresh_token) # -------------- or (use a Python Decorator) -------------- import descope_validate_auth @app.route('/protected', methods=['GET']) @descope_validate_auth(descope_client, permissions=["read"], roles=["user"], audience="__ProjectID__") def protected_route(): return "Access to protected resource granted" ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // sessionToken (str): The session token, which contains the signature that will be validated sessionToken := "xxxx" // extract from request authorization header. The above sample code sends the the session token in authorization header. //refreshToken (str): The refresh token refreshToken := "xxxx" // extract from request authorization header. authorized, userToken, err := descopeClient.Auth.ValidateSessionWithToken(ctx, sessionToken) if (err != nil){ fmt.Println("Could not validate user session: ", err) } else { fmt.Println("Successfully validated user session: ", userToken) } // If ValidateSessionWithRequest raises an exception, you will need to refresh the session using if authorized, sessionToken, err := descopeClient.Auth.RefreshSessionWithToken(ctx, refreshToken); !authorized { // unauthorized error } // If a response writer is available (e.g. inside an HTTP handler), use // RefreshSessionWithTokenAndWriter instead of RefreshSessionWithToken. It // automatically applies the refreshed session cookies to the response via w // (an http.ResponseWriter), so you don't need to manually forward the token. authorized, refreshedToken, err := descopeClient.Auth.RefreshSessionWithTokenAndWriter(ctx, refreshToken, w) if err != nil { fmt.Println("Could not refresh user session: ", err) } else if !authorized { // unauthorized error } else { fmt.Println("Successfully refreshed user session: ", refreshedToken) } // Alternatively, you could combine the two and // have the session validated and automatically refreshed when expired if authorized, sessionToken, err := descopeClient.Auth.ValidateAndRefreshSessionWithTokens(ctx, sessionToken, refreshToken); !authorized { // unauthorized error } ``` ```java // Validate the session. Will return an error if expired AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { Token t = as.validateSessionWithToken(sessionToken); } catch (DescopeException de) { // Handle the unauthorized error } // If ValidateSessionWithRequest raises an exception, you will need to refresh the session using try { Token t = as.refreshSessionWithToken(refreshToken); } catch (DescopeException de) { // Handle the unauthorized error } // If JWT rotation is enabled in your project settings, refreshing a session also returns a new // refresh token. Use the AuthenticationInfo variant to retrieve it try { AuthenticationInfo authInfo = as.refreshSessionWithTokenAuthenticationInfo(refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); // rotated refresh token JWT } catch (DescopeException de) { // Handle the unauthorized error } // Alternatively, you could combine the two and // have the session validated and automatically refreshed when expired try { Token t = as.validateAndRefreshSessionWithTokens(sessionToken, refreshToken); } catch (DescopeException de) { // unauthorized error } // Or use the AuthenticationInfo variant to also retrieve the new refresh token when // JWT rotation is enabled try { AuthenticationInfo authInfo = as.validateAndRefreshSessionWithTokensAuthenticationInfo(sessionToken, refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); // rotated refresh token JWT } catch (DescopeException de) { // unauthorized error } ``` ```ruby # Validate the session. Will raise if expired begin jwt_response = descope_client.validate_session('session_token') rescue AuthException => e # Session expired end # If validate_session raises an exception, you will need to refresh the session using jwt_response = descope_client.refresh_session('refresh_token') # Alternatively, you could combine the two and # have the session validated and automatically refreshed when expired jwt_response = descope_client.validate_and_refresh_session('session_token', 'refresh_token') ``` ```php // Will validate the session token and return either TRUE or FALSE, depending on if the JWT is valid and expired. if (isset($_POST["sessionToken"])) { if ($descopeSDK->verify($_POST["sessionToken"])) { $_SESSION["user"] = json_decode($_POST["userDetails"], true); $_SESSION["sessionToken"] = $_POST["sessionToken"]; session_write_close(); // User session validated and token saved } else { error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page } } else { error_log("Session token is not set in POST request."); // Redirect to login page } // Will refresh your session and return a new session token, with the refresh token. if (isset($_POST["refreshToken"])) { try { $newSession = $descopeSDK->refreshSession($_POST["refreshToken"]); $_SESSION["sessionToken"] = $newSession["sessionJwt"]; $_SESSION["refreshToken"] = $newSession["refreshJwt"]; session_write_close(); // Session successfully refreshed } catch (Exception $e) { error_log("Session refresh failed: " . $e->getMessage()); $descopeSDK->logout(); // Redirect to login page } } else { error_log("Refresh token is not set in POST request."); // Redirect to login page } // Will validate the session token and return either TRUE or FALSE, and will refresh your session and return a new session token. if (isset($_POST["sessionToken"]) && isset($_POST["refreshToken"])) { try { $isValid = $descopeSDK->verifyAndRefreshSession($_POST["sessionToken"], $_POST["refreshToken"], $newSession); if ($isValid) { $_SESSION["sessionToken"] = $newSession["sessionJwt"]; $_SESSION["refreshToken"] = $newSession["refreshJwt"]; session_write_close(); // Session verified and refreshed } else { error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page } } catch (Exception $e) { error_log("Verification or refresh failed: " . $e->getMessage()); $descopeSDK->logout(); // Redirect to login page } } else { error_log("Session or refresh token is not set in POST request."); // Redirect to login page } ``` ```csharp // Args: // sessionToken (string): The session token, which contains the signature that will be validated var sessionToken = "xxxx"; // extract from request authorization header. // refreshToken (string): The refresh token var refreshToken = "xxxx"; // extract from request authorization header. // Validate the session. Will return an error if session token is expired. try { var sessionToken = await descopeClient.Auth.ValidateSessionAsync(sessionJwt); } catch (DescopeException ex) { // Handle the error } // If ValidateSession throws an exception, you will need to refresh the session using the refresh token. try { var sessionToken = await descopeClient.Auth.RefreshSessionAsync(refreshJwt); } catch (DescopeException ex) { // Handle the error } // Alternatively, you could combine the two and have the session validated and automatically refreshed when expired. try { var sessionToken = descopeClient.Auth.ValidateAndRefreshSession(sessionJwt, refreshJwt); } catch (DescopeException ex) { // Handle the error } ``` ## Logout Current Session using Backend SDK You can log out a user from an active session by providing their refreshToken for that session. After calling this function, you must invalidate or remove any cookies you have created. ```javascript // Args: // refreshToken: refresh token from the successful sign-in of the user const refreshToken = "xxxx" const resp = await descopeClient.logout(refreshToken); if (!resp.ok) { console.log("Failed to log user out of current session.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully logged user out of current session.") console.log(resp.data) } ``` ```python # Args: # refresh_token: refresh token from the successful sign-in of the user try: resp = descope_client.logout(refresh_token) print ("Successfully logged user out of current session.") except AuthException as error: print ("Failed to log user out of current session.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) # -------------- or (use a Python Decorator) -------------- from descope.flask import descope_logout @app.route('/logout', methods=['GET']) @descope_logout(descope_client) def logout(): return "Logged out successfully" ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // r: HttpRequest for the action. Refresh token will be taken from the request header or cookies automatically // w (optional): If provided, the optional `w http.ResponseWriter` will empty out the session cookies automatically. err := descopeClient.logout(ctx, request, w) if (err != nil){ fmt.Println("Failed to log user out of current session: ", err) } else { fmt.Println("Successfully logged user out of current session.") } ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { as.logout(refreshToken); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.sign_out('refresh_token') ``` ```php $descopeSDK->logout($refreshToken) // Throws an AuthException on fail ``` ```csharp // Args: // refreshToken (string): refresh token from the successful sign-in of the user. var refreshToken = "xxxx"; try { await descopeClient.Auth.V1.Logout.PostWithJwtAsync(new LogoutRequest(), refreshToken); } catch (DescopeException ex) { // Handle the error } ``` ## Logout All Sessions using Backend SDK It is possible to sign the user out of all the devices they are currently signed-in with. Calling the logout all function will invalidate all user's refresh tokens. After calling this function, you must invalidate or remove any cookies you have created. ```javascript // Args: // refreshToken: refresh token from the successful sign-in of the user const refreshToken = "xxxx" const resp = await descopeClient.logoutAll(refreshToken); if (!resp.ok) { console.log("Failed to log user out of all current sessions.") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully logged user out of all current sessions.") console.log(resp.data) } ``` ```python # Args: # refresh_token: refresh token from the successful sign-in of the user try: resp = descope_client.logout_all(refresh_token) print ("Successfully logged user out of all current sessions.") except AuthException as error: print ("Failed to log user out of all current sessions.") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // r: HttpRequest for the action. Refresh token will be taken from the request header or cookies automatically // w (optional): If provided, the optional `w http.ResponseWriter` will empty out the session cookies automatically. err := descopeClient.logoutAll(ctx, request, w) if (err != nil){ fmt.Println("Failed to log user out of all current sessions: ", err) } else { fmt.Println("Successfully logged user out of all current sessions.") } ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { as.logoutAll(refreshToken); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.sign_out('refresh_token') # It is also possible to sign the user out of all the devices they are currently signed-in with. Calling logout_all will invalidate all user's refresh tokens. After calling this function, you must invalidate or remove any cookies you have created. descope_client.sign_out_all('refresh_token') ``` ```php $descopeSDK->logoutAll($refreshToken) // Throws an AuthException on fail ``` To learn more about how refresh token storage works, check out our [Refresh Token Storage](/additional-security-features-in-descope/refresh-token-rotation) page. ## Validating roles and permissions If your applications does not need different roles for the logged in users, then skip this section. If you are using [authorization feature](/authorization/role-based-access-control) in Descope, then you can use the Descope backend sdk to validate roles and permissions. The roles and permissions for the user are returned in the session token. You can either use your own JWT validation library and access the tenant information, roles and permissions for the authenticated user or use the SDK calls shown below. If you are using tenant capabilities in Descope, then use the tenant validation calls for validating permissions and roles. ### Roles The "Roles" section pertains to the process of validating user roles within an application. These roles are typically defined and allocated by an administrator and are used to manage user access to certain resources or actions within an application. The validateRoles function is used to confirm whether a user has a valid role, based on the role names provided as arguments. The roles to be validated are provided as a string array. This function returns a Boolean value indicating whether the user's roles are valid (true) or invalid (false). ```javascript // validateRoles function // Args: // authenticationInfo : authentication information with claims returned from the validateSession call // roles: string array containing names of roles to be validated const roles=[] // Return value : Boolean (true or false) try { const isRoleValid = descopeClient.validateRoles(authInfo, roles); if (isRoleValid) { console.log("These roles are valid for user") } else { console.log("These roles are invalid for user") } } catch (error) { console.log ("Could not confirm if roles are valid - error prior to confirmation. " + error); } ``` ```python # validate_roles function # Args: # jwt_response: The response that you get from the validate_session call. # roles: list of strings - each string represents a role name that is validated. # Return value: Boolean (True or False) try: isRoleValid = descope_client.validate_roles(jwt_response, []) if isRoleValid == True: print ("These roles are valid for user") else: print ("These roles are invalid for user") except Exception as error: print ("Could not confirm if roles are valid - error prior to confirmation. Error:") print (error) ``` ```go // ValidateRoles validates the roles that are given as an array using the userToken returned from ValidateSession function // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userToken: token returned from ValidateSession call. // roles: list of roles to validate roles:= [] // Return value: // bool: true or false depending of the roles match or not // err - has the error message (can be populated even if session is valid) // If the session is not valid, the function returns an error. if isValidRoles, err := descopeClient.Auth.ValidateRoles(ctx, userToken, roles); !isValidRoles { // roles are invalid for the user //WIP log.Println("These roles are invalid for the user") } ``` ```java // You can validate specific permissions AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); // Or validate roles directly try { if (!as.validateRoles(sessionToken, Arrays.asList("Role to validate"))) { // Deny access } } catch (DescopeException de) { // Handle the error } ``` ```ruby # Or validate roles directly valid_roles = descope_client.validate_roles( jwt_response: 'resp', roles: ['Role to validate'] ) unless valid_roles # Deny access end ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var roles = new List { "role-name1", "role-name2" }; // tenantID (string?): Optional tenant ID if roles are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" bool isRoleValid = sessionToken.ValidateRoles(roles, tenantID); if (!isRoleValid) { // Deny access } ``` ### Permissions The "Permissions" section discusses the mechanism of checking user permissions. Permissions, similar to roles, regulate what actions a user can perform within a system, but they are often more granular and specific. The validatePermissions function is used to validate if a user has specific permissions based on the permission names provided as arguments. Like with roles, the permissions to be validated are supplied as a string array. The function then returns a Boolean value, where 'true' indicates the user has the validated permissions, and 'false' suggests the opposite. ```javascript // validatePermissions function // Args: // authenticationInfo : authentication information with claims returned from the validateSession call // roles: string array containing names of roles to be validated const permissions=[] // Return value : Boolean (true or false) try { const isPermissionValid = descopeClient.validatePermissions(authInfo, permissions); if (isPermissionValid) { console.log("These permissions are valid for user") } else { console.log("These permissions are invalid for user") } } catch (error) { console.log ("Could not confirm if permissions are valid - error prior to confirmation. " + error); } ``` ```python # validate_permissions function # Args: # jwt_response: The response that you get from the validate_session call. # permissions: list of strings - each string represents a role name that is validated. # Return value: Boolean (True or False) try: isPermissionValid = descope_client.validate_permissions(jwt_response, []) if isPermissionValid == True: print ("These permissions are valid for user") else: print ("These permissions are invalid for user") except Exception as error: print ("Could not confirm if permissions are valid - error prior to confirmation. Error:") print (error) ``` ```go // ValidatePermissions validates the roles that are given as an array using the userToken returned from ValidateSession function // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userToken: token returned from ValidateSession call. // permissions: list of permissions to validate permissions:=[] // Return value: // bool: true or false depending of the permissions match or not // err - has the error message (can be populated even if session is valid) // If the session is not valid, the function returns an error. if isValidPermissions, err := descopeClient.Auth.ValidatePermissions(ctx, userToken, permissions); !isValidPermissions { // permissions are invalid for the user //WIP log.Println("These permissions are invalid for the user") } ``` ```java // You can validate specific permissions AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { if (!as.validatePermissions(sessionToken, Arrays.asList("Permission to validate"))) { // Deny access } } catch (DescopeException de) { // Handle the error } ``` ```ruby # You can validate specific permissions valid_permissions = descope_client.validate_permissions( jwt_response: 'resp', permissions: ['Permission to validate'] ) unless valid_permissions # Deny access end ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var permissions = new List { "permission-name1", "permission-name2" }; // tenantID (string?): Optional tenant ID if permissions are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" bool isPermissionValid = sessionToken.ValidatePermissions(permissions, tenantID); if (!isPermissionValid) { // Deny access } ``` ### Roles with Tenant Function The "Roles with Tenant Function" section deals with the validation of user roles within the context of a specific tenant. In multi-tenant environments, where a single instance of software serves multiple users or groups of users (tenants), users might have different roles depending on the tenant they are interacting with. The validateTenantRoles function is used to check if a user's role is valid for a specific tenant. This function takes in the tenant's ID and the roles to be validated, and it returns a Boolean value indicating the validity of the roles for the specified tenant. ```javascript // validateRoles with tenant function // Args: // authenticationInfo : authentication information with claims returned from the validateSession call // tenantId : tenant id for the tenant the user is logging in const tenantId="xxx" // roles: string array containing names of roles to be validated const roles=[] // Return value : Boolean (true or false) try { const isRoleValid = descopeClient.validateTenantRoles(authInfo, tenantId, roles); if (isRoleValid) { console.log("These roles are valid for tenant " + tenantId) } else { console.log("These roles are invalid for tenant " + tenantId) } } catch (error) { console.log ("Could not confirm if roles are valid - error prior to confirmation. " + error); } ``` ```python # validate_tenant_roles function # Args: # jwt_response: The response that you get from the validate_session call. # tenant_id: tenant id for the tenant the user is logging in tenant_id="xxx" # roles: list of strings - each string represents a role name that is validated. # Return value: Boolean (True or False) try: isRoleValid = descope_client.validate_tenant_roles(jwt_response, tenant_id, []) if isRoleValid == True: print ("These roles are valid for tenant " + tenant_id) else: print ("These roles are invalid for tenant " + tenant_id) except Exception as error: print ("Could not confirm if roles are valid - error prior to confirmation. Error:") print (error) ``` ```go // ValidateTenantRoles validates the roles that are given as an array using the userToken returned from ValidateSession function // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userToken: token returned from ValidateSession call. // tenantID: tenant id for the tenant the user is logging in tenantID:="xxx" // roles: list of roles to validate roles:= [] // Return value: // bool: true or false depending of the roles match or not // err - has the error message (can be populated even if session is valid) // If the session is not valid, the function returns an error. if isValidRoles, err := descopeClient.Auth.ValidateTenantRoles(ctx, userToken, tenantID, roles); !isValidRoles { // roles are invalid for the tenant //WIP log.Println("These roles are invalid for tenant " + tenantID) } ``` ```java // You can validate specific permissions AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); // Or validate roles directly try { if (!as.validateRoles(sessionToken, "my-tenant-ID", Arrays.asList("Role to validate"))) { // Deny access } } catch (DescopeException de) { // Handle the error } ``` ```ruby # Or validate roles directly valid_roles = descope_client.validate_tenant_roles( jwt_response: 'resp', tenant: 'my-tenant-ID', roles: ['Role to validate'] ) unless valid_roles # Deny access end ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var roles = new List { "Role to validate" }; // tenantID (string?): Optional tenant ID if roles are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" bool isRoleValid = sessionToken.ValidateRoles(roles, tenantID); if (!isRoleValid) { // Deny access } ``` ### Permissions with Tenant Function The "Permissions with Tenant Function" section revolves around the process of verifying user permissions within the context of a specific tenant. Similar to the role validation, permission validation in multi-tenant environments can be tenant-specific. The validateTenantPermissions function is used to determine whether a user's permissions are valid for a particular tenant. This function takes in the tenant's ID and the permissions to be validated, returning a Boolean value that represents the validity of these permissions for the specified tenant. ```javascript // validatePermissions with tenant function // Args: // authenticationInfo : authentication information with claims returned from the validateSession call // tenantId : tenant id for the tenant the user is logging in const tenantId="xxx" // roles: string array containing names of roles to be validated const permissions=[] // Return value : Boolean (true or false) try { const isPermissionValid = descopeClient.validateTenantPermissions(authInfo, tenantId, permissions); if (isPermissionValid) { console.log("These permissions are valid for tenant " + tenantId) } else { console.log("These permissions are invalid for tenant " + tenantId) } } catch (error) { console.log ("Could not confirm if permissions are valid - error prior to confirmation. " + error); } ``` ```python # validate_tenant_permissions function # Args: # jwt_response: The response that you get from the validate_session call. # tenant_id: tenant id for the tenant the user is logging in tenant_id="xxx" # permissions: list of strings - each string represents a role name that is validated. # Return value: Boolean (True or False) try: isPermissionValid = descope_client.validate_tenant_permissions(jwt_response, tenant_id, []) if isPermissionValid == True: print ("These permissions are valid for tenant " + tenant_id) else: print ("These permissions are invalid for tenant " + tenant_id) except Exception as error: print ("Could not confirm if permissions are valid - error prior to confirmation. Error:") print (error) ``` ```go // ValidateTenantPermissions validates the roles that are given as an array using the userToken returned from ValidateSession function // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // userToken: token returned from ValidateSession call. // tenantID: tenant id for the tenant the user is logging in tenantID:="xxx" // permissions: list of permissions to validate permissions:=[] // Return value: // bool: true or false depending of the permissions match or not // err - has the error message (can be populated even if session is valid) // If the session is not valid, the function returns an error. if isValidPermissions, err := descopeClient.Auth.ValidateTenantPermissions(ctx, userToken, tenantID, permissions); !isValidPermissions { // permissions are invalid for the tenant //WIP log.Println("These permissions are invalid for tenant " + tenantID) } ``` ```java // You can validate specific permissions AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { if (!as.validatePermissions(sessionToken, "my-tenant-ID", Arrays.asList("Permission to validate"))) { // Deny access } } catch (DescopeException de) { // Handle the error } ``` ```ruby # You can validate specific permissions valid_permissions = descope_client.validate_tenant_permissions( jwt_response: 'resp', tenant: 'my-tenant-ID', permissions: ['Permission to validate'] ) unless valid_permissions # Deny access end ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var permissions = new List { "Permission to validate" }; // tenantID (string?): Optional tenant ID if permissions are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" bool isPermissionValid = sessionToken.ValidatePermissions(permissions, tenantID); if (!isPermissionValid) { // Deny access } ``` ## Retrieving Matching Roles and Permissions To get matching roles or permissions of a user, you can use the Descope backend sdk, as shown below. The roles and permissions for the user are returned in the session token. So alternatively, you can even use your own JWT validation library. If you are using tenant capabilities in Descope, then use the tenant matching calls for getting matching permissions and roles. ### Roles Roles are extracted from JWT claims based on specified criteria. Below is an example of how to retrieve roles using different programming languages: ```javascript /** * Retrieves the roles from JWT top level claims that match the specified roles list * @param authInfo JWT parsed info containing the roles * @param roles List of roles to match against the JWT claims * @returns An array of roles that are both in the JWT claims and the specified list. Returns an empty array if no matches are found */ ``` ```javascript try { const matchedRoles = descopeClient.getMatchedRoles(authInfo, [ 'Role to validate', 'Another role to validate', ]); } catch (error) { console.log ("Could not confirm if roles are valid - error prior to confirmation. " + error); } ``` ```python try: matched_roles = descope_client.get_matched_roles( jwt_response, ["role-name1", "role-name2"] ) except Exception as error: print ("Could not confirm if roles are valid - error prior to confirmation. Error:") print (error) ``` ```go matchedRoles := descopeClient.Auth.GetMatchedRoles(sessionToken, []string{"role-name1", "role-name2"}) ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { List matchedRoles = as.getMatchedRoles(sessionToken, Arrays.asList("Role1", "Role2")); } catch (DescopeException de) { // Handle the error } ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var roles = new List { "role-name1", "role-name2" }; // tenantID (string?): Optional tenant ID if roles are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" var matchedRoles = sessionToken.GetMatchedRoles(roles, tenantID); ``` ### Permissions Permissions are similarly retrieved from JWT claims and checked against a specified list. Example implementations across different languages are provided below: ```javascript /** * Retrieves the permissions from JWT top level claims that match the specified permissions list * @param authInfo JWT parsed info containing the permissions * @param permissions List of permissions to match against the JWT claims * @returns An array of permissions that are both in the JWT claims and the specified list. Returns an empty array if no matches are found */ ``` ```javascript try { const matchedPermissions = descopeClient.getMatchedPermissions(authInfo, [ 'Permission to validate', 'Another permission to validate', ]); if (isPermissionValid) { console.log("These permissions are valid for user") } else { console.log("These permissions are invalid for user") } } catch (error) { console.log ("Could not confirm if permissions are valid - error prior to confirmation. " + error); } ``` ```python try: matched_permissions = descope_client.get_matched_permissions( jwt_response, ["permission-name1", "permission-name2"] ) except Exception as error: print ("Could not confirm if permissions are valid - error prior to confirmation. Error:") print (error) ``` ```go matchedPermissions := descopeClient.Auth.GetMatchedPermissions(sessionToken, []string{"permission-name1", "permission-name2"}) ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { List matchedPermissions = as.getMatchedPermissions(sessionToken, Arrays.asList("Permission1", "Permission2")); } catch (DescopeException de) { // Handle the error } ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var permissions = new List { "permission-name1", "permission-name2" }; // tenantID (string?): Optional tenant ID if permissions are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" var matchedPermissions = sessionToken.GetMatchedPermissions(permissions, tenantID); ``` ### Roles with Tenant Function When using tenant-specific functionalities, roles can be matched as follows: ```javascript /** * Retrieves the roles from JWT tenant claims that match the specified roles list * @param authInfo JWT parsed info containing the roles * @param tenant tenant to match the roles for * @param roles List of roles to match against the JWT claims * @returns An array of roles that are both in the JWT claims and the specified list. Returns an empty array if no matches are found */ ``` ```javascript try { const matchedTenantRoles = descopeClient.getMatchedTenantRoles(authInfo, 'my-tenant-ID', [ 'Role to validate', 'Another role to validate', ]); } catch (error) { console.log ("Could not confirm if roles are valid - error prior to confirmation. " + error); } ``` ```python try: matched_tenant_roles = descope_client.get_matched_tenant_roles( jwt_response, "my-tenant-ID", ["role-name1", "role-name2"] ) except Exception as error: print ("Could not confirm if roles are valid - error prior to confirmation. Error:") print (error) ``` ```go matchedTenantRoles := descopeClient.Auth.GetTenantRoles(sessionToken, "my-tenant-ID", []string{"role-name1", "role-name2"}) ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { List matchedRoles = as.getMatchedRoles(sessionToken, "my-tenant-ID", Arrays.asList("Role1", "Role2")); } catch (DescopeException de) { // Handle the error } ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var roles = new List { "role-name1", "role-name2" }; // tenantID (string?): Optional tenant ID if roles are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" var matchedTenantRoles = sessionToken.GetMatchedRoles(roles, tenantID); ``` ### Permissions with Tenant Function Matching permissions within a specific tenant context is handled similarly: ```javascript /** * Retrieves the permissions from JWT top level claims that match the specified permissions list * @param authInfo JWT parsed info containing the permissions * @param permissions List of permissions to match against the JWT claims * @returns An array of permissions that are both in the JWT claims and the specified list. Returns an empty array if no matches are found */ ``` ```javascript try { const matchedTenantPermissions = descopeClient.getMatchedTenantPermissions( authInfo, 'my-tenant-ID', ['Permission to validate', 'Another permission to validate'], ); } catch (error) { console.log ("Could not confirm if permissions are valid - error prior to confirmation. " + error); } ``` ```python try: matched_tenant_permissions = descope_client.get_matched_tenant_permissions( jwt_response, "my-tenant-ID", ["permission-name1", "permission-name2"] ) except Exception as error: print ("Could not confirm if permissions are valid - error prior to confirmation. Error:") print (error) ``` ```go matchedTenantPermissions := descopeClient.Auth.GetTenantPermissions(sessionToken, "my-tenant-ID", []string{"permission-name1", "permission-name2"}) ``` ```java AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { List matchedPermissions = as.getMatchedPermissions(sessionToken, "my-tenant-ID", Arrays.asList("Permission1", "Permission2")); } catch (DescopeException de) { // Handle the error } ``` ```csharp // sessionToken is the Token object returned from ValidateSessionAsync var permissions = new List { "permission-name1", "permission-name2" }; // tenantID (string?): Optional tenant ID if permissions are tenant-scoped. string? tenantID = null; // e.g., "tenant-ID1" var matchedTenantPermissions = sessionToken.GetMatchedPermissions(permissions, tenantID); ``` # Validating JWTs Offline (/sessions/validation/backend/offline-jwt-validation) Learn how to validate JSON Web Tokens (JWTs) via SDK offline and in middleware # Validating JWTs Offline Descope employs JSON Web Token (JWT) to ensure secure authentication, and authorization. In web applications, it's essential to parse and validate these tokens to guarantee their integrity and authenticity. Here's how to use Descope's backend SDKs to validate JWTs: ## Call the Validate JWT Function After passing in the JWT from the frontend to your backend, you can simply call the validate JWT function. You can optionally validate the `aud` claim by passing an `audience` parameter to prevent token reuse across applications. The parameter accepts either a string or an array of strings. **Note:** Not all SDKs support audience validation - see code examples below for supported SDKs. ```javascript // Args: // sessionToken (str): The session token, which contains the signature that will be validated const sessionToken = "xxxx"; // extract from request authorization header try { // Basic validation without audience checking const authInfo = await descopeSdk.validateSession(sessionToken); // Or validate with a single audience (string) const authInfoWithAudience = await descopeSdk.validateSession(sessionToken, { audience: '__ProjectID__' }); // Or validate with multiple audiences (array) const authInfoWithMultipleAudiences = await descopeSdk.validateSession(sessionToken, { audience: ['__ProjectID__', 'my-custom-audience'] }); console.log("Successfully validated user session:"); console.log(authInfo); } catch (error) { console.log("Could not validate user session " + error); } ``` ```python # Args: # session_token (str): The session token, which contains the signature that will be validated session_token = "xxxx" # extract from request authorization header try: # Basic validation without audience checking jwt_response = descope_client.validate_session(session_token=session_token) # Or validate with a single audience (string) jwt_response_with_audience = descope_client.validate_session( session_token=session_token, audience="__ProjectID__" ) # Or validate with multiple audiences (list) jwt_response_with_multiple_audiences = descope_client.validate_session( session_token=session_token, audience=["__ProjectID__", "my-custom-audience"] ) print("Successfully validated user session:") print(jwt_response) except Exception as error: print("Could not validate user session. Error:") print(error) # -------------- or (use a Python Decorator) -------------- import descope_validate_auth @app.route('/protected', methods=['GET']) @descope_validate_auth(descope_client, permissions=["read"], roles=["user"], audience="__ProjectID__") def protected_route(): return "Access to protected resource granted" ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // sessionToken (str): The session token, which contains the signature that will be validated sessionToken := "xxxx" // extract from request authorization header. The above sample code sends the the session token in authorization header. authorized, userToken, err := descopeClient.Auth.ValidateSessionWithToken(ctx, sessionToken) if (err != nil){ fmt.Println("Could not validate user session: ", err) } else { fmt.Println("Successfully validated user session: ", userToken) } ``` ```java // Validate the session. Will return an error if expired AuthenticationService as = descopeClient.getAuthenticationServices().getAuthenticationService(); try { Token t = as.validateSessionWithToken(sessionToken); } catch (DescopeException de) { // Handle the unauthorized error } // If ValidateSessionWithRequest raises an exception, you will need to refresh the session using try { Token t = as.refreshSessionWithToken(refreshToken); } catch (DescopeException de) { // Handle the unauthorized error } // If JWT rotation is enabled in your project settings, refreshing a session also returns a new // refresh token. Use the AuthenticationInfo variant to retrieve it try { AuthenticationInfo authInfo = as.refreshSessionWithTokenAuthenticationInfo(refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); // rotated refresh token JWT } catch (DescopeException de) { // Handle the unauthorized error } // Alternatively, you could combine the two and // have the session validated and automatically refreshed when expired try { Token t = as.validateAndRefreshSessionWithTokens(sessionToken, refreshToken); } catch (DescopeException de) { // unauthorized error } // Or use the AuthenticationInfo variant to also retrieve the new refresh token when // JWT rotation is enabled try { AuthenticationInfo authInfo = as.validateAndRefreshSessionWithTokensAuthenticationInfo(sessionToken, refreshToken); String newRefreshJwt = authInfo.getRefreshToken().getJwt(); // rotated refresh token JWT } catch (DescopeException de) { // unauthorized error } ``` ```ruby # Validate the session. Will raise if expired begin jwt_response = descope_client.validate_session('session_token') rescue AuthException => e # Session expired end # If validate_session raises an exception, you will need to refresh the session using jwt_response = descope_client.refresh_session('refresh_token') # Alternatively, you could combine the two and # have the session validated and automatically refreshed when expired jwt_response = descope_client.validate_and_refresh_session('session_token', 'refresh_token') ``` ```php if (isset($_POST["sessionToken"])) { if ($descopeSDK->verify($_POST["sessionToken"])) { $_SESSION["user"] = json_decode($_POST["userDetails"], true); $_SESSION["sessionToken"] = $_POST["sessionToken"]; session_write_close(); // User session validated and token saved } else { error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page } } else { error_log("Session token is not set in POST request."); // Redirect to login page } ``` ## Configuring JWT Validation Leeway Currently JWT validation leeway is only configurable in the [Go](https://github.com/descope/go-sdk) and [Python](https://github.com/descope/python-sdk) SDKs. When validating JWTs, the SDK accounts for minor clock differences (time skew) between the systems that issued and validate the token. By default, the SDK applies a **5-second leeway** to the time-based claims (such as `exp`, `nbf`, and `iat`), so small clock drifts between servers do not cause otherwise-valid tokens to be rejected. If your systems experience larger clock differences, you can increase the leeway when initializing the SDK. ```go import "github.com/descope/go-sdk/descope/client" import "time" // JWTLeeway (optional, defaults to 5 seconds) sets the acceptable time skew (leeway) // applied to JWT time-based claims during validation. descopeClient, err := client.NewWithConfig(&client.Config{ ProjectID: "__ProjectID__", JWTLeeway: 30 * time.Second, // Increase leeway to handle larger clock differences }) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```python from descope import DescopeClient # jwt_validation_leeway (optional, in seconds, defaults to 5) sets the acceptable # time skew (leeway) applied to JWT time-based claims during validation. descope_client = DescopeClient( project_id="__ProjectID__", jwt_validation_leeway=30, # Increase leeway to handle larger clock differences ) ``` ## Offline Validating JSON Web Tokens (JWTs) offline is crucial in situations where the server running the SDK does not have access to the internet. Descope SDKs allow you to handle this scenario with ease. This article explains how to validate JWTs offline by providing a custom public key. ### Providing a Custom Public Key #### Finding Your Public Key Your public key can be located at `https://api.descope.com/v2/keys/__ProjectID__` for US-based projects. Use the localized baseURL for projects located outside of the US. Refer to the [Descope Documentation and API reference page](/api/session/get-keys-v2) for additional details on locating and handling public keys. #### Initializing the SDK with a Custom Public Key To provide your own public key, you can do so by including the `publicKey` option when initializing the SDK. The public key must be a JSON object containing the appropriate algorithm and other details. Below are examples of initializing the SDK with a public key. ```javascript import DescopeClient from '@descope/node-sdk'; try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', publicKey: '{"alg":"RS256", ... }'}); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping, LoginOptions ) try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', public_key='{"alg": "RS256", ... }') except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__"}, PublicKey:'{"alg":"RS256", ... }') if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` #### Conclusion Validating JWTs offline via SDK by providing a custom public key enhances security and functionality, especially when working in environments without internet access. If you have any other questions about Descope, feel free to reach out to [us](/support)! # AWS AppSync (/sessions/validation/jwt-authorizers/aws-app-sync) Learn how to configure Descope JWTs to work with AWS AppSync. # Descope with AWS AppSync AWS AppSync is a managed serverless GraphQL service that simplifies application development by enabling you to create a flexible API to securely access, manipulate, and combine data from multiple sources. This guide will walk you through the steps to configure AWS AppSync to validate Descope JWTs, ensuring that only authenticated users can access your GraphQL endpoints. If you want to use the Descope JWTs to protect your AWS API Gateway endpoints, you can configure a [JWT Authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) to handle this. ## Step 1: Configuring Your Descope Project **Issuer (`iss`) claim:** The standard issuer in Descope JWTs is: - `__BaseURL__/v1/apps/__ProjectID__` Pre-existing projects might not have the default OIDC-compliant User JWT template enabled, so the `iss` claim may be the shorter form (`__BaseURL__/__ProjectID__`) until you switch to or create a template that uses the new issuer URL. The OpenID Connect Provider URL you configure in AppSync (Step 3) must match the `iss` claim in your tokens exactly. Check your JWT's `iss` claim (e.g. in [jwt.guru](https://jwt.guru)) and use that value. First, adjust your Descope project settings to generate JWTs compatible with AWS services: 1. Log in to your Descope dashboard. 2. Navigate to the JWT Templates page on [Project Settings](https://app.descope.com/settings/project/jwt). 3. Create a new JWT template using the **AWS API Gateway** default. This sets the `Issuer` (`iss`) claim to the full URL: `__BaseURL__/v1/apps/__ProjectID__` (replacing `__ProjectID__` with your project ID). If you use a federated app or an older template, your issuer may remain `__BaseURL__/__ProjectID__`—use whichever issuer your tokens actually contain. ## Step 2: Setting Up AWS AppSync Next, configure AWS AppSync to use Descope JWTs: 1. Go to the AWS Management Console and open up **AppSync**. 2. Select or create a new GraphQL API. 3. Navigate to the `Settings` section of your API, on the left-hand menu. ## Step 3: Configuring the Authorization Type In the settings: 1. Under `Authorization type`, select `OpenID Connect`. 2. For `OpenID Connect Provider URL`, enter the issuer URL that matches the `iss` claim in your Descope JWTs: - **Standard:** `__BaseURL__/v1/apps/__ProjectID__` - **Federated app or older JWT template:** `__BaseURL__/__ProjectID__` Replace `__ProjectID__` with your Descope Project ID. The value must match your token's `iss` exactly. 3. Set the `Client ID` field to your Descope Project ID. ![Enable Descope as OIDC Provider in API settings](/assets/aws-appsync-oidc-provider.webp) ## Step 4: Modifying your GraphQL Schema If you're using Descope as an OIDC provider for your authorization controls, you'll need to modify your GraphQL schema to be able to accept Descope JWTs as authorization tokens. If you're using AWS Amplify, you can do this by following the guide in the AWS Amplify [docs page](https://docs.amplify.aws/javascript/build-a-backend/graphqlapi/customize-authorization-rules/#using-oidc-authorization-provider). This table outlines different authorization strategies and their corresponding providers for accessing data in an AWS AppSync environment. Each row represents a specific use case and the strategy/provider to use for that scenario. | Recommended use case | Strategy | | ------- | ------- | | Per user data access. Access is restricted to the "owner" of a record. Leverages amplify add auth Cognito user pool by default. | owner | | Any signed-in data access. Unlike owner-based access, any signed-in user has access. | private | | Per user group data access. A specific or dynamically configured group of users have access | groups | In your app, using the Amplify SDK, you can edit the `graphql.schema` file with your desired authorization strategy, and then do a `amplify push`. As an example, this schema will allow all users (as long as their signed in with Descope) to query from your backend: ```graphql title="graphql.schema" type Todo @model @auth( rules: [ { allow: private, provider: oidc } ] ) { content: String } ``` ## Step 5: Make Sure that Descope JWTs are AWS Compliant AWS validates the JWT's `iss` (issuer) claim against the OpenID Connect Provider URL you set in [Step 3](#step-3-configuring-the-authorization-type). They must match exactly. - **Standard issuer:** `__BaseURL__/v1/apps/__ProjectID__` - **Alternate (federated app or older JWT template):** `__BaseURL__/__ProjectID__` Confirm which issuer your tokens use (e.g. decode a token at [jwt.guru](https://jwt.guru) or check your JWT template in Descope), then ensure the same URL is configured in AppSync. You can manage JWT templates in [Project Settings](https://app.descope.com/settings/project) in the Descope Console. ![Descope AWS Compliant JWT configuration](/assets/aws-jwt-template-3.webp) ## Step 6: Utilizing the Descope Tokens with your Queries and Mutations You'll need to pass your Descope `sessionToken` into your queries using the `authToken` parameter. You can get access to this on `client-side` pages with the React SDK and `getServerSession`. An example of this is below: ```javascript title="index.js" import { generateClient } from 'aws-amplify/api'; import { getSessionToken } from '@descope/react-sdk'; const client = generateClient(); const fetchData = async () => { const sessionToken = getSessionToken(); console.log(sessionToken); const todos = await client.graphql({ query: listTodos, authToken: sessionToken }); console.log(todos); return todos; }; ``` ## Step 7: Deploying and Testing After configuring the authorization settings: 1. Deploy your GraphQL API. 2. Test the API using a valid JWT token obtained from your Descope project. Ensure that authenticated requests are successful and unauthorized requests are denied. ## Sample App If you want to look at the source code of a sample Next.js application using the Amplify SDK with an AppSync schema and the Descope React SDK, you can look at our sample application on [GitHub](https://github.com/descope-sample-apps/descope-amplify-appsync). There are also instructions in the [README](https://github.com/descope-sample-apps/descope-amplify-appsync/blob/main/README.md) for how to spin up your own version of the sample app, with your own AWS instance and AppSync backend. ## Conclusion By integrating Descope JWTs with AWS AppSync, you benefit from the streamlined and secure authentication flow provided by Descope, while leveraging the powerful features of AWS AppSync. This setup ensures that your GraphQL APIs are protected and only accessible by authenticated users. For more detailed information on AWS AppSync and JWT authorization, refer to the [AWS AppSync Developer Guide](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html). If you have any questions or need further assistance with Descope, don't hesitate to contact [Descope Support](/support). # AWS API Gateway (/sessions/validation/jwt-authorizers/aws-jwt-authorizer) Learn how to configure Descope JWTs to work with AWS API Gateway. # Using Descope JWTs with AWS API Gateway By configuring Descope JWTs to work with AWS API Gateway, you leverage the built-in JWT validation mechanism of AWS and the secure token issuing capabilities of Descope. This ensures that the API's endpoints are only accessible by clients that present a valid Descope JWT. If you're using Descope and want to use the Descope JWT tokens to protect your AWS API endpoints, you can configure a [JWT Authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) to handle this. An AWS JWT Authorizer is a specific type of Lambda authorizer, which allows you to use custom JSON Web Tokens (JWTs) as authorization tokens when clients try to access your APIs. The primary function of a JWT Authorizer is to: ``` 1. Decode the incoming JWT token. 2. Validate the JWT signature. 3. Extract claims from the token. 4. Return an IAM policy to allow or deny the original API Gateway request. ``` On the Descope side of things, you'll need to generate an AWS API Gateway compliant JWT by going to the JWT Templates page in your [Project Settings](https://app.descope.com/settings/project/jwt) and creating an AWS API Gateway template: ![Enable API Gateway compliant JWT in Project Settings](/assets/aws-jwt-template.webp) This will change the `Issuer` claim in the Descope JWT to also contain the full base url of the Descope authentication service, instead of just the Project ID string. Example: `"iss": https://api.descope.com/P2PqjhPcC8Hri2nXUY7f234F` instead of `"iss": P2PqjhPcC8Hri2nXUY7f234F` Once you've done this, you'll need to configure an AWS JWT Authorizer by following the next section's steps closely. ## How to Setup an AWS JWT Authorizer Assuming you've already configured your Routes and have the necessary permissions established for each of your routes in the IAM Console, all you'll need to do from here is create a Lambda Trigger for the JWT 1. **Create a JWT Authorizer**: In the API Gateway console, navigate to the `Authorizers` section and choose `Create New Authorizer`. For the type, select `JWT`. ![Create New Authorizer](/assets/aws-jwt-authorizer.webp) 2. **Input the following JWT Authorizer information (an example is shown below)**: - Name: Can be anything, or you can call it `JWTAuth`. - Input Token Source: Specify where the JWT will come from, usually this is the `Authorization` header, as shown in the photo below. - Issuer & Audience: You need to provide the issuer URL and audience of Descope. The issuer URL is `__BaseURL__/` and the audience is `` ![JWTAuth settings](/assets/aws-jwt-authorizer-settings.webp) 4. **Associate with API Endpoint**: Once the authorizer is created, you can associate it with specific API methods. Whenever these methods are called, the JWT token will be validated by the JWT Authorizer. You do this by "attaching" an authorizer to an API route. ![JWTAuth settings](/assets/aws-jwt-authorizer-settings-1.webp) You can also use additional scopes and include these in your JWT if you want to have app specific scopes for each API route that's configured. ![JWTAuth settings](/assets/aws-jwt-authorizer-settings-2.webp) 5. **Test**: Once everything is set up, test by making an API request with a valid JWT. Ensure that the request is authorized successfully, and invalid or expired tokens are appropriately denied. Once you've set this up, you should be all set. To read more about JWT Authorizers and how they can be used, you can take a look at [this guide](https://aws.amazon.com/blogs/security/how-to-secure-api-gateway-http-endpoints-with-jwt-authorizer/) on the AWS Documentation site. #### Conclusion Being able to use Descope and your custom JWT tokens with all of your current API Gateway infrastructure, is an incredibly powerful tool. With this, you can harness the power of Descope Flows and authentication, with the convenience of API Gateway and the rest of the AWS suite. If you have any other questions about Descope, feel free to reach out to [us](/support)! # Azure API Management (/sessions/validation/jwt-authorizers/azure-jwt-authorizer) Learn how to configure Descope JWTs to work with Azure API Management (APIM). # Using Descope JWTs with Azure API Management By configuring Descope JWTs to work with Azure API Management (APIM), you can protect your backend APIs using secure, validated tokens issued by Descope. This allows you to enforce fine-grained access control and identity verification before traffic reaches your API endpoints. Azure APIM offers a built-in policy called [`validate-jwt`](https://learn.microsoft.com/en-us/azure/api-management/validate-jwt-policy) that you can use to decode and validate JWTs issued by trusted identity providers like Descope. ## Requirements - A Descope project with JWT templates configured. - An Azure API Management instance. - An API published in your APIM instance. ## Step 1: Configure a JWT Template in Descope Go to the [JWT Template](https://app.descope.com/settings/project/jwt) section of the Descope Console and select the **AWS API Gateway** template. This changes the `iss` claim from just the Project ID to a fully qualified URL, such as `"iss": "https://api.descope.com/P2PqjhPcC8Hri2nXUY7f234F"`. ## Step 2: Apply `validate-jwt` Policy in APIM In Azure API Management (APIM), **policies** are a way to control how requests and responses are processed through your APIs. You can apply policies at different levels depending on your needs: * **Product level**: A product is a collection of APIs bundled together (e.g., all internal APIs, all partner APIs). When you apply a policy at the product level, it applies to all APIs and operations in that product. * **API level**: An API in APIM is a logical wrapper for a backend service. Applying a policy at the API level applies it to all operations (routes) within that API. * **Operation level**: An operation is a single endpoint, such as `GET /orders` or `POST /users`. Applying a policy at this level only affects that specific route. If you want to apply JWT validation across all endpoints of an API, use the **API level**. If you need more granular control (e.g., only protect `POST` endpoints), apply the policy at the **operation level**. ### How to Apply the `validate-jwt` Policy To validate Descope JWTs at the **operation level**, follow these steps: 1. Navigate to your Azure API Management instance in the [Azure portal](https://portal.azure.com/). 2. In the left-hand menu, go to **APIs** and select the API you want to protect. 3. From the API design view, select a specific **operation** (e.g., `GET /users`) or click on the API name to apply the policy to all operations. 4. Go to the **Design** tab, then select **Inbound processing**. 5. Click **Add policy**, then choose **Validate JWT** from the policy templates. 6. You can now paste the policy below, or use the policy editor to configure the policy. ![APIM Policy](/assets/azure-apim-policy.webp) Here's a sample `validate-jwt` policy: ```xml __ProjectID__ __BaseURL__/__ProjectID__ ```` Replace `__ProjectID__` with your actual Descope project ID. If you are using a custom domain, or in a region outside of the US, your `openid-config` URL will be different. You can find this under the Discovery URL section of your default [federated OIDC application](https://app.descope.com/applications/descope-default-oidc). Once added, this policy will: * Look for a `Bearer` token in the `Authorization` header. * Use the OpenID configuration from Descope to verify the JWT signature. * Ensure the `aud` (audience) claim matches your Descope project ID. * Reject requests with missing or invalid tokens by returning a `401 Unauthorized` response. This setup allows APIM to handle JWT validation directly at the gateway, reducing the need to repeat validation logic in each backend service. All OIDC-compliant JWTs created with Descope have an `aud` claim that matches the project ID. If you're create JWTs with a custom audience claim, add the other audience claim to the `validate-jwt` policy as well. ## Step 3: Test with a Descope JWT 1. Authenticate a user with Descope and retrieve their JWT. 2. Make an API call to your APIM-protected endpoint, including the JWT in the `Authorization` header: ```bash curl https:///your-api-route \ -H "Authorization: Bearer " ``` If the token is valid, the request will be allowed through. Otherwise, APIM will return a `401 Unauthorized` response. ### Optional: Enforce Roles or Scopes You can add custom claims to your Descope JWT (e.g., `role`, `scope`) and enforce them using the `` tags in the `validate-jwt` policy. ```xml deals:read ``` #### Handling Authorization Claims in Descope JWTs By default, Descope JWTs use a nested `tenants` object to define roles and permissions per tenant. However, Azure API Management's `validate-jwt` policy does **not** currently support evaluating nested JSON structures, which means you cannot validate roles that are nested within the `tenants` object. ```json { "tenants": { "tenant-123": { "roles": ["manager", "editor"], "permissions": ["deals:write"] } } } ``` To validate roles effectively, you can use a different authorization claim format in your JWT template. If you use the [Current tenant, no tenant reference](/management/token/jwt-templates#authorization-claims-configuration) authorization claim format in your JWT template, `roles` will exist in the JWT at directly at the top level (e.g., project-wide roles or tenant-scoped roles with `dct`), and therefore can be validated using your `validate-jwt` policy. This is an example of a JWT with flat roles: ```json { "roles": ["admin", "user"], "permissions": ["deals:read"], "dct": "tenant-123", ... } ``` With this format, you can enforce access by matching the role or permission directly: ```xml admin deals:read ``` You can configure this custom authorization structure via your [JWT Template](https://app.descope.com/settings/project/jwt) settings, in the **Authorization Claims** section. ![JWT Template](/assets/jwt-template-nested-roles.webp) ## Step 3: Consuming JWT Output in the Backend Once `validate-jwt` is configured, you can forward validated token data to your backend by using the `output-token-variable-name` parameter and setting a custom header with the token contents. ### Using Custom Headers to Forward the JWT ```xml @(JsonConvert.SerializeObject(((Jwt)context.Variables["descope-token"]))) ``` This sets a header called `Descope-Token` with the entire validated JWT object as a JSON object. Use hyphenated header names like `Descope-Token`, as underscores or spaces are not supported. #### Conditionally Add Custom Headers If Token Is Present If you only want to set the header if the token is present, you can use the `condition` attribute to check if the token is present. ```xml @(JsonConvert.SerializeObject(((Jwt)context.Variables["descope-token"]))) ``` This ensures the header is only set when token validation is successful. #### Adding Specific Claims from the JWT to Custom Headers The `sub` claim is the subject of the JWT, which is the Descope user ID. If your services need to access specific claims from the JWT, rather than the entire JWT, you can extract them and forward them as custom headers. ```xml @((string)((Jwt)context.Variables["descope-token"]).Claims.GetValueOrDefault("sub")) @((string)((Jwt)context.Variables["descope-token"]).Claims.GetValueOrDefault("email")) ``` This will set the `Descope-Subject` and `Descope-Email` headers with the `sub` and `email` claims from the JWT. ## Creating a Reusable JWT Validation Fragment Azure API Management (APIM) policies are often repeated across different APIs or endpoints — for example, validating a JWT and extracting claims might be needed in multiple places. Rather than copying and pasting the same policy logic into every API or operation, APIM lets you define a reusable block of policy code called a policy fragment. ### Example Fragment You create a fragment called `ValidateJwtFragment` that includes: * A `validate-jwt` policy to check the Descope token. * A few `set-header` policies to forward user claims like `email` and `sub`. Then, in any API, you just include the fragment like this: ```xml __ProjectID__ __BaseURL__/__ProjectID__ @((string)((Jwt)context.Variables["descope-token"]).Claims.GetValueOrDefault("sub")) @((string)((Jwt)context.Variables["descope-token"]).Claims.GetValueOrDefault("email")) ``` This automatically applies all the validation and header logic — without needing to repeat it every time. Once defined, you can include the fragment at the inbound policy section: ```xml ``` This modular approach keeps your JWT validation logic consistent and reusable across multiple APIs or operations. ## Conclusion Using Descope JWTs with Azure API Management enables you to enforce identity-based access control directly at the API gateway layer. By applying a few policy configurations in APIM and issuing structured JWTs from Descope, you can manage access to your APIs without adding custom authentication logic to your backend services. For more information on how to configure Azure API Management policies in general, see the [Azure API Management policies](https://learn.microsoft.com/en-us/azure/api-management/api-management-policies) documentation. # .NET (/sessions/validation/jwt-authorizers/dotnet-jwt-validation) Learn how to validate Descope JWTs through .NET libraries. # Validating JWTs with .NET To maintain secure access to your .NET application, it's essential to validate session tokens for each request. Descope issues these session tokens as [JWTs (JSON Web Tokens)](/sessions), which are signed and can be verified using standard .NET libraries—no need for an external SDK. While the [Descope .NET SDK](https://github.com/descope/descope-dotnet) provides a simple and integrated way to handle session validation, this guide walks through how to manually configure JWT validation using built-in Microsoft packages. This guide is particularly helpful if you're already using Microsoft's JwtBearer middleware with other authentication providers and want to extend it to validate Descope JWTs. It's also ideal for those who prefer not to use third-party SDKs and want full control over the JWT validation process by handling it manually. ## Generating an OIDC-Compliant JWT To ensure your JWTs are OAuth compliant JWT tokens, you'll need to configure an [AWS API Gateway JWT template](/management/token/jwt-templates#user-jwt-templates) for your Descope project. Navigate to the [JWT Templates](https://app.descope.com/settings/project/jwt) section of your **Project Settings** and create a new template based off of the **AWS API Gateway** default template: ![Enable API Gateway compliant JWT in Project Settings](/assets/jwt-template-library.webp) ## JwtBearer Middleware (ASP.NET Core) If you're building an ASP.NET Core application, the [`Microsoft.AspNetCore.Authentication.JwtBearer`](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) package is the recommended way to validate JWTs issued by Descope. You'll also need the [`Microsoft.IdentityModel.Tokens`](https://www.nuget.org/packages/Microsoft.IdentityModel.Tokens) package to configure token validation parameters. Install the required packages using the following commands: ```sh title="Terminal" dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package Microsoft.IdentityModel.Tokens ``` ### Basic Configuration with `AddJwtBearer` For most applications, the default configuration provided by `.AddJwtBearer()` is sufficient. You can define your JWT settings in `appsettings.json`, including: * **Authority**: Your Descope issuer URL (`https://api.descope.com/`) You can find the Issuer URL under the Settings of your default Descope [Federated Application](/identity-federation/applications/oidc-apps) or any other federated application you may be using with your application. * **ValidIssuer**: Same as the Authority * **ValidAudiences**: Your Descope Project ID ```json title="appsettings.json" { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "Authentication": { "Schemes": { "Bearer": { "Authority": "__BaseURL__/", "ValidAudiences": [ "" ], "ValidIssuer": "api.descope.com/" } } } } ``` Then, enable JWT authentication in your app with the following code: ```csharp title="Program.cs" using Microsoft.AspNetCore.Authentication.JwtBearer; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication().AddJwtBearer(); ``` ### Advanced Configuration with `TokenValidationParameters` For more granular control, you can supply custom `TokenValidationParameters` when configuring the JWT middleware. This is useful if you want to customize how issuer, audience, expiration, and other claims are validated. ```json title="appsettings.json" { "Descope": { "Authority": "__BaseURL__/", "Audience": "" } } ``` Then configure your authentication logic in code: ```csharp title="Program.cs" using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); string authority = builder.Configuration["Descope:Authority"]; string audience = builder.Configuration["Descope:Audience"]; builder.Services.AddAuthentication() .AddJwtBearer("Descope", options => { options.Audience = audience; options.Authority = authority; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = authority, ValidateAudience = true, ValidAudiences = new[] { audience }, ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = TimeSpan.FromMinutes(2) }; }); ``` ## Token Validation in .NET Framework If you're working in a .NET Framework environment (or another non-Core framework), you can validate JWTs using the `Microsoft.IdentityModel.Tokens` and `Microsoft.IdentityModel.Protocols.OpenIdConnect` libraries. Install the required packages: ```sh title="Terminal" dotnet add package Microsoft.IdentityModel.Tokens dotnet add package Microsoft.IdentityModel.Protocols.OpenIdConnect ``` Define your token settings in `appsettings.json`: ```json title="appsettings.json" { "Descope": { "Authority": "__BaseURL__/", "Audience": "" } } ``` ### Step 1: Fetch OIDC Signing Keys Use Descope's OIDC discovery endpoint to retrieve the public signing keys: ```csharp title="Program.cs" using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.Extensions.Configuration; var builder = WebApplication.CreateBuilder(args); string authority = builder.Configuration["Descope:Authority"]; string audience = builder.Configuration["Descope:Audience"]; var configManager = new ConfigurationManager( $"{authority}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever(), new HttpDocumentRetriever()); var discoveryDocument = await configManager.GetConfigurationAsync(); var signingKeys = discoveryDocument.SigningKeys; ``` ### Step 2: Validate JWTs Manually Once you've retrieved the signing keys, you can validate a token manually: ```csharp title="TokenValidation.cs" public bool ValidateToken( string token, string issuer, string audience, ICollection signingKeys, out JwtSecurityToken jwt ) { jwt = null; var validationParams = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = issuer, ValidateAudience = true, ValidAudience = audience, ValidateIssuerSigningKey = true, IssuerSigningKeys = signingKeys, RequireExpirationTime = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(2) }; try { var handler = new JwtSecurityTokenHandler(); var principal = handler.ValidateToken(token, validationParams, out SecurityToken validatedToken); jwt = (JwtSecurityToken)validatedToken; return true; } catch (SecurityTokenValidationException) { // Handle invalid token return false; } } ``` # GCP API Gateway (/sessions/validation/jwt-authorizers/gcp-api-gateway) Discover how to integrate Descope JWTs for secure authentication with GCP API Gateway. # Using Descope JWTs with GCP API Gateway JWTs issued by Descope, as an OAuth 2.0-compliant provider, can easily be integrated to work with and protect existing GCP API Gateway endpoints. This integration helps provide a secure way to manage access to your application services. If you're utilizing Descope for authentication and wish to protect your GCP API endpoints using Descope JWT tokens, you can set up a security mechanism in GCP API Gateway to validate these tokens. This involves configuring a service that can verify the JWTs against the expected issuer and audience, as well as the token's signature. The steps below will guide you through the process of making your Descope JWTs compatible with GCP API Gateway, including setting up the necessary components on Google Cloud. This set of instructions is based on an example API which you can create by following along with the steps outlined in this Google [guide](https://cloud.google.com/endpoints/docs/deploy-api) to create an example API. The process should be the same for previously created custom APIs, so if you already have one you can skip to the next section to add the necessary security policies for Descope JWT tokens. ## Setting Up JWT Validation in GCP API Gateway To use Descope JWTs with GCP API Gateway, you need to follow the steps below: 1. Ensure your Descope JWTs provide the correct Issuer URL in them by creating an AWS API Gateway JWT Template in your [Project Settings](https://app.descope.com/settings/project/jwt) to include the necessary claims for GCP compatibility. ![Enable API Gateway compliant JWT in Project Settings](/assets/aws-jwt-template-2.webp) This adjustment ensures the `Issuer` (iss) claim in the Descope JWT aligns with what GCP API Gateway expects, including the project ID and a recognizable issuer URL format. 2. Create an API configuration if you have not already done so. You can follow the steps in the Google [guide](https://cloud.google.com/api-gateway/docs/creating-api-config), in order to do this. 3. Navigate to the Google Cloud Console and define a new security scheme for your API Gateway that specifies JWT validation parameters. You'll need to define this using your own custom [Auth ID](https://cloud.google.com/endpoints/docs/openapi/authenticating-users-custom#configuring_esp_to_support_client_authentication). You can add this to your pre-existing GCP API config. ```sh title="Terminal" securityDefinitions: your_custom_auth_id: authorizationUrl: "__BaseURL__/oauth2/v1/authorize" flow: "implicit" type: "oauth2" # YOUR_DESCOPE_PROJECT_ID should be the Project ID from the Descope Console under Project Settings x-google-issuer: "__BaseURL__/YOUR_DESCOPE_PROJECT_ID" x-google-jwks_uri: "__BaseURL__/YOUR_DESCOPE_PROJECT_ID/.well-known/jwks.json" ``` 4. Finally, add a security section at either the API level to apply to the entire API, or at the method level to apply to a specific method. Make sure these changes are saved and your API is re-deployed. ```sh title="Terminal" security: - your_custom_auth_id: [] ``` Now, you should be able to only make authenticated requests to your API. To verify that the setup is working correctly, you can send a CURL request with a valid Descope JWT in the `Authorization` header, like the following example: ```sh title="Terminal" curl --request POST \ --header "Authorization: Bearer ${DESCOPE_ACCESS_TOKEN}" \ "https://${YOUR_GCP_PROJECT}.appspot.com/airportName?iataCode=SFO" ``` ## Conclusion Integrating Descope JWTs with GCP API Gateway allows you to control access to your APIs effectively, ensuring that only authenticated users through Descope can access your services. For further assistance with Descope or JWTs on GCP, feel free to reach out to [our support team](/support)! # JWT Authorizers (/sessions/validation/jwt-authorizers) This guide is about how to use GCP, AWS, other services with Descope JWTs using the OIDC standard. # JWT Authorizers JWT authorizers provide a streamlined way to validate Descope tokens without using our Backend SDKs directly, especially useful in these scenarios: - **Built-in API gateway integrations**: If you're using API gateways like AWS API Gateway or GCP Cloud Endpoints, you can leverage their native OIDC JWT authorizers to automatically validate session tokens issued by Descope. This eliminates the need for additional custom validation logic or SDK integration - **Interoperability through OIDC**: Since OIDC is an open, widely adopted standard, using OIDC JWT authorizers ensures compatibility with third-party services and simplifies integration across your existing infrastructure In contrast, consider using Descope's [Backend SDKs](/sessions/validation/backend) when your use case requires custom token validation logic related to user management or authorization, that would go beyond standard token signature validation. ## How JWT Authorizers Work When using JWT authorizers with Descope, your application issues tokens that adhere to the OIDC standard. These tokens contain claims that describe the user’s identity and permissions. The token is then passed to a cloud service, like GCP or AWS, which uses its built-in JWT authorizer to validate the token and authorize the request. ### **Key Steps in the Process:** 1. **Token Issuance**: Descope issues a JWT after successful authentication, embedding the necessary claims and metadata. 2. **Token Transmission**: The JWT is sent with the request from the client to your backend or directly to the cloud service. 3. **Token Validation**: The cloud service’s JWT authorizer validates the token, ensuring it’s correctly signed and not expired, and that the audience matches. 4. **Authorization Decision**: Based on the token’s claims, the cloud service decides whether to allow or deny the request. ## Use Cases ### 1. **Serverless Applications** In serverless architectures, where the backend logic is distributed across various services, using a JWT authorizer ensures that each service independently validates tokens without relying on a central backend. ### 2. **Microservices** In a microservices architecture, where different services may be developed and deployed independently, JWT authorizers enable consistent token validation across all services, even if they are written in different languages or hosted on different platforms. ### 3. **API Gateways** API Gateways often include JWT authorizers to secure API endpoints. By using an authorizer with Descope tokens, you can enforce authentication and authorization at the gateway level, ensuring that only valid and authorized requests reach your services. ## Setting Up JWT Authorizers with Descope To set up JWT authorizers with your cloud provider, follow these steps: 1. Configure Descope to issue OIDC-compliant JWTs. You can do this with [JWT Templates](/management/token/jwt-templates). 2. Set up the JWT authorizer in your cloud service (e.g., AWS API Gateway, GCP API Gateway). 3. Specify the token issuer and audience to ensure the authorizer validates Descope tokens correctly. 4. Test the integration to verify that tokens are validated and requests are authorized as expected. Using JWT authorizers with Descope’s OIDC-compliant tokens offers a robust, scalable, and secure method of token validation. By leveraging cloud services to handle this critical function, you can simplify your application’s architecture while adhering to industry standards. # Python FastAPI (/sessions/validation/jwt-authorizers/python-fastapi-jwt-authorizer) Learn how to verify Descope JWTs in a Python FastAPI backend application # Validating JWTs in FastAPI (Python) Before you start, you'll need to make sure you're using a [AWS Gateway JWT template](/management/token/jwt-templates#user-jwt-templates) with your Descope project. That way, your Descope JWTs will be OIDC compliant with the proper issuer and audience claims. This guide is a step-by-step walkthrough of how to implement a custom Descope JWT authorizer in a Python FastAPI application. To implement JWT validation, you'll need a reusable function or callable class that: 1. **Extracts and verifies the JWT** from the request's Authorization header - Returns `401 Unauthorized` if verification fails (missing, malformed, expired, or invalid token) 2. **Enforces required scopes** for scoped routes - Returns `403 Forbidden` if the token lacks necessary scopes This guide shows you how to create those reusable functions and classes, and how to use them to secure your API routes. ## Implementing the Descope JWT Authorizer To validate Descope JWTs, you'll need to fetch the public key from Descope's JWKs endpoint. If you're using a [custom domain](/how-to-deploy-to-production/custom-domain), replace the Base URL below with your own. Your JWKs URL follows this format: ``` https:////.well-known/jwks.json ``` ```python title="auth.py" from jwt import PyJWKClient class TokenVerifier: def __init__(self): # In real apps, load this from a config or environment variable. This is just an example. self.jwks_url = "__BaseURL__/__Project_ID__/.well-known/jwks.json" self.jwks_client = PyJWKClient(self.jwks_url) ``` To fetch the public keys, we'll define a helper method `_get_signing_key()` that wraps `get_signing_key_from_jwt()`. ```python title="auth.py" def _get_signing_key(self, token: str): try: return self.jwks_client.get_signing_key_from_jwt(token).key except Exception as e: raise UnauthorizedException(f"Failed to fetch signing key: {str(e)}") ``` ### Setting Up Custom User-Agent for JWKs Fetching In some cases you may need to configure a **custom `User-Agent` header** before making JWKs requests. This is because the `PyJWKClient` internally uses Python's built-in `urllib.request` to fetch the JWKs. By default, `urllib` sends requests with a `User-Agent` like `Python-urllib/3.x`, which may be flagged or blocked by some CDNs or API gateways (as is the case with Descope's JWKs endpoint). To avoid this, you can globally install a custom opener that adds a more typical User-Agent: ```python title="main.py" import urllib.request # Set a custom User-Agent to avoid being blocked by security filters or rate limiters. opener = urllib.request.build_opener() opener.addheaders = [('User-agent', 'Mozilla/5.0 (DescopeFastAPISampleApp)')] urllib.request.install_opener(opener) ``` This ensures JWKs requests are treated as legitimate traffic and not blocked as bot or scanner activity. It's recommended to place this in your `main.py` or app startup script before any JWT validation occurs. ## Validating the JWTs with the `TokenVerifier` Once we've fetched the `TokenVerifier` class and the `_get_signing_key()` method, the next step is to decode and validate the JWT. We'll implement a `__call__` method inside our `TokenVerifier` class to handle this process. #### Extracting the Token from Incoming Requests FastAPI provides a built-in way to extract and parse access tokens using the `HTTPBearer()` dependency. This reads the Bearer token from the HTTP authorization header from the incoming request and passes it into your function as an `HTTPAuthorizationCredentials` object. First, you can define the custom exceptions that will be used for error handling: ```python title="exceptions.py" from fastapi import HTTPException, status class UnauthenticatedException(HTTPException): def __init__(self): super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required") class UnauthorizedException(HTTPException): def __init__(self, detail: str = "Not authorized"): super().__init__(status_code=status.HTTP_403_FORBIDDEN, detail=detail) ``` Next you can implement the `__call__` method, which will be invoked by FastAPI whenever a protected route is accessed, to validate the incoming token. You can find the Issuer URL under your default federated app settings [here](https://app.descope.com/applications/descope-default-oidc). ```python title="auth.py" from typing import Optional import jwt # From PyJWT from fastapi import Depends, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer, SecurityScopes from app.exceptions import UnauthenticatedException, UnauthorizedException async def __call__( self, # token injected by FastAPI Security, specified in the FastAPI route definition token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer()) ): if token is None: raise UnauthenticatedException token = token.credentials key = self._get_signing_key(token) payload = self._decode_token(token, key) return payload # helper which calls jwt.decode() def _decode_token(self, token: str, key): try: return jwt.decode( token, key, algorithms=['RS256'], # modify if using different algorithm(s) # `issuer` is the expected value of the `iss` claim in the JWT. # It helps verify that the token was actually issued by a trusted entity # (e.g., your authentication backend, auth server, or token provider). # If the token's `iss` does not match the expected value, the token is rejected. issuer="__BaseURL__/__Project_ID__/.well-known/openid-configuration", # Recommended: load from config, e.g., self.config.issuer # `audience` is the expected recipient of the token — usually your API or backend service. # This should match the `aud` claim in the token. # It ensures the token was intended to be used by your application, not another system. audience='my-api-audience' # Recommended: load from config # You may also add additional claims to validate here using the `options` argument, # or by manually inspecting the decoded payload after this step. # For example: enforce `sub`, `azp`, or custom claims depending on your app logic. ) except Exception as e: raise UnauthorizedException(f"Token decoding failed: {str(e)}") ``` ## Enforcing Scopes This section is optional. This is only necessary if you want to enforce scoped-based access control on your API routes. In addition to validating your Descope access tokens in your FastAPI backend, you may also want to restrict access to specific API routes based on scopes/claims embedded in the JWT. You can read more scoping generally in our [Inbound Apps](/identity-federation/inbound-apps/developing-apis#1-enforce-scope-based-access-control) docs page. To do this, you can write a helper method `_enforce_scopes()`, that checks whether the token's scope claim contains all the required scopes for the API route. If any are missing, the request is will be deined with a `403 Forbidden` error, and optionally will include the missing scopes in the error message returned to the client. ```python title="auth.py" from typing import List def _enforce_scopes(self, payload: dict, required_scopes: List[str]): scope_claim = payload.get("scope") if scope_claim is None: raise UnauthorizedException('Missing required claim: "scope"') # Scopes may be a space-separated string or a list scopes = scope_claim.split() if isinstance(scope_claim, str) else scope_claim missing = [scope for scope in required_scopes if scope not in scopes] if missing: raise UnauthorizedException( f'Missing required scopes: {", ".join(missing)}' ) ``` Now, let's complete our `__call__` function, which should now accept the `SecurityScopes` parameter: The `SecurityScopes` parameter is automatically injected by FastAPI when you use the `Security()` dependency. It contains information about what scopes are required for the current route, allowing your authorizer to enforce scope-based access control dynamically. ```python title="auth.py" from typing import Optional from fastapi import Depends, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, SecurityScopes async def __call__( self, security_scopes: SecurityScopes, token: Optional[HTTPAuthorizationCredentials] = Depends(HTTPBearer()) ): if token is None: raise UnauthenticatedException token = token.credentials key = self._get_signing_key(token) payload = self._decode_token(token, key) # Enforce required scopes if security_scopes.scopes: self._enforce_scopes(payload, security_scopes.scopes) return payload ``` ## Protecting Routes Using the `TokenVerifier` With the `TokenVerifier` fully implemented, you can use it to secure any route in your FastAPI application. ### Making a Route Private To protect a route with authentication (i.e., require a valid JWT), use the `TokenVerifier` as a dependency: ```python title="main.py" from app.auth import TokenVerifier auth = TokenVerifier() @app.get("/api/private") def private(auth_result: str = Security(auth)): # This API is now protected by our TokenVerifier object `auth` return auth_result ``` #### Adding Scope Validation to Private Routes For more fine-grained access control via scopes, you can declare required scopes directly on the route. This tells FastAPI to inject the decoded JWT token into the route, and enforce that it contains the `read:messages` and `write:messages` scopes: ```python title="main.py" from app.auth import TokenVerifier auth = TokenVerifier() # if not already defined @app.get("/api/private-scoped/write") def private_scoped(auth_result: str = Security(auth, scopes=['read:messages', 'write:messages'])): """ This is a protected route with scope-based access control. Access to this endpoint requires: - A valid access token (authentication), and - The presence of the `read:messages` and `write:messages` scope in the token. """ return auth_result ``` # Backend SDKs (/auth-methods/auth-apps/with-sdks/backend) Add TOTP authenticator apps to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Authenticator Apps (TOTP) via Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/auth-apps/with-sdks/client). Descope supports validating sign-up and sign-ins via Authenticator Applications which provide a Time-based One-time Password (TOTP). Google Authenticator, Microsoft Authenticator, and Authy are examples of authenticator apps. Descope generates the required QR code or key (also called a secret or seed) in order to configure new a new Authenticator. ## User Sign-Up The first step for implementing TOTP authentication is sign-up. In this step the user registers their TOTP app with the authentication service. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full TOTP sign-up flow has been successfully completed. ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com" const resp = await descopeClient.totp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize TOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized TOTP signup.") console.log(resp.data) } ``` ```python # Args: # user: Optional user object to populate new user information. user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # login_id: email or phone - becomes the login_id or the user from here on and also used for delivery login_id = "email@company.com" try: resp = descope_client.totp.sign_up(login_id=login_id, user=user) print ("Successfully initialized TOTP signup.") print (resp) except AuthException as error: print ("Failed to initialize TOTP signup") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email, phone or any other unique value - becomes the loginID for the user from here on loginID := "email@company.com" // user: Optional user object to populate new user information. user := &auth.User{Name:"Joe", Email:"email@company.com", Phone:"+15555555555"} resp, err := descopeClient.Auth.TOTP.SignUp(ctx, loginID, user) if (err != nil){ fmt.Println("Failed to initialize TOTP signup: ", err) } else { fmt.Println("Successfully initialized TOTP signup: ", resp) } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com" User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); TOTPService ts = descopeClient.getAuthenticationServices().getTOTPService(); try { TOTPResponse resp = ts.signUp(loginId, user); } catch (DescopeException de) { // Handle the error } // Use one of the provided options to have the user add their credentials to the authenticator // resp.getProvisioningURL() // resp.getImage() // resp.getKey() ``` ```ruby # Every user must have a login ID. All other user information is optional email = 'desmond@descope.com' user = {name: 'Desmond Copeland', phone: '+15555555555', email: 'someone@example.com'} totp_response = descope_client.totp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::EMAIL , login_id: 'someone@example.com', user: user) # Use one of the provided options to have the user add their credentials to the authenticator provisioning_url = totp_response['provisioningURL'] image = totp_response['image'] key = totp_response['key'] ``` ```csharp // Args: // LoginId (string): The login ID of the user being signed up // User (SignUpUser) optional: Additional user metadata // SsoAppId (string) optional: Associate sign-up with a specific Federated App var response = await client.Auth.V1.Auth.Totp.Signup.PostAsync( new TOTPSignUpRequest { LoginId = "email@company.com", User = new SignUpUser { Name = "Joe Person", Phone = "+15555555555", Email = "email@company.com" }, SsoAppId = "my-sso-app-id" }); // response is a TOTPResponse containing: // response.ProvisioningURL — add to authenticator app via URL // response.Image — QR code image (base64) to display to user // response.Key — raw TOTP secret key ``` ## User Sign-In / Verify For signing in, your application client must prompt the user for loginId, such as email or phone, and the code from the authenticator application. Your client will then call the `verify` function. Upon successful verification, the user will be logged in and the response will include the JWT information. ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com" // code: code entered by the user from the authenticator application. const code = "xxxx" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.totp.verify(loginId, code, loginOptions); if (!resp.ok) { console.log("Failed to Sign-In via TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data)) } ``` ```python # Args: # login_id: email or phone - must be same as provided at the time of signup. login_id = "email@company.com" # code: code entered by the user from the authenticator application. code = "xxxx" # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa refresh_token = "xxxx" # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.totp.sign_in_code(login_id=login_id, code=verify_code, login_options=login_options, refresh_token=refresh_token, audience=audience) print ("Successfully signed in via TOTP.") print (resp) except AuthException as error: print ("Failed to Sign-In via TOTP") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: mail or phone - must be same as provided at the time of signup. loginID := "email@company.com" // code (string): code entered by the user from the authenticator application. code := "xxxxxx" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation authInfo, err := descopeClient.Auth.TOTP.SignInCode(ctx, loginID, code, r, loginOptions, w) if (err != nil){ fmt.Println("Failed to Sign-In via TOTP: ", err) } else { fmt.Println("Successfully signed in via TOTP: ", authInfo) } ``` ```java // The optional `w http.ResponseWriter` adds the session and refresh cookies to the response automatically. // Otherwise they're available via authInfo TOTPService ts = descopeClient.getAuthenticationServices().getTOTPService(); var loginOptions = LoginOptions.builder() var loginOptions = LoginOptions.builder() .stepUp(true) .mfa(true) .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); var code = "xxx"; try { AuthenticationInfo info = ts.signInCode(loginId, code, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby jwt_response = descope_client.totp_sign_in_code( login_id: 'someone@example.com', code: '123456' # Code from authenticator app ) session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```csharp // Args: // LoginId (string): The login ID of the user — must match the value used at sign-up // Code (string): The code from the user's authenticator app // LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims var response = await client.Auth.V1.Auth.Totp.Verify.PostAsync( new TOTPVerifyCodeRequest { LoginId = "email@company.com", Code = "xxxxxx", LoginOptions = new LoginOptions { /* ... */ } }); ``` ## Update User The update user call is used when you would like to associate a new authenticator method with an existing and authenticated user. You need to pass the refresh token or http request of an authenticated user. The update will work only if the user is authenticated. ```javascript // Args: // loginId: email, phone or username of the authenticated user const loginId = "email@company.com" // refresh_token: string with the refresh token of the user. This should be extracted from cookies sent with the query. const refreshToken = "xxxxxxxx" const resp = await descopeClient.totp.update(loginId, refreshToken) if (!resp.ok) { console.log("Failed to initialized updating user's TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized updating user's TOTP: " + resp.data) } ``` ```python # Args: # login_id: email, phone or username for the authenticated user. login_id = "email@company.com" # refresh_token: string with the refresh token of the user. This should be extracted from cookies sent with the query. refresh_token: "xxxxxxx" try: resp = descope_client.totp.update_user(login_id=login_id, refresh_token=verify_code) print ("Successfully initialized updating user's TOTP.") print (resp) except AuthException as error: print ("Failed to initialized updating user's TOTP") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email, phone or username of the authenticated user loginID := "email@company.com" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. resp, err := descopeClient.Auth.TOTP.UpdateUser(ctx, loginID, r) if (err != nil){ fmt.Println("Failed to initialized updating user's TOTP: ", err) } else { fmt.Println("Successfully initialized updating user's TOTP: ", resp) } ``` ```java // loginId: email, phone or username of the authenticated user String loginId = "email@company.com"; // refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query. String refreshToken = "xxxxxxx" TOTPService ts = descopeClient.getAuthenticationServices().getTOTPService(); try { AuthenticationInfo info = ts.updateUser(loginId, refreshToken); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.totp_add_update_key(login_id: login_id, refresh_token: refresh_token) ``` ```csharp // Args: // LoginId (string): The login ID of the authenticated user // refreshJwt (string): The session's refresh token (required — user must be authenticated) var response = await client.Auth.V1.Auth.Totp.Update.PostWithJwtAsync( new TOTPUpdateRequest { LoginId = "email@company.com" }, refreshJwt); // response is a TOTPResponse containing the new provisioning URL, QR image, and key ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/auth-apps/with-sdks/client) Add TOTP authenticator apps to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Authenticator Apps (TOTP) via Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. Descope supports validating sign-up and sign-ins via Authenticator Applications which provide a Time-based One-time Password (TOTP). Google Authenticator, Microsoft Authenticator, and Authy are examples of authenticator apps. Descope generates the required QR code or key (also called a secret or seed) in order to configure new a new Authenticator. ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### User Sign-Up The first step for implementing TOTP authentication is sign-up. In this step the user registers their TOTP app with the authentication service. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full TOTP sign-up flow has been successfully completed. ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com" const descopeSdk = useDescope(); const resp = await descopeSdk.totp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize TOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized TOTP signup.") console.log(resp.data) } ``` ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com" const resp = await descopeSdk.totp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize TOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized TOTP signup.") console.log(resp.data) } ``` ```html ``` ### User Sign-In / Verify For signing in, your application client must prompt the user for loginId, such as email or phone, and the code from the authenticator application. Your client will then call the `verify` function. Upon successful verification, the user will be logged in and the response will include the JWT information. ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com" // code: code entered by the user from the authenticator application. const code = "xxxx" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.totp.verify(loginId, code, loginOptions); if (!resp.ok) { console.log("Failed to Sign-In via TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data)) } ``` ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com" // code: code entered by the user from the authenticator application. const code = "xxxx" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.totp.verify(loginId, code, loginOptions); if (!resp.ok) { console.log("Failed to Sign-In via TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data)) } ``` ```html ``` ### Update User The update user call is used when you would like to associate a new authenticator method with an existing and authenticated user. You need to pass the refresh token or http request of an authenticated user. The update will work only if the user is authenticated. ```javascript // Args: // loginId: email, phone or username of the authenticated user const loginId = "email@company.com" // refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query. const refreshToken = "xxxxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.totp.update(loginId, refreshToken) if (!resp.ok) { console.log("Failed to initialized updating user's TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized updating user's TOTP: " + resp.data) } ``` ```javascript // Args: // loginId: email, phone or username of the authenticated user const loginId = "email@company.com" // refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query. const refreshToken = "xxxxxxxx" const resp = await descopeSdk.totp.update(loginId, refreshToken) if (!resp.ok) { console.log("Failed to initialized updating user's TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized updating user's TOTP: " + resp.data) } ``` ```html ``` # Mobile SDKs (/auth-methods/auth-apps/with-sdks/mobile) Add TOTP authenticator apps to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Authenticator Apps (TOTP) via Mobile SDKs Descope supports validating sign-up and sign-ins via Authenticator Applications which provide a Time-based One-time Password (TOTP). Google Authenticator, Microsoft Authenticator, and Authy are examples of authenticator apps. Descope generates the required QR code or key (also called a secret or seed) in order to configure new a new Authenticator. ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## User Sign-Up The first step for implementing TOTP authentication is sign-up. In this step the user registers their TOTP app with the authentication service. Descope will generate a TOTP key (also called a secret or seed) that will be entered into the end user's authenticator app so that TOTP codes can be successfully verified. The new end user will be registered after the full TOTP sign-up flow has been successfully completed. ```swift // Args: // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery let loginId = "email@company.com" // user: Optional user object to populate new user information. let user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} do { let totpResponse = try await Descope.totp.signUp(loginId: loginId, user: user) print("Successfully initiated TOTP Sign Up") print("TOTP QR Code: Returned as a UIImage within totpResponse.image") print("TOTP Key: " + totpResponse.key) print("TOTP Provisioning URL: " + totpResponse.provisioningURL) } catch { print("Failed to initiate TOTP Sign Up") print(error) } ``` ```kotlin // Args: // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery // details: Optional user details object to populate new user information. val details = SignUpDetails( name = "firstName lastName", email = "email@company.com", phone = "+15555555555", givenName = "firstName", middleName = "middleName", familyName = "lastName" ) try { Descope.totp.signUp(loginId = "email@company.com", details = details) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com"; // details: Optional user details object to populate new user information. final details = SignUpDetails(name: "Joe Person"); final totpResponse = await Descope.totp.signUp(loginId: loginId); // Use one of the provided options to have the user add their credentials to the authenticator // totpResponse.provisioningUrl // totpResponse.image; // totpResponse.key ``` ``` javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com" const descopeSdk = useDescope(); const resp = await descopeSdk.totp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize TOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized TOTP signup.") console.log(resp.data) } ``` ## User Sign-In / Verify For signing in, your application client must prompt the user for loginId, such as email or phone, and the code from the authenticator application. Your client will then call the `verify` function. Upon successful verification, the user will be logged in and the response will include the JWT information. ```swift // Args: // loginId: email or phone - must be same as provided at the time of signup. let loginId = "email@company.com" // code: code entered by the user from the authenticator application. let code = "xxxx" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { let descopeSession = try await Descope.totp.verify(loginId: loginId, code: code, options: signInOptions) print("Successfully verified TOTP Code") print(descopeSession as Any) } catch { print("Failed to verify TOTP Code") print(error) } ``` ```kotlin // Args: // loginId: email or phone - must be same as provided at the time of signup. // code: code entered by the user from the authenticator application. try { Descope.totp.verify(loginId = "email@company.com", code = "xxxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com"; // code: code entered by the user from the authenticator application. const code = "xxxx"; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); final authResponse = await Descope.totp.verify(loginId: loginId, code: code, options: options); ``` ``` javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com" // code: code entered by the user from the authenticator application. const code = "xxxx" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.totp.verify(loginId, code, loginOptions); if (!resp.ok) { console.log("Failed to Sign-In via TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via TOTP. " + JSON.stringify(resp.data)) } ``` ## Update User The update user call is used when you would like to associate a new authenticator method with an existing and authenticated user. You need to pass the refresh token or http request of an authenticated user. The update will work only if the user is authenticated. ```swift // Args: // loginId: email, phone or username of the authenticated user let loginId = "email@company.com" // refresh_token: string with the refresh token of the user. This should be extracted from cookies sent with the query. let refresh_token = "xxxxxxxx" do { let totpResponse = try await Descope.totp.update(loginId: loginId, refreshJwt: descopeSession!.refreshJwt) print("Successfully initiated TOTP Update") print("TOTP QR Code: Returned as a UIImage within totpResponse.image") print("TOTP Key: " + totpResponse.key) print("TOTP Provisioning URL: " + totpResponse.provisioningURL) } catch { print("Failed to initiate TOTP Update") print(error) } ``` ```kotlin // Args: // loginId: email, phone or username of the authenticated user // refresh_token: string with the refresh token of the user. This should be extracted from cookies sent with the query. try { Descope.totp.update(loginId = "email@company.com", refreshJwt = "xxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email, phone or username of the authenticated user const loginId = "email@company.com"; // refresh_token: string with the refresh token of the user. This should be extracted from cookies sent with the query. const refresh_token = "xxxxxxxx"; Descope.totp.update(loginId: loginId, refreshJwt: refresh_token); ``` ``` javascript // Args: // loginId: email, phone or username of the authenticated user const loginId = "email@company.com" // refreshToken: string with the refresh token of the user. This should be extracted from cookies sent with the query. const refreshToken = "xxxxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.totp.update(loginId, refreshToken) if (!resp.ok) { console.log("Failed to initialized updating user's TOTP") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized updating user's TOTP: " + resp.data) } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # Backend SDKs (/auth-methods/embedded-link/with-sdks/backend) Add embedded link authentication to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Embedded Link via Backend SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. An embedded link generates a single-use token for authenticating an existing user. Once generated, the embedded link token can be sent to a user via various use cases such as email, SMS, etc, or you can use it manually similarly to a machine-to-machine implementation. Embedded link tokens are verified using the magic link verification function. ## Generate Embedded Link When authenticating via embedded link, you first need to generate the embedded link token. The code below shows how to generate the embedded link token. Also note that signup is not complete without the user verification step below. ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // customClaims: Additional claims to place on the jwt after verification const customClaims = {"Key1": "Value1"} const resp = await descopeClient.management.user.generateEmbeddedLink(loginId, customClaims); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") const token = resp.data.token; console.log("Token " + token) } ``` ```python # Args: # login_id: email - becomes the login_id for the user from here on and also used for delivery login_id = "email@company.com" # custom_claims: Additional claims to place on the jwt after verification custom_claims = {"Key1": "Value1"} try: token = descope_client.mgmt.user.generate_embedded_link(login_id=login_id, custom_claims=custom_claims) print ("Successfully initialized signup flow") print ("Token: " + str(token)) except AuthException as error: print ("Failed to initialize signup flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery loginID := "email@company.com" // customClaims: Additional claims to place on the jwt after verification customClaims := map[string]any{"key1":"value1"} token, err := descopeClient.Management.User().GenerateEmbeddedLink(ctx, loginID, customClaims) if (err != nil){ fmt.Println("Failed to initialize signup flow: ", err) } else { fmt.Println("Successfully initialized signup flow, token: ", token) } ``` ```java // Args: // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery String loginId = "email@company.com"; // customClaims: Additional claims to place on the jwt after verification Map customClaims = new HashMap() {{ put("custom-key1", "custom-value1");}} UserService us = descopeClient.getManagementServices().getUserService(); try { String token - us.generateEmbeddedLink("desmond@descope.com", customClaims); } catch (DescopeException de) { // Handle the error } ``` ```ruby token = descope_client.generate_embedded_link(login_id: 'desmond@descope.com', custom_claims: {'key1':'value1'}) ``` ```csharp // Args: // loginId (string): The login ID of the existing user to generate a token for // customClaims (Dictionary) optional: Additional claims to place on the jwt after verification // timeout (int) optional: Token expiry time in seconds (defaults to project setting) var loginId = "email@company.com"; var customClaims = new Dictionary { { "Key1", "Value1" } }; try { var token = await descopeClient.Management.User.GenerateEmbeddedLink(loginId: loginId, customClaims: customClaims, timeout: 60); Console.WriteLine("Token: " + token); } catch (DescopeException ex) { // Handle the error } ``` ## Embedded Link Verification Once the embedded token has been generated, you can send to a user via various use cases such as email, SMS, etc, or you can use it manually similarly to a machine-to-machine implementation. Embedded link tokens are verified using the magic link verification function. Below are examples of verifying the token utilizing the backend SDKs. ```javascript // Args: // token: generated embedded link token const token = "xxxx" const resp = await descopeClient.magicLink.verify(token) if (!resp.ok) { console.log("Failed to verify user") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified user") } ``` ```python # Args: # token: generated embedded link token token = "xxxx" try: resp = descope_client.magiclink.verify(token=token) print ("Successfully verified user") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to verify user") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // token: generated embedded link token token := "xxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation err := descopeClient.Auth.MagicLink().Verify(ctx, token, w) if (err != nil){ fmt.Println("Failed to verify user: ", err) } else { fmt.Println("Successfully verified user") } ``` ```java // Args: // token: generated embedded link token String token = "xxxx" MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { AuthenticationInfo info = mls.verify(token); } catch (DescopeException de) { // Handle the error } ``` ```ruby # To verify an embedded link, your redirect page must call the validation function on the token (t) parameter (https://your-redirect-address.com/verify?t=): jwt_response = descope_client.magiclink_verify_token('token-here') ``` ```csharp // Args: // token (string): The embedded link token to verify var token = "xxxx"; try { var authInfo = await descopeClient.MagicLink.Verify(token); } catch (DescopeException ex) { // Handle the error } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Backend SDKs (/auth-methods/magic-link/with-sdks/backend) Add magic link authentication to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Magic Link via Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/magic-link/with-sdks/client). A Magic Link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service can send Magic Links via email or SMS texts. The browser tab that is opened after clicking the Magic Link gets the authenticated session cookies. For example, consider a user that starts the login process on a laptop browser and gets a Magic Link delivered to their email inbox. When they click the email link, a new browser tab will open and they will be logged in on the new tab. ![The Magic Link flow within Descope for backend SDK](/assets/magic-link-flow-backend.webp) Consider using Magic Links when your users typically use only one device to access your application, and when opening new tabs is not a big inconvenience. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/magic-link/with-sdks/backend#user-sign-up) then [User Verification](/auth-methods/magic-link/with-sdks/backend#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/magic-link/with-sdks/backend#user-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/backend#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/magic-link/with-sdks/backend#user-sign-up-or-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/backend#user-verification) ## User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. In this sample code, the Magic Link will be sent by email to "email@company.com". To change the delivery method to send the Magic Link as a text, you would change the delivery_method to sms within the below example. Also note that signup is not complete without the user verification step below. ```javascript // Args: // user: Optional user object to populate new user information. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify-magic-link" // deliveryMethod: Delivery method to use to send Magic Link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.magicLink.signUp[deliveryMethod](loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```python # Args: # user: Optional user object to populate new user information. user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # login_id: email or phone - becomes the loginId for the user from here on and also used for delivery login_id = "email@company.com" # delivery_method: Method used to deliver the Magic Link. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' uri = "http://auth.company.com/api/verify-magic-link" # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: resp = descope_client.magiclink.sign_up(method=delivery_method, login_id=login_id, uri=uri, user=user, signup_options=signup_options) print ("Successfully initialized signup flow") print (resp) except AuthException as error: print ("Failed to initialize signup flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Method used to deliver the MagicLink. Supported delivery methods - descope.MethodEmail or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify-magic-link" // user: Optional user object to populate new user information. user := &descope.User{Name:"Joe", Email:"email@company.com", Phone:"+15555555555"} // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } err := descopeClient.Auth.MagicLink().SignUp(ctx, deliveryMethod, loginID, URI, user, signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup flow: ", err) } else { fmt.Println("Successfully initialized signup flow") } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { String uri = "http://auth.company.com/api/verify-magic-link"; String maskedAddress = mls.signUp(DeliveryMethod.EMAIL, loginId, uri, user, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // loginId (string): Becomes the user's loginId and the address used for delivery var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI to override the console setting var redirectUrl = "http://auth.company.com/api/verify-magic-link"; // request (MagicLinkSignUpEmailRequest): For SMS / WhatsApp, use MagicLinkSignUpPhoneRequest // and call Signup.Sms / Signup.Whatsapp instead of Signup.Email. var request = new MagicLinkSignUpEmailRequest { LoginId = loginId, Email = loginId, RedirectUrl = redirectUrl, TemplateId = "magic-link-template", User = new SignUpUser { Name = "Joe Person", Email = loginId, Phone = "+15555555555", }, }; var response = await descopeClient.Auth.V1.Magiclink.Signup.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; ``` ```ruby email = 'desmond@descope.com' user = {'name': 'Desmond Copeland', 'phone': '+15555555555', 'email': email} masked_address = descope_client.magiclink_sign_up( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'desmond@descope.com', uri: 'https://myapp.com/verify-magic-link', # Set redirect URI here or via console user: user ) ``` ## User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the Magic Link will be sent by email to "email@company.com". Also note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify-magic-link" // deliveryMethod: Delivery method to use to send Magic Link. Supported values include "email" or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const refreshToken = "xxxxx" const resp = await descopeClient.magicLink.signIn[deliveryMethod](loginId, uri, loginOptions, refreshToken); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```python # Args: # email or phone to use for delivery login_id = "email@company.com" # delivery_method: support delivery methods - DeliveryMethod.SMS or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' uri = "http://auth.company.com/api/verify-magic-link" # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa refresh_token = "xxxxx" try: resp = descope_client.magiclink.sign_in(method=delivery_method, login_id=login_id, uri=uri, login_options=login_options, refresh_token=refresh_token) print ("Successfully initialized signin flow") except AuthException as error: print ("Failed to initialize signin flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send Magic Link. Supported values include descope.MethodEmail or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify-magic-link" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } err := descopeClient.Auth.MagicLink().SignIn(ctx, deliveryMethod, loginID, URI, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize signin flow: ", err) } else { fmt.Println("Successfully initialized signin flow") } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var loginOptions = LoginOptions.builder() .stepUp(true) .mfa(true) .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { String uri = "http://auth.company.com/api/verify-magic-link"; var request = HttpRequest.newBuilder() .build(); String maskedAddress = mls.signIn(DeliveryMethod.EMAIL, loginId, uri, request, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // loginId (string): User's login identifier var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI configured in your application for handling the magic link var redirectUrl = "http://auth.company.com/api/verify-magic-link"; // request (MagicLinkSignInRequest): For SMS / WhatsApp, call Signin.Sms / Signin.Whatsapp instead. var request = new MagicLinkSignInRequest { LoginId = loginId, RedirectUrl = redirectUrl, LoginOptions = new LoginOptions { Stepup = false, Mfa = false, }, }; var response = await descopeClient.Auth.V1.Magiclink.Signin.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; ``` ```ruby masked_address = descope_client.magiclink_sign_in( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'desmond@descope.com', uri: 'https://myapp.com/verify-magic-link' # Set redirect URI here or via console ) ``` ## User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the Magic Link will be sent by email to "email@company.com". To change the delivery method to send the Magic Link as a text, you would change the `delivery_method` to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify-magic-link" // deliveryMethod: Delivery method to use to send Magic Link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.magicLink.signUpOrIn[deliveryMethod](loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```python # Args: # login_id: email or phone to use for delivery login_id = "email@company.com" # delivery_method: support delivery methods - DeliveryMethod.SMS or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' uri = "http://auth.company.com/api/verify-magic-link" # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: resp = descope_client.magiclink.sign_up_or_in(method=delivery_method, login_id=login_id, uri=uri, signup_options=signup_options) print ("Successfully initialized signup or in flow") except AuthException as error: print ("Failed to initialize signup or in flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send Magic Link. Supported values include descope.MethodEmail or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify-magic-link" // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } err := descopeClient.Auth.MagicLink().SignUpOrIn(ctx, deliveryMethod, loginID, URI, signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup or in flow: ", err) } else { fmt.Println("Successfully initialized signup or in flow") } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { String uri = "http://auth.company.com/api/verify-magic-link"; String maskedAddress = mls.signUpOrIn(DeliveryMethod.EMAIL, loginId, uri, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // loginId (string): User's login identifier var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI to override the console setting var redirectUrl = "http://auth.company.com/api/verify-magic-link"; // request (MagicLinkSignInRequest): Signup-or-signin payload (same shape as sign-in). // For SMS / WhatsApp, call SignupIn.Sms / SignupIn.Whatsapp instead. var request = new MagicLinkSignInRequest { LoginId = loginId, RedirectUrl = redirectUrl, }; var response = await descopeClient.Auth.V1.Magiclink.SignupIn.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; ``` ```ruby masked_address = descope_client.magiclink_sign_up_or_in( method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: 'desmond@descope.com', uri: 'https://myapp.com/verify-magic-link', # Set redirect URI here or via console ) ``` ## User Verification Once a user clicks the Magic Link, your application must call the `verify` function. This means that this function needs to be called from your application when the user clicks the Magic Link. The function call will return all the the necessary JWT tokens and claims and user information in the `resp` dictionary. The `sessionJwt` within the `resp` is needed for session validation. ```javascript // Args: // token: URL parameter containing the Magic Link token for example, https://auth.yourcompany.com/api/verify-magic-link?t=token. const token = "xxxx" const resp = await descopeClient.magicLink.verify(token) if (!resp.ok) { console.log("Failed to verify Magic Link token") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified Magic Link token") } ``` ```python # Args: # token: URL parameter containing the Magic Link token for example, https://auth.yourcompany.com/api/verify-magic-link?t=token. token = "xxxx" # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.magiclink.verify(token=token, audience=audience) print ("Successfully verified user") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Failed to verify user") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // token: URL parameter containing the Magic Link token for example, https://auth.yourcompany.com/api/verify-magic-link?t=token. token := "xxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation err := descopeClient.Auth.MagicLink().Verify(ctx, token, w) if (err != nil){ fmt.Println("Failed to verify user: ", err) } else { fmt.Println("Successfully verified user") } ``` ```java MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { AuthenticationInfo info = mls.verify(token); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // token (string): The magic link token received as the `t` query parameter var token = "xxxx"; // request (VerifyMagicLinkRequest): Verify payload with the token from the magic link URL var request = new VerifyMagicLinkRequest { Token = token, }; var authInfo = await descopeClient.Auth.V1.Magiclink.Verify.PostAsync(request); var sessionJwt = authInfo?.SessionJwt; var refreshJwt = authInfo?.RefreshJwt; ``` ```ruby # To verify a Magic Link, your redirect page must call the validation function on the token (t) parameter (https://your-redirect-address.com/verify?t=): jwt_response = descope_client.magiclink_verify_token('token-here') session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ## Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the Magic Link, you will need to host a page to verify the Magic Link token using the [Magic Link Verify Function](/auth-methods/magic-link/with-sdks/backend#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"}, "templateId": "template-id", "providerId": "provider-id" } const resp = await descopeClient.magicLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start Magic Link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started Magic Link email update") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user being updated login_id = "email@company.com" # email (str): The new email address. If an email address already exists for this end user, it will be overwritten email = "newEmail@company.com" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxx" # add_to_login_ids (boolean): if true, the email will be appended to the login ID array. add_to_login_ids = True # on_merge_use_existing (boolean): if true, on merge, the existing user's information (roles, tenants, etc) will be retained on_merge_use_existing = True # template_options (dict): email template options template_options = {"option": "Value1"} # template_id (str): The ID of the messaging template to use template_id = "template-id" # provider_id (str): The ID of the messaging provider to use provider_id = "provider-id" try: jwt_response = descope_client.magiclink.update_user_email(login_id=login_id, email=email, refresh_token=refresh_token, add_to_login_ids=add_to_login_ids, on_merge_use_existing=on_merge_use_existing, template_options=template_options, template_id=template_id, provider_id=provider_id) print ("Successfully started Magic Link email update") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to start Magic Link email update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginId (str): The loginId of the user being updated loginID := "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten email := "newEmail@company.com" // updateOptions (&descope.UpdateOptions): this allows you to configure behavior during the authentication process. updateOptions := &descope.UpdateOptions{} updateOptions.AddToLoginIDs = false updateOptions.OnMergeUseExisting = false updateOptions.TemplateOptions = map[string]any{"option": "Value1"} updateOptions.TemplateID = "template-id" updateOptions.ProviderID = "provider-id" // request (*http.Request): Request is needed to obtain JWT and send it to Descope, for verificatio res, err := descopeClient.Auth.MagicLink().UpdateUserEmail(ctx, loginID, email, updateOptions, request) if (err != nil){ fmt.Println("Failed to start Magic Link email update: ", err) } else { fmt.Println("Successfully started Magic Link email update", res) } ``` ```java // Will throw DescopeException if there is an error with update MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { AuthenticationInfo info = mls.updateUserEmail(loginId, email, refreshToken, UpdateOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.magiclink_update_user_email( login_id: login_id, email: email, uri: 'https://myapp.com/verify-magic-link', # Set redirect URI here or via console template_id: 'template-id', provider_id: 'provider-id' ) ``` ```csharp // Args: // loginId (string): The user's current login ID var loginId = "email@company.com"; // newEmail (string): The new email. If an email already exists for this end user, it will be overwritten var newEmail = "newEmail@company.com"; // refreshJwt (string): The session's refresh token (used for verification) var refreshJwt = "xxxxx"; // redirectUrl (string): Optional redirect URI for the magic link var redirectUrl = "http://auth.company.com/api/verify-magic-link"; // request (UpdateUserEmailMagicLinkRequest): Update email payload with optional merge flags var request = new UpdateUserEmailMagicLinkRequest { LoginId = loginId, Email = newEmail, RedirectUrl = redirectUrl, AddToLoginIDs = true, OnMergeUseExisting = true, }; var response = await descopeClient.Auth.V1.Magiclink.Update.Email.PostWithJwtAsync(request, refreshJwt); var maskedEmail = response?.MaskedEmail; ``` ## Update Phone This function allows you to update the user's phone number address via SMS. This requires a valid refresh token. Once the user has received the Magic Link Code, you will need to host a page to verify the Magic Link code using the [Magic Link Verify Function](/auth-methods/magic-link/with-sdks/backend#user-verification). ```javascript // Args: // deliveryMethod: Delivery method to use to send Magic Link. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"}, "templateId": "template-id", "providerId": "provider-id" } const resp = await descopeClient.magicLink.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start Magic Link phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started Magic Link phone update") console.log(resp.data) } ``` ```python # Args: # delivery_method: Method used to deliver the Magic Link. delivery_method = DeliveryMethod.SMS # login_id (str): The login_id of the user being updated login_id = "phone@company.com" # phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten phone = "+12223334455" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxx" # add_to_login_ids (boolean): if true, the phone will be appended to the login ID array. add_to_login_ids = True # on_merge_use_existing (boolean): if true, on merge, the existing user's information (roles, tenants, etc) will be retained on_merge_use_existing = True # template_options (dict): messaging template options template_options = {"option": "Value1"} # template_id (str): The ID of the messaging template to use template_id = "template-id" # provider_id (str): The ID of the messaging provider to use provider_id = "provider-id" try: jwt_response = descope_client.magiclink.update_user_phone(delivery_method=delivery_method, login_id=login_id, phone=phone, refresh_token=refresh_token, add_to_login_ids=add_to_login_ids, on_merge_use_existing=on_merge_use_existing, template_options=template_options, template_id=template_id, provider_id=provider_id) print ("Successfully started Magic Link phone update") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to start Magic Link phone update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Method used to deliver the Magic Link. deliveryMethod := descope.MethodSMS // loginId (str): The loginId of the user being updated loginID := "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten phone := "+12223334455" // updateOptions (&descope.UpdateOptions): this allows you to configure behavior during the authentication process. updateOptions := &descope.UpdateOptions{} updateOptions.AddToLoginIDs = false updateOptions.OnMergeUseExisting = false updateOptions.TemplateOptions = map[string]any{"option": "Value1"} updateOptions.TemplateID = "template-id" updateOptions.ProviderID = "provider-id" // request (*http.Request): Request is needed to obtain JWT and send it to Descope, for verificatio res, err := descopeClient.Auth.MagicLink().UpdateUserPhone(ctx, deliveryMethod, loginID, phone, updateOptions, request) if (err != nil){ fmt.Println("Failed to start Magic Link phone update: ", err) } else { fmt.Println("Successfully started Magic Link phone update", res) } ``` ```java // Will throw DescopeException if there is an error with update MagicLinkService mls = descopeClient.getAuthenticationServices().getMagicLinkService(); try { AuthenticationInfo info = mls.updateUserPhone(deliveryMethod, loginId, phone, refreshToken, UpdateOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.magiclink_update_user_phone( login_id: login_id, phone: phone, uri: 'https://myapp.com/verify-magic-link', # Set redirect URI here or via console template_id: 'template-id', provider_id: 'provider-id' ) ``` ```csharp // Args: // loginId (string): The user's current login ID var loginId = "phone@company.com"; // newPhone (string): The new phone number. If a phone number already exists for this end user, it will be overwritten var newPhone = "+12223334455"; // refreshJwt (string): The session's refresh token (used for verification) var refreshJwt = "xxxxx"; // redirectUrl (string): Optional redirect URI for the magic link var redirectUrl = "http://auth.company.com/api/verify-magic-link"; // request (UpdateUserPhoneMagicLinkRequest): Update phone payload with optional merge flags. // For WhatsApp, call Update.Phone.Whatsapp instead of Update.Phone.Sms. var request = new UpdateUserPhoneMagicLinkRequest { LoginId = loginId, Phone = newPhone, RedirectUrl = redirectUrl, AddToLoginIDs = true, OnMergeUseExisting = true, }; var response = await descopeClient.Auth.V1.Magiclink.Update.Phone.Sms.PostWithJwtAsync(request, refreshJwt); var maskedPhone = response?.MaskedPhone; ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/magic-link/with-sdks/client) Add magic link authentication to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Magic Link via Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. A magic link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service can send magic links via email or SMS texts. The browser tab that is opened after clicking the magic link gets the authenticated session cookies. For example, consider a user that starts the login process on a laptop browser and gets a magic link delivered to their email inbox. When they click the email link, a new browser tab will open and they will be logged in on the new tab. ![The magic link flow within Descope for client SDK](/assets/magic-link-flow-descope.webp) Consider using magic links when your users typically use only one device to access your application, and when opening new tabs is not a big inconvenience ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/magic-link/with-sdks/client#user-sign-up) then [User Verification](/auth-methods/magic-link/with-sdks/client#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/magic-link/with-sdks/client#user-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/client#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/magic-link/with-sdks/client#user-sign-up-or-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/client#user-verification) ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. In this sample code, the magic-link will be sent by email to `email@company.com`. To change the delivery method to send the magic-link as a text, you would change the deliveryMethod to sms within the below example. Also note that signup is not complete without the user verification step below. ```javascript // Args: // user: Optional user object to populate new user information. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signUp[deliveryMethod](loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```javascript // Args: // user: Optional user object to populate new user information. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.magicLink.signUp[deliveryMethod](loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```html ``` ### User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the magic-link will be sent by email to `email@company.com`. Also note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signIn[deliveryMethod](loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.magicLink.signIn[deliveryMethod](loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```html ``` ### User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the magic-link will be sent by email to `email@company.com`. To change the delivery method to send the magic-link as a text, you would change the deliveryMethod to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signUpOrIn[deliveryMethod](loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.magicLink.signUpOrIn[deliveryMethod](loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```html ``` ### User Verification Once a user clicks the magic-link, your application must call the `verify` function. This means that this function needs to be called from your application when the user clicks the magiclink. The function call will return all the the necessary JWT tokens and claims and user information in the `resp` dictionary. The `sessionJwt` within the `resp` is needed for session validation. ```javascript // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. const token = "xxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.verify(token) if (!resp.ok) { console.log("Failed to verify magic link token") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified magic link token") } ``` ```javascript // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. const token = "xxxx" const resp = await descopeSdk.magicLink.verify(token) if (!resp.ok) { console.log("Failed to verify magic link token") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified magic link token") } ``` ```html ``` ### Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the magic link Code, you will need to host a page to verify the magic link code using the [magic link Verify Function](/auth-methods/magic-link/with-sdks/client#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link email update") console.log(resp.data) } ``` ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.magicLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link email update") console.log(resp.data) } ``` ```html ``` ### Update Phone This function allows you to update the user's phone number address via SMS. This requires a valid refresh token. Once the user has received the magic link Code, you will need to host a page to verify the magic link code using the [magic link Verify Function](/auth-methods/magic-link/with-sdks/client#user-verification). ```javascript // Args: // deliveryMethod: Delivery method to use to send magic link. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await useDescope.magicLink.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link phone update") console.log(resp.data) } ``` ```javascript // Args: // deliveryMethod: Delivery method to use to send magic link. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await useDescope.magicLink.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link phone update") console.log(resp.data) } ``` ```html ``` # Mobile SDKs (/auth-methods/magic-link/with-sdks/mobile) Add magic link authentication to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Magic Link via Mobile SDKs A magic link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service can send magic links via email or SMS texts. The browser tab that is opened after clicking the magic link gets the authenticated session cookies. For example, consider a user that starts the login process on a laptop browser and gets a magic link delivered to their email inbox. When they click the email link, a new browser tab will open and they will be logged in on the new tab. ![The magic link flow within Descope for mobile SDK](/assets/magic-link-flow-mobile-descope.webp) Consider using magic links when your users typically use only one device to access your application, and when opening new tabs is not a big inconvenience. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/magic-link/with-sdks/mobile#user-sign-up) then [User Verification](/auth-methods/magic-link/with-sdks/mobile#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/magic-link/with-sdks/mobile#user-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/mobile#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/magic-link/with-sdks/mobile#user-sign-up-or-sign-in) then [User Verification](/auth-methods/magic-link/with-sdks/mobile#user-verification) ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## User Sign-Up For registering a new user, your application should accept user information, including an email or phone number used for verification. In this sample code, the magic-link will be sent by email to `email@company.com`. To change the delivery method to send the magic-link as a text, you would change the deliveryMethod to sms within the below example. Also note that signup is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery let loginId = "email@company.com" // user: Optional user object to populate new user information. let user = User("name": "Joe Person", "phone": "+15555555555", "email": "email@company.com") // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_magiclink" do { try await Descope.magicLink.signUp(with: deliveryMethod, loginId: loginId, user: user, uri: uri) print("Successfully initiated Magic Link Sign Up") } catch { print("Failed to initiate Magic Link Sign Up") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery // user: Optional user object to populate new user information. // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // details: Optional object to populate new user information. try { Descope.magicLink.signUp( method = DeliveryMethod.Email, loginId = "email@company.com", // Optional object to populate new user information. details = SignUpDetails( name = "firstName lastName", email = "email@company.com", phone = "+15555555555", givenName = "firstName", middleName = "middleName", familyName = "lastName" ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // user: Optional user object to populate new user information. final details = SignUpDetails(name: "Joe Person"); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink"; Descope.magicLink.signUp( method: deliveryMethod, loginId: loginId, details: details, uri: uri); ``` ``` javascript // Args: // user: Optional user object to populate new user information. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signUp[deliveryMethod](loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ## User Sign-In For authenticating a user, your application should accept the user's identity (typically an email address or phone number). In this sample code, the magic-link will be sent by email to `email@company.com`. Also note that signin is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_magiclink" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { try await Descope.magicLink.signIn(with: deliveryMethod, loginId: loginId, uri: uri, options: signInOptions) print("Successfully initiated Magic Link Sign In") } catch { print("Failed to initiate Magic Link Sign In") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId: email or phone - the loginId of the user // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.magicLink.signIn( method = DeliveryMethod.Email, loginId = "email@company.com", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink"; Descope.magicLink.signIn( method: deliveryMethod, loginId: loginId, options: options, uri: uri); ``` ``` javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signIn[deliveryMethod](loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ## User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the magic-link will be sent by email to `email@company.com`. To change the delivery method to send the magic-link as a text, you would change the deliveryMethod to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - email or phone - the loginId of the user let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_magiclink" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { try await Descope.magicLink.signUpOrIn(with: deliveryMethod, loginId: loginId, uri: uri, options: signInOptions) print("Successfully initiated Magic Link Sign Up or In") } catch { print("Failed to initiate Magic Link Sign Up or In") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId: email or phone - email or phone - the loginId of the user // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.magicLink.signUpOrIn( method = DeliveryMethod.Email, loginId = "email@company.com", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send magic link. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink"; Descope.magicLink.signUpOrIn( method: deliveryMethod, loginId: loginId, options: options, uri: uri); ``` ``` javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink" // deliveryMethod: Delivery method to use to send magic-link. Supported values include "email" or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.signUpOrIn[deliveryMethod](loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ## User Verification Once a user clicks the magic-link, your application must call the `verify` function. This means that this function needs to be called from your application when the user clicks the magiclink. The function call will return all the the necessary JWT tokens and claims and user information in the `resp` dictionary. The `sessionJwt` within the `resp` is needed for session validation. ```swift // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. let token = "xxxx" do { let descopeSession = try await Descope.magicLink.verify(token: token) print("Successfully verified Magic Link Token") print(descopeSession as Any) } catch { print("Failed to verify Magic Link Token") print(error) } ``` ```kotlin // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. // loginId: email or phone - the loginId of the user // method: delivery method of magic link try { val authResponse = Descope.magicLink.verify( method = DeliveryMethod.Email, loginId = "email@company.com", code = "" ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // To verify a magic link, your redirect page must call the validation function on the token (t) parameter (https://your-redirect-address.com/verify?t=): // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. const token = 'http://auth.company.com/api/verify_magiclink?t=token'; final authResponse = await Descope.magicLink.verify(token: token); ``` ``` javascript // Args: // token: URL parameter containing the magic link token for example, http://auth.company.com/api/verify_magiclink?t=token. const token = "xxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.verify(token) if (!resp.ok) { console.log("Failed to verify magic link token") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified magic link token") } ``` ## Update Email The Descope SDK allows for you to update user's email address. With this function, you will pass the user's `loginId` and the new email address you want associated to the user. In order to verify the email address, the magic link will be sent via the email delivery method. Once the update email function has been called, you will need to verify the token before the email address will be updated. ```swift // Args: // email: the new email address you want to associate with the user let email = "newEmail@company.com" // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_magiclink" // refreshJwt: The refreshJwt of the user to be updated let refreshJwt = "xxxxxx" do { try await Descope.magicLink.updateEmail(email, loginId: loginId, uri: uri, refreshJwt: refreshJwt) print("Successfully initiated Magic Link Email Update") } catch { print("Failed to initiate Magic Link Email Update") print(error) } ``` ```kotlin // Args: // email: the new email address you want to associate with the user // loginId: email or phone - the loginId of the user // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // refreshJwt: The refreshJwt of the user to be updated // options: optional options for loginId and merging behavior try { Descope.magicLink.updateEmail( email = "email2@gompany.com", loginId = "email@company.com", uri = "", refreshJwt = "" options = UpdateOptions( addToLoginIds = true, onMergeUseExisting = true ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // email: the new email address you want to associate with the user const email = "newEmail@company.com"; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink"; // refreshJwt: The refreshJwt of the user to be updated const refreshJwt = "xxxxxx"; /// You can optionally pass the [options] parameter to add the new phone number /// as a `loginId` for the existing user, and to determine how to resolve conflicts /// if another user already exists with the same `loginId`. Check out the // Update Options (https://github.com/descope/descope-flutter/blob/main/lib/src/types/others.dart) type for more details. final options = UpdateOptions( addToLoginIds: true, onMergeUseExisting: true ); Descope.magicLink.updateEmail( loginId: loginId, email: email, uri: uri, refreshJwt: refreshJwt, options: options); ``` ``` javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.magicLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link email update") console.log(resp.data) } ``` ## Update Phone The Descope SDK allows for you to update user's phone number. With this function, you will pass the user's `loginId` and the new phone number you want associated to the user. In order to verify the phone number, the magic link will be sent via the sms delivery method. Once the update phone function has been called, you will need to verify the token before the phone number will be updated. ```swift // Args: // phone: the new phone number you want to associate with the user let phone = "+12222222222" // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_magiclink" // refreshJwt: The refreshJwt of the user to be updated let refreshJwt = "xxxxxx" do { try await Descope.magicLink.updatePhone(phone, with: .sms, loginId: loginId, uri: uri, refreshJwt: refreshJwt) print("Successfully started Magic Link Phone Update") } catch { print("Failed to initiate Magic Link Phone Update") print(error) } ``` ```kotlin // Args: // phone: the new phone number you want to associate with the user // loginId: email or phone - the loginId of the user // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // refreshJwt: The refreshJwt of the user to be updated // method: the delivery modality of the link // options: optional options for loginId and merging behavior try { Descope.magicLink.updatePhone( phone = "+11231231234", method = "sms", uri = "", loginId = "email@company.com", refreshJwt = "" options = UpdateOptions( addToLoginIds = true, onMergeUseExisting = true ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // phone: the new phone number you want to associate with the user const phone = "+12222222222"; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_magiclink"; // refreshJwt: The refreshJwt of the user to be updated final refreshJwt = "xxxxxx"; // method: the delivery modality of the link const method = DeliveryMethod.email; /// You can optionally pass the [options] parameter to add the new phone number /// as a `loginId` for the existing user, and to determine how to resolve conflicts /// if another user already exists with the same `loginId`. Check out the // Update Options (https://github.com/descope/descope-flutter/blob/main/lib/src/types/others.dart) type for more details. final options = UpdateOptions( addToLoginIds: true, onMergeUseExisting: true ); Descope.magicLink.updatePhone( loginId: loginId, method: method, phone: phone, uri: uri, refreshJwt: refreshJwt, options: options); ``` ``` javascript // Args: // deliveryMethod: Delivery method to use to send magic link. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await useDescope.magicLink.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start magic link phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started magic link phone update") console.log(resp.data) } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # Backend SDKs (/auth-methods/enchanted-link/with-sdks/backend) Add enchanted link authentication to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Enchanted Link via Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/enchanted-link/with-sdks/client). An enchanted link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service sends enchanted links via email. Enchanted links are an enhanced version of magic links. Enchanted links enable users to start the login process on one device (the originating device) while clicking the enchanted link on a different device. When the user clicks the correct link, their session on the originating device is validated, and they are logged in. A special security feature of enchanted link is that the end-user needs to pick the correct link from the three links delivered to them. ![The enchanted link flow within Descope for backend SDK](/assets/enchanted-link-flow-backend.webp) Enchanted links are user friendly since the user does not have to switch tabs or applications to log in. The browser tab they initiated login from is the only tab they need to use. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/enchanted-link/with-sdks/backend#user-sign-up), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/backend#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/backend#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/enchanted-link/with-sdks/backend#user-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/backend#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/backend#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/enchanted-link/with-sdks/backend#user-sign-up-or-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/backend#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/backend#user-verification) ## User Sign-Up To register a new user, you can use the `SignUp` function. In the example below, an Enchanted Link is sent to email@company.com. The SignUp call returns two important values: - **pendingRef** — used by your application to poll the verification status on the originating device. - **linkId** — should be shown to the user in your application so they can identify and click the correct link in the email they receive. Make sure your application uses the **pendingRef** to check when the user has successfully verified their sign-up. Also note that signup is not complete without the user verification step below. ```javascript // Args: // user: user meta data for signup. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.enchantedLink.signUp(loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```python # Args: # user: Optional user object to populate new user information. user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # login_id: email - becomes the login_id for the user from here on and also used for delivery login_id = "email@company.com" # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification uri = "http://auth.company.com/api/verify_enchantedlink" # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: resp = descope_client.enchantedlink.sign_up(login_id=login_id, uri=uri, user=user, signup_options=signup_options) print ("Successfully initialized signup flow") link_identifier = resp["linkId"] pending_ref = resp["pendingRef"] print ("Link Identifier: " + str(link_identifier)) print ("Pending Ref: " + str(pending_ref)) # initiate polling - see below except AuthException as error: print ("Failed to initialize signup flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify_enchantedlink" // user: Optional user object to populate new user information. user := &descope.User{Name:"Joe", Email:"email@company.com", Phone:"+15555555555"} // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } res, err := descopeClient.Auth.EnchantedLink().SignUp(ctx, loginID, URI, user, signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup flow: ", err) } else { fmt.Println("Successfully initialized signup flow: ", res) } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration. // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); EnchantedLinkService els = descopeClient.getAuthenticationServices().getEnchantedLinkService(); EnchantedLinkResponse res = null; try { String uri = "http://auth.company.com/api/verify_enchantedlink"; res = els.signUp(loginId, uri, user, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby email = 'desmond@descope.com' user = {'name': 'Desmond Copeland', 'phone': '+15555555555', 'email': email} res = descope_client.enchanted_link_sign_up( login_id: 'someone@example.com', uri: 'https://myapp.com/verify-enchanted-link', # Set redirect URI here or via console user: user ) link_identifier = res['linkId'] # Show the user which link they should press in their email pending_ref = res['pendingRef'] # Used to poll for a valid session masked_email = res['maskedEmail'] # The email that the message was sent to in a masked format ``` ```csharp // Args: // loginId (string): email - becomes the loginId for the user from here on and also used for delivery var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI for the enchanted link. Your application needs to host this page // and extract the token for verification. The token arrives as a query parameter named 't' var redirectUrl = "http://auth.company.com/api/verify_enchantedlink"; // request (EnchantedLinkSignUpEmailRequest): Signup payload var request = new EnchantedLinkSignUpEmailRequest { LoginId = loginId, Email = loginId, RedirectUrl = redirectUrl, User = new SignUpUser { Name = "Joe Person", Phone = "+15555555555", Email = loginId, }, }; try { var resp = await descopeClient.Auth.V1.Enchantedlink.Signup.Email.PostAsync(request); Console.WriteLine("Successfully initialized signup flow"); var linkId = resp?.LinkId; var pendingRef = resp?.PendingRef; Console.WriteLine($"linkId: {linkId}"); Console.WriteLine($"pendingRef: {pendingRef}"); // initiate polling - see below } catch (DescopeException ex) { Console.WriteLine("Failed to initialize signup flow"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## User Sign-In To login an existing user, you can use the `SignIn` function. In this example, an Enchanted Link is sent to email@company.com. The signIn call returns two key values: - **pendingRef** — used by your application to poll the verification status on the originating device. - **linkId** — should be displayed to the user so they can identify and click the correct link in the email they receive. Your application should then use the **pendingRef** to monitor when the user completes verification. Also note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.enchantedLink.signIn(loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```python # Args: # login_id: email - becomes the loginId for the user from here on and also used for delivery login_id = "email@company.com" # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification uri = "http://auth.company.com/api/verify_enchantedlink" # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa try: resp = descope_client.enchantedlink.sign_in(login_id=login_id, uri=uri, login_options=login_options) print ("Successfully initialized signin flow") link_identifier = resp["linkId"] pending_ref = resp["pendingRef"] print ("Link Identifier: " + str(link_identifier)) print ("Pending Ref: " + str(pending_ref)) # initiate polling - see below except AuthException as error: print ("Failed to initialize signin flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - the loginId for the user loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify_enchantedlink" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } res, err := descopeClient.Auth.EnchantedLink().SignIn(ctx, loginID, URI, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize signin flow: ", err) } else { fmt.Println("Successfully initialized signin flow: ", res) } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration. // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var loginOptions = LoginOptions.builder() .stepUp(true) .mfa(true) .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); EnchantedLinkService els = descopeClient.getAuthenticationServices().getEnchantedLinkService(); EnchantedLinkResponse res = null; try { String uri = "http://auth.company.com/api/verify_enchantedlink"; res = els.signIn(loginId, uri, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby res = descope_client.enchanted_link_sign_up( login_id: 'someone@example.com', uri: 'https://myapp.com/verify-enchanted-link' # Set redirect URI here or via console ) link_identifier = res['linkId'] # Show the user which link they should press in their email pending_ref = res['pendingRef'] # Used to poll for a valid session masked_email = res['maskedEmail'] # The email that the message was sent to in a masked format ``` ```csharp // Args: // loginId (string): email - becomes the loginId for the user from here on and also used for delivery var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI for the enchanted link. Your application needs to host this page // and extract the token for verification. The token arrives as a query parameter named 't' var redirectUrl = "http://auth.company.com/api/verify_enchantedlink"; // request (EnchantedLinkSignInRequest): Sign-in payload var request = new EnchantedLinkSignInRequest { LoginId = loginId, RedirectUrl = redirectUrl, LoginOptions = new LoginOptions { Stepup = false, Mfa = false, }, }; try { var resp = await descopeClient.Auth.V1.Enchantedlink.Signin.Email.PostAsync(request); Console.WriteLine("Successfully initialized signin flow"); var linkId = resp?.LinkId; var pendingRef = resp?.PendingRef; Console.WriteLine($"linkId: {linkId}"); Console.WriteLine($"pendingRef: {pendingRef}"); // initiate polling - see below } catch (DescopeException ex) { Console.WriteLine("Failed to initialize signin flow"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## User Sign-Up or Sign-In To sign up a new user or sign in an existing user, you can use the `signUpOrIn` function. In the example below, an Enchanted Link is sent to email@company.com. The signUpOrIn call returns two important values: - **pendingRef** — used by your application to poll the verification status on the originating device. - **linkId** — should be shown to the user in your application so they can identify and click the correct link in the email they receive. Make sure your application uses the **pendingRef** to check when the user has successfully verified their sign-in or sign-up. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.enchantedLink.signUpOrIn(loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```python # Args: # login_id: email - becomes the login_id for the user from here on and also used for delivery login_id = "email@company.com" # uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification uri = "http://auth.company.com/api/verify_enchantedlink" # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: resp = descope_client.enchantedlink.sign_up_or_in(login_id=login_id, uri=uri, signup_options=signup_options) print ("Successfully initialized signup or in flow") link_identifier = resp["linkId"] pending_ref = resp["pendingRef"] print ("Link Identifier: " + str(link_identifier)) print ("Pending Ref: " + str(pending_ref)) # initiate polling - see below except AuthException as error: print ("Failed to initialize signup or in flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - Used as the unique ID for the user from here on and also used for delivery loginID := "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' URI := "http://auth.company.com/api/verify_enchantedlink" // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } res, err := descopeClient.Auth.EnchantedLink().SignUpOrIn(ctx, loginID, URI. signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup or in flow: ", err) } else { fmt.Println("Successfully initialized signup or in flow: ", res) } ``` ```java // If configured globally, the redirect URI is optional. If provided however, it will be used // instead of any global configuration. // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); EnchantedLinkService els = descopeClient.getAuthenticationServices().getEnchantedLinkService(); EnchantedLinkResponse res = null; try { String uri = "http://auth.company.com/api/verify_enchantedlink"; res = els.signUpOrIn(loginId, uri, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby res = descope_client.enchanted_link_sign_up_or_in( login_id: 'someone@example.com', uri: 'https://myapp.com/verify-enchanted-link', # Set redirect URI here or via console ) link_identifier = res['linkId'] # Show the user which link they should press in their email pending_ref = res['pendingRef'] # Used to poll for a valid session masked_email = res['maskedEmail'] # The email that the message was sent to in a masked format ``` ```csharp // Args: // loginId (string): email - becomes the loginId for the user from here on and also used for delivery var loginId = "email@company.com"; // redirectUrl (string): Optional redirect URI for the enchanted link. Your application needs to host this page // and extract the token for verification. The token arrives as a query parameter named 't' var redirectUrl = "http://auth.company.com/api/verify_enchantedlink"; // request (EnchantedLinkSignInRequest): Signup-or-signin payload (same shape as sign-in) var request = new EnchantedLinkSignInRequest { LoginId = loginId, RedirectUrl = redirectUrl, }; try { var resp = await descopeClient.Auth.V1.Enchantedlink.SignupIn.Email.PostAsync(request); Console.WriteLine("Successfully initialized signUpOrIn flow"); var linkId = resp?.LinkId; var pendingRef = resp?.PendingRef; Console.WriteLine($"linkId: {linkId}"); Console.WriteLine($"pendingRef: {pendingRef}"); // initiate polling - see below } catch (DescopeException ex) { Console.WriteLine("Failed to initialize signUpOrIn flow"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## User Verification Call the `verify` function from your verify url. This means that this function needs to be called when the user clicks the enchanted link. If the token is valid, the user will be authenticated and session returned to the polling thread (see next step). ```javascript // Args: // token: URL parameter containing the enchanted link token for example, https://auth.company.com/api/verify_enchantedlink?t=token. const token = "xxxx" const resp = await descopeClient.enchantedLink.verify(token); if (!resp.ok) { console.log("Failed to verify enchanted link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified enchanted link") } ``` ```python # Args: # token: URL parameter containing the enchanted link token, for example, https://auth.yourcompany.com/api/verify_enchantedlink?t=token. token = "xxxx" try: resp = descope_client.enchantedlink.verify(token=token) print ("Successfully verified user") except AuthException as error: print ("Failed to verify user") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // token: URL parameter containing the enchanted link token, for example, https://auth.yourcompany.com/api/verify_enchantedlink?t=token. token = "xxxx" err := descopeClient.Auth.EnchantedLink().Verify(ctx, token) if (err != nil){ fmt.Println("Failed to verify user: ", err) } else { fmt.Println("Successfully verified user") } ``` ```java // Args: // token: URL parameter containing the enchanted link token, for example, https://auth.yourcompany.com/api/verify_enchantedlink?t=token. token = "xxxx" EnchantedLinkService els = descopeClient.getAuthenticationServices().getEnchantedLinkService(); try { els.verify(token); } catch (DescopeException de) { // Token is invalid, handle the error } ``` ```ruby begin descope_client.enchanted_link_verify_token(token=token) # Token is valid rescue AuthException => e # Token is invalid end ``` ```csharp // Args: // token (string): URL parameter containing the enchanted link token, for example, // https://auth.company.com/api/verify_enchantedlink?t=token. var token = "xxxx"; // request (VerifyEnchantedLinkRequest): Verify payload. Does not return JWTs — poll PendingSession next. var request = new VerifyEnchantedLinkRequest { Token = token, }; try { await descopeClient.Auth.V1.Enchantedlink.Verify.PostAsync(request); Console.WriteLine("Successfully verified enchanted link"); } catch (DescopeException ex) { Console.WriteLine("Failed to verify enchanted link"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## Polling for valid session On the route where you initialized the signIn, signUp, or signUpOrIn, you need to repeatedly poll for a valid session. `get_session(token)` is called repeatedly until the user clicks the enchanted link URL they received, so that the session on the initiating device can be directed to your desired page. ```javascript // Args: // pendingRef: Reference token received from signup or signin call. const pendingRef = "xxxxx" const resp = await descopeClient.enchantedLink.waitForSession(pendingRef); if (!resp.ok) { console.log("Failed to complete polling flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed polling flow") console.log(resp) } ``` ```python # Args: # pendingRef: Reference token received from signup or signin call. pendingRef = "xxxxx" i = 0 done = False max_tries = 18 while not done and i < max_tries: try: i = i + 1 time.sleep(10) jwt_response = descope_client.enchantedlink.get_session(pending_ref) done = True except AuthException as e: # Poll while still receiving 401 Unauthorized if e.status_code != 401: # Other failures means something's wrong, abort logging.info(f"Failed pending session, err: {e}") done = True if jwt_response[SESSION_TOKEN_NAME].get("jwt"): print ("Successfully completed polling flow") session_token = jwt_response[SESSION_TOKEN_NAME].get("jwt") print ("Session Token: " + str(session_token)) refresh_token = jwt_response[REFRESH_SESSION_TOKEN_NAME].get("jwt") print ("Refresh Token: " + str(refresh_token)) else: print ("Failed to complete polling flow") print ("Status Code: " + jwt_response.code) print ("Error Code: " + jwt_response.error.errorCode) print ("Error Description: " + jwt_response.error.errorDescription) print ("Error Message: " + jwt_response.error.errorMessage) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // pendingRef: Reference token received from signup or signin call. pendingRef = "xxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation. This code should go into your application server route handling the verification of code. for i := retriesCount; i > 0; i-- { authInfo, err := client.Auth.EnchantedLink().GetSession(ctx, pendingRef, w) if err == nil { // The user successfully authenticated using the correct link // The optional `w http.ResponseWriter` adds the session and refresh cookies to the response automatically. // Otherwise they're available via authInfo fmt.Println("Successfully completed polling flow: ", authInfo) break } if err == errors.EnchantedLinkUnauthorized && i > 1 { // poll again after X seconds time.Sleep(time.Second * time.Duration(retryInterval)) continue } if err != nil { fmt.Println("Failed to complete polling flow: ", err) break } } ``` ```java // After sending the link, you must poll to receive a valid session using the PendingRef from the previous step. A valid session will be returned only after the user clicks the right link. EnchantedLinkService els = descopeClient.getAuthenticationServices().getEnchantedLinkService(); // Poll for a certain number of tries / time frame for (int i = retriesCount; i > 0; i--) { try { AuthenticationInfo info = els.getSession(res.getPendingRef()); } catch (DescopeException de) { if (i > 1) { // Poll again after X seconds TimeUnit.SECONDS.sleep(retryInterval); continue; } else { // Handle the error break; } } } // To verify an enchanted link, your redirect page must call the validation function on the token (t) parameter (https://your-redirect-address.com/verify?t=). Once the token is verified, the session polling will receive a valid response. try { els.verify(token); } catch (DescopeException de) { // Token is invalid, handle the error } ``` ```ruby # After sending the link, you must poll to receive a valid session using the pending_ref from the previous step. A valid session will be returned only after the user clicks the right link. def poll_for_session(descope_client, pending_ref) max_tries = 15 i = 0 done = false while !done && i < max_tries begin i += 1 puts 'waiting 4 seconds for session to be created...' sleep(4) print '.' jwt_response = descope_client.enchanted_link_get_session(pending_ref) done = true rescue Descope::AuthException, Descope::Unauthorized => e puts 'Failed pending session, err: #{e}' nil end if jwt_response puts 'jwt_response: #{jwt_response}' refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME]['jwt'] puts 'refresh_token: #{refresh_token}' puts :'Done logging out!' descope_client.sign_out(refresh_token) puts 'User logged out' done = true end end end poll_for_session(descope_client, pending_ref) # To verify an enchanted link, your redirect page must call the validation function on the token (t) parameter (https://your-redirect-address.com/verify?t=). Once the token is verified, the session polling will receive a valid jwt_response. begin descope_client.enchanted_link_verify_token(token=token) # Token is valid rescue AuthException => e # Token is invalid end ``` ```csharp // Args: // pendingRef (string): Reference token received from signup or signin call. var pendingRef = "xxxxx"; // request (GetEnchantedLinkSessionRequest): Poll until the user clicks the enchanted link. var request = new GetEnchantedLinkSessionRequest { PendingRef = pendingRef, }; try { var session = await descopeClient.Auth.V1.Enchantedlink.PendingSession.PostAsync(request); Console.WriteLine("Successfully completed polling flow"); Console.WriteLine($"Session Token: {session?.SessionJwt}"); Console.WriteLine($"Refresh Token: {session?.RefreshJwt}"); } catch (DescopeException ex) { Console.WriteLine("Failed to complete polling flow"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the enchanted link, you will need to host a page to verify the enchanted link token using the [enchanted link Verify Function](/auth-methods/enchanted-link/with-sdks/backend#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.enchantedLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start enchanted link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started enchanted link email update") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user being updated login_id = "email@company.com" # email (str): The new email address. If an email address already exists for this end user, it will be overwritten email = "newEmail@company.com" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxx" # add_to_login_ids (boolean): if true, the email will be appended to the login ID array. add_to_login_ids = True # on_merge_use_existing (boolean): if true, on merge, the existing user's information (roles, tenants, etc) will be retained on_merge_use_existing = True # template_options (dict): email template options template_options = {"option": "Value1"} try: jwt_response = descope_client.enchantedlink.update_user_email(login_id=login_id, email=email, refresh_token=refresh_token, add_to_login_ids=add_to_login_ids, on_merge_use_existing=on_merge_use_existing, template_options=template_options) print ("Successfully started enchanted link email update") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to start enchanted link email update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginId (str): The loginId of the user being updated loginID := "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten email := "newEmail@company.com" // updateOptions (&descope.UpdateOptions): Optional login options for MFA, stepup, or custom claims. updateOptions := &descope.UpdateOptions{} updateOptions.AddToLoginIDs = false updateOptions.OnMergeUseExisting = false updateOptions.TemplateOptions = map[string]any{"option": "Value1"} // request (*http.Request): Request is needed to obtain JWT and send it to Descope, for verification res, err := descopeClient.Auth.EnchantedLink().UpdateUserEmail(ctx, loginID, email, updateOptions, request) if (err != nil){ fmt.Println("Failed to start enchanted link email update: ", err) } else { fmt.Println("Successfully started enchanted link email update", res) } ``` ```java // Will throw DescopeException if there is an error with update EnchantedLinkService mls = descopeClient.getAuthenticationServices().getEnchantedLinkService(); try { AuthenticationInfo info = mls.updateUserEmail(loginId, email, refreshToken, UpdateOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.enchanted_link_update_user_email(login_id: login_id, email: email, uri: uri) ``` ```csharp // Args: // loginId (string): The loginId of the user being updated var loginId = "email@company.com"; // email (string): The new email address. If an email address already exists for this end user, it will be overwritten var email = "newEmail@company.com"; // refreshJwt (string): The session's refresh token (used for verification) var refreshJwt = "xxxxx"; // redirectUrl (string): Optional redirect URI for the enchanted link var redirectUrl = "http://auth.company.com/api/verify_enchantedlink"; // request (UpdateUserEmailEnchantedLinkRequest): Update email payload with optional merge flags var request = new UpdateUserEmailEnchantedLinkRequest { LoginId = loginId, Email = email, RedirectUrl = redirectUrl, AddToLoginIDs = true, OnMergeUseExisting = true, }; try { var resp = await descopeClient.Auth.V1.Enchantedlink.Update.Email.PostWithJwtAsync(request, refreshJwt); Console.WriteLine("Successfully started enchanted link email update"); Console.WriteLine($"linkId: {resp?.LinkId}"); Console.WriteLine($"pendingRef: {resp?.PendingRef}"); } catch (DescopeException ex) { Console.WriteLine("Failed to start enchanted link email update"); Console.WriteLine($"Error: {ex.Message}"); } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/enchanted-link/with-sdks/client) Add enchanted link authentication to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Enchanted Link via Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. An enchanted link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service sends enchanted links via email. Enchanted links are an enhanced version of magic links. Enchanted links enable users to start the login process on one device (the originating device) while clicking the enchanted link on a different device. When the user clicks the correct link, their session on the originating device is validated, and they are logged in. A special security feature of enchanted link is that the end-user needs to pick the correct link from the three links delivered to them. ![The enchanted link flow within Descope for client SDK](/assets/enchanted-link-flow-descope.webp) Enchanted links are user friendly since the user does not have to switch tabs or applications to log in. The browser tab they initiated login from is the only tab they need to use. ## Use Cases 1. New user signup: The following actions must be completed, first [User Sign-Up](/auth-methods/enchanted-link/with-sdks/client#user-sign-up), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/client#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/client#user-verification) 2. Existing user signin: The following actions must be completed, first [User Sign-In](/auth-methods/enchanted-link/with-sdks/client#user-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/client#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/client#user-verification) 3. Sign-Up or Sign-In (Signs up a new user or signs in an existing user): The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/enchanted-link/with-sdks/client#user-sign-up-or-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/client#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/client#user-verification) ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### User Sign-up For registering a new user, your application client should accept user information, including a mandatory email used for verification. The application client should send this information to your application server. In this sample code, the enchanted link will be sent by email to `email@company.com`. The signup call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Also note that signup is not complete without the user verification step below. ```javascript // Args: // user: user meta data for signup. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signUp(loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```javascript // Args: // user: user meta data for signup. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.enchantedLink.signUp(loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```html ``` ### User Sign-in For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the enchanted link will be sent by email to `email@company.com`. The signin call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Also note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signIn(loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.enchantedLink.signIn(loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```html ``` ### User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. In this sample code, the enchanted link will be sent by email to `email@company.com`. The signin call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signUpOrIn(loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.enchantedLink.signUpOrIn(loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ```html ``` ### User Verification Call the `verify` function from your verify url. This means that this function needs to be called when the user clicks the enchanted link. If the token is valid, the user will be authenticated and session returned to the polling thread (see next step). ```javascript // Args: // token: URL parameter containing the enchanted link token for example, https://auth.company.com/api/verify_enchantedlink?t=token. const token = "xxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.verify(token); if (!resp.ok) { console.log("Failed to verify enchanted link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified enchanted link") } ``` ```javascript // Args: // token: URL parameter containing the enchanted link token for example, https://auth.company.com/api/verify_enchantedlink?t=token. const token = "xxxx" const resp = await descopeSdk.enchantedLink.verify(token); if (!resp.ok) { console.log("Failed to verify enchanted link") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified enchanted link") } ``` ```html ``` ### Polling for valid session On the route where you initialized the signIn, signUp, or signUpOrIn, you need to repeatedly poll for a valid session. `get_session(token)` is called repeatedly until the user clicks the enchanted link URL they received, so that the session on the initiating device can be directed to your desired page. ```javascript // Args: // pendingRef: Reference token received from signup or signin call. const pendingRef = "xxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.waitForSession(pendingRef); if (!resp.ok) { console.log("Failed to complete polling flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed polling flow") console.log(resp) } ``` ```javascript // Args: // pendingRef: Reference token received from signup or signin call. const pendingRef = "xxxxx" const resp = await descopeSdk.enchantedLink.waitForSession(pendingRef); if (!resp.ok) { console.log("Failed to complete polling flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed polling flow") console.log(resp) } ``` ```html ``` ### Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the enchanted link Code, you will need to host a page to verify the enchanted link code using the [enchanted link Verify Function](/auth-methods/enchanted-link/with-sdks/client#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start enchanted link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started enchanted link email update") console.log(resp.data) } ``` ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.enchantedLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start enchanted link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started enchanted link email update") console.log(resp.data) } ``` ```html ``` # Mobile SDKs (/auth-methods/enchanted-link/with-sdks/mobile) Add enchanted link authentication to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Enchanted Link via Mobile SDKs An enchanted link is a single-use link sent to the user for authentication (sign-up or sign-in) that validates their identity. The Descope service sends enchanted links via email. Enchanted links are an enhanced version of magic links. Enchanted links enable users to start the login process on one device (the originating device) while clicking the enchanted link on a different device. When the user clicks the correct link, their session on the originating device is validated, and they are logged in. A special security feature of enchanted link is that the end-user needs to pick the correct link from the three links delivered to them. ![The enchanted link flow within Descope for mobile SDK](/assets/enchanted-link-workflow-mobile.webp) Enchanted links are user friendly since the user does not have to switch tabs or applications to log in. The browser tab they initiated login from is the only tab they need to use. ## Use Cases 1. New user signup: The following actions must be completed, first [User Sign-Up](/auth-methods/enchanted-link/with-sdks/mobile#user-sign-up), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/mobile#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/mobile#user-verification) 2. Existing user signin: The following actions must be completed, first [User Sign-In](/auth-methods/enchanted-link/with-sdks/mobile#user-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/mobile#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/mobile#user-verification) 3. Sign-Up or Sign-In (Signs up a new user or signs in an existing user): The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/enchanted-link/with-sdks/mobile#user-sign-up-or-sign-in), then within the same route begin [Polling for a valid session](/auth-methods/enchanted-link/with-sdks/mobile#polling-for-valid-session), and when the enchanted link is clicked [User Verification](/auth-methods/enchanted-link/with-sdks/mobile#user-verification) ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## User Sign-up For registering a new user, your application client should accept user information, including an email or phone number used for verification. The application client should send this information to your application server. In this sample code, the enchanted link will be sent by email to `email@company.com`. The signup call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Also note that signup is not complete without the user verification step below. ```swift // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery let loginId = "email@company.com" // user: Optional user object to populate new user information. let user = User("name": "Joe Person", "phone": "+15555555555", "email": "email@company.com") // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_enchantedlink" do { let enchantedResponse = try await Descope.enchantedLink.signUp(loginId: loginId, user: user, uri: uri) print("Successfully initiated Enchanted Sign Up") print("Enchanted Link linkId: " + enchantedResponse!.linkId) print("Enchanted Link pendingRef: " + enchantedResponse!.pendingRef) } catch { print("Failed to initiate Enchanted Sign Up") print(error) } ``` ```kotlin // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery // user: Optional user object to populate new user information. // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // details: Optional object to populate new user information. try { Descope.enchantedLink.signUp( loginId = "email@company.com", uri = "http://auth.company.com/api/verify_enchantedlink", details = SignUpDetails( name = "firstName lastName", email = "email@company.com", phone = "+15555555555", givenName = "firstName", middleName = "middleName", familyName = "lastName" ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // user: Optional user object to populate new user information. final details = SignUpDetails(name: "Name"); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = 'http://auth.company.com/api/verify_enchantedlink'; Descope.enchantedLink.signUp(loginId: loginId, uri: uri, details: details); ``` ```javascript // Args: // user: user meta data for signup. const user = {"name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signUp(loginId, uri, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ## User Sign-in For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the enchanted link will be sent by email to `email@company.com`. The signin call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Also note that signin is not complete without the user verification step below. ```swift // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_enchantedlink" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { let enchantedResponse = try await Descope.enchantedLink.signIn(loginId: loginId, uri: uri, options: signInOptions) print("Successfully initiated Enchanted Link Sign In") print("Enchanted Link linkId: " + enchantedResponse!.linkId) print("Enchanted Link pendingRef: " + enchantedResponse!.pendingRef) } catch { print("Failed to initiate Enchanted Sign Up") print(error) } ``` ```kotlin // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // options: (Optional) options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.enchantedLink.signIn( loginId = "email@company.com", uri = "http://auth.company.com/api/verify_enchantedlink", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // user: Optional user object to populate new user information. const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = 'http://auth.company.com/api/verify_enchantedlink'; Descope.enchantedLink.signIn(loginId: loginId, uri: uri, options: options); ``` ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signIn(loginId, uri, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ## User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. In this sample code, the enchanted link will be sent by email to `email@company.com`. The signin call returns a `pendingRef` and a `linkId`. Display the `linkId` to end user from your application so that they can click on the correct link in the email that they receive. Then your application will utilize the `pendingRef` to poll for verification status on the originating device. Note that signUpOrIn is not complete without the user verification step below. ```swift // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_enchantedlink" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { let enchantedResponse = try await Descope.enchantedLink.signUpOrIn(loginId: loginId, uri: uri, options: signInOptions) print("Successfully initiated Enchanted Link Sign Up or In") print("Enchanted Link linkId: " + enchantedResponse!.linkId) print("Enchanted Link pendingRef: " + enchantedResponse!.pendingRef) } catch { print("Failed to initiate Enchanted Sign Up or in") print(error) } ``` ```kotlin // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // options: (Optional) options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.enchantedLink.signUpOrIn( loginId = "email@company.com", uri = "http://auth.company.com/api/verify_enchantedlink", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = 'email@company.com'; // user: Optional user object to populate new user information. const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = 'http://auth.company.com/api/verify_enchantedlink'; Descope.enchantedLink.signUpOrIn(loginId: loginId, uri: uri, options: options); ``` ```javascript // Args: // loginId: email - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.signUpOrIn(loginId, uri, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") const linkId = resp.data.linkId; console.log("linkId " + linkId) const pendingRef = resp.data.pendingRef; console.log("pendingRef " + pendingRef) } ``` ## User Verification Call the `verify` function from your verify url. This means that this function needs to be called when the user clicks the enchanted link. If the token is valid, the user will be authenticated and session returned to the polling thread (see next step). This should be implemented from your web client or backend sdk when the link is clicked from the email received. The mobile application will then poll for a session. ## Polling for valid session On the route where you initialized the signIn, signUp, or signUpOrIn, you need to repeatedly poll for a valid session. `get_session(token)` is called repeatedly until the user clicks the enchanted link URL they received, so that the session on the initiating device can be directed to your desired page. ```swift // Args: // pendingRef: Reference token received from signup or signin call. let pendingRef = "xxxxx" do { let descopeSession = try await Descope.enchantedLink.pollForSession(pendingRef: enchantedResponse!.pendingRef, timeout: 180) print("Successfully completed polling flow") print(descopeSession as Any) } catch { print("Failed to complete polling flow") print(error) } ``` ```kotlin // Args: // pendingRef: Reference token received from signup or signin call. // timeout: If timeout expires, an error is thrown try { Descope.enchantedLink.pollForSession( pendingRef = "", timeoutMilliseconds "" ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart var enchantedLinkResponse = await Descope.enchantedLink .signUpOrIn(loginId: loginId, uri: uri, options: options); // Args: // pendingRef: Reference token received from signup or signin call. var pendingRef = enchantedLinkResponse.pendingRef; // timeout: If timeout expires, an error is thrown const timeout = Duration(seconds: ""); final authResponse = await Descope.enchantedLink.pollForSession( pendingRef: pendingRef, timeout: timeout); ``` ```javascript // Args: // pendingRef: Reference token received from signup or signin call. const pendingRef = "xxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.waitForSession(pendingRef); if (!resp.ok) { console.log("Failed to complete polling flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed polling flow") console.log(resp) } ``` ## Update Email The Descope SDK allows for you to update user's email address. With this function, you will pass the user's `loginId` and the new email address you want associated to the user. In order to verify the email address, the enchanted link will be sent via the email delivery method. Once the update email function has been called, you will need to verify the token before the email address will be updated. ```swift // Args: // email: the new email address you want to associate with the user let email = "newEmail@company.com" // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' let uri = "http://auth.company.com/api/verify_enchantedlink" // refreshJwt: The refreshJwt of the user to be updated let refreshJwt = "xxxxxx" do { let enchantedResponse = try await Descope.enchantedLink.updateEmail(email, loginId: loginId, uri: uri, refreshJwt: refreshJwt) print("Successfully started Enchanted Link Email Update") print("Enchanted Link linkId: " + enchantedResponse!.linkId) print("Enchanted Link pendingRef: " + enchantedResponse!.pendingRef) } catch { print("Successfully started Enchanted Link Email Update") print(error) } ``` ```kotlin // Args: // email: the new email address you want to associate with the user // loginId: email or phone - the loginId of the user // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' // refreshJwt: The refreshJwt of the user to be updated // options: optional options for loginId and merging behavior try { Descope.enchantedLink.updateEmail( email = "email@company.com", loginId = "email@company.com", uri = "http://auth.company.com/api/verify_enchantedlink", refreshJwt = "" options = UpdateOptions( addToLoginIds = true, onMergeUseExisting = true ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // email: the new email address you want to associate with the user const email = "newEmail@company.com"; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // uri: (Optional) this is the link that user is sent (code appended) for verification. Your application needs to host this page and extract the token for verification. The token arrives as a query parameter named 't' const uri = "http://auth.company.com/api/verify_enchantedlink"; // refreshJwt: The refreshJwt of the user to be updated final refreshJwt = Descope.sessionManager.session!.refreshJwt; /// You can optionally pass the [options] parameter to add the new phone number /// as a `loginId` for the existing user, and to determine how to resolve conflicts /// if another user already exists with the same `loginId`. Check out the // Update Options (https://github.com/descope/descope-flutter/blob/main/lib/src/types/others.dart) type for more details. final options = UpdateOptions( addToLoginIds: true, onMergeUseExisting: true ); Descope.enchantedLink.updateEmail( email: email, refreshJwt: refreshJwt, loginId: loginId, uri: uri, options: options); ``` ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.enchantedLink.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start enchanted link email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started enchanted link email update") console.log(resp.data) } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # Backend SDKs (/auth-methods/notp/with-sdks/backend) Add nOTP login to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # nOTP (WhatsApp) Authentication with Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/notp/with-sdks/client). nOTP allows users to log in via WhatsApp with just a single click, eliminating the need for codes, usernames, and typing. Unlike traditional OTP methods, nOTP doesn't require the company to connect to email servers or SMS providers, which can significantly reduce costs as it scales with the number of users. To get started with authentication using nOTP (WhatsApp), refer to our [nOTP Documentation](/auth-methods/notp/settings). Continue reading to learn how to integrate nOTP authentication into your application using our Backend SDKs. ## User Sign-Up To implement nOTP (WhatsApp) authentication, the first step is user sign-up using the NOTP (WhatsApp) authentication method. Use the SignUp function to create a new user via WhatsApp. The Login ID should ideally be a phone number or can be left empty, in which case the phone number from WhatsApp will be used as the Login ID during verification. After calling the sign-up function, you will receive a response that includes a redirect URL or a QR code image. Present this to the user, who will then use WhatsApp to scan the QR code or follow the link to begin the authentication process. ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery const loginId = "email@company.com" const resp = await descopeClient.notp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize NOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP signup.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete auth console.log(resp.data) } ``` ```go // ctx: context.Context - Application context for function calls, like cancellation signals during requests. // loginID: The login ID for the user, should be a phone number (or leave empty to be provided later). // user: (optional) *descope.User object to pre-fill with user info (name, email, phone, etc.). // signUpOptions: (optional) Additional sign-up options for nOTP (WhatsApp). ctx := context.Background() loginID := "+15555555555" // The user's phone number; can be "" (empty string) if you want to use WhatsApp phone as login ID user := &descope.User{ Name: "Joe Person", Email: "email@company.com", // Phone: If left empty, defaults to loginID. } signUpOptions := &descope.SignUpOptions{ // Optional sign-up options. You can use any of the following (or leave as nil for defaults): // CustomClaims: Set any custom claims to associate with the user. // TemplateID: Override the default WhatsApp message template. // TemplateOptions: Map for template variables (for customizing WhatsApp message contents). // TenantID: If your app supports multi-tenancy, specify a Tenant ID. // // Example: // signUpOptions := &descope.SignUpOptions{ // CustomClaims: map[string]any{"role": "admin"}, // TemplateID: "whatsapp-custom-template", // TemplateOptions: map[string]string{ // "greeting": "Hi", // "product": "YourProduct", // }, // TenantID: "tenant-123", // } // Or, to use default values: // signUpOptions := nil } resp, err := descopeClient.Auth.NOTP.SignUp(ctx, loginID, user, signUpOptions) if err != nil { fmt.Println("Failed to start nOTP (WhatsApp) sign-up flow:", err) return } // resp will include a redirect URL and/or QR code image, plus a pending reference used to poll for the session. fmt.Println("Successfully started nOTP sign-in (WhatsApp).") fmt.Println("Redirect URL:", resp.RedirectURL) if resp.Image != "" { fmt.Println("QR Code image (base64):", resp.Image) } // Save resp.PendingRef and pass it to GetSession to complete the flow. fmt.Println("Pending reference:", resp.PendingRef) ``` ```csharp // Args: // LoginId (string): The login ID for the user — should be a phone number, or empty to use the WhatsApp number // User (SignUpUser) optional: Additional user metadata // Phone (string) optional: Phone number to associate with the user // LoginOptions (SignupLoginOptions) optional: Set custom claims, locale, or template options // Provider (string) optional: The nOTP provider (e.g. "whatsapp") // ProviderId (string) optional: Override the default provider ID // SsoAppId (string) optional: Associate sign-up with a specific Federated App // Templates (NOTPTemplateIDs) optional: Override default WhatsApp message templates var response = await client.Auth.V1.Auth.Notp["whatsapp"].Signup.PostAsync( new NOTPSignUpRequest { LoginId = "+15555555555", User = new SignUpUser { Name = "Joe Person", Email = "email@company.com" }, Templates = new NOTPTemplateIDs { VerifyTemplateId = "my-verify-template", SuccessTemplateId = "my-success-template", ErrorTemplateId = "my-error-template" } }); // response.PendingRef — pass to PendingSession to poll for the completed session // response.LinkUrl — redirect URL to present to the user // response.Image — QR code image (base64) to display to the user ``` ## User Sign-In To sign in a user with nOTP (WhatsApp) authentication, use the `SignIn` function with the NOTP (WhatsApp) authentication method. The Login ID should be a phone number or can be left empty. If left empty, the phone number from WhatsApp will be used as the login ID during the verification process. Upon calling the sign-in function, the response will include a redirect URL and/or a QR code image. Present this information to the user; they should scan the QR code or follow the link in WhatsApp to start the authentication flow. ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "email@company.com" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.notp.signIn(loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete sign-in console.log(resp.data) } ``` ```go // ctx: context.Context - Application context for the request (supports cancellation, etc.). // loginID: The login ID for the user, should be a phone number (or leave empty to be provided later). // r: *http.Request - the incoming HTTP request, used if refresh tokens are required (for step-up/MFA). // loginOptions: (optional) *descope.LoginOptions for advanced login flows (MFA, step-up, custom claims, template options, etc.). ctx := context.Background() loginID := "+15555555555" // The user's phone number. Can be "" (empty) to allow WhatsApp phone number as login ID. // Use *http.Request from your handler if performing step-up/MFA, otherwise can be nil if not used in your flow. var r *http.Request = nil // Replace with your actual http.Request if available. // Example loginOptions. See SDK docs for all available options. // For defaults, set to nil. loginOptions := &descope.LoginOptions{ // Stepup: false, // MFA: false, // CustomClaims: map[string]any{"role": "user"}, // TemplateID: "your-custom-template", // TemplateOptions: map[string]string{"greeting": "Hi"}, // TenantID: "tenant-123", } // Call the SignIn function to initiate nOTP (WhatsApp) authentication. resp, err := descopeClient.Auth.NOTP.SignIn(ctx, loginID, r, loginOptions) if err != nil { fmt.Println("Failed to start nOTP (WhatsApp) sign-in flow:", err) return } // resp will include a redirect URL and/or QR code image, plus a pending reference used to poll for the session. fmt.Println("Successfully started nOTP sign-in (WhatsApp).") fmt.Println("Redirect URL:", resp.RedirectURL) if resp.Image != "" { fmt.Println("QR Code image (base64):", resp.Image) } // Save resp.PendingRef and pass it to GetSession to complete the flow. fmt.Println("Pending reference:", resp.PendingRef) ``` ```csharp // Args: // LoginId (string): The login ID of the user — should be a phone number, or empty to use the WhatsApp number // LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims // Provider (string) optional: The nOTP provider (e.g. "whatsapp") // ProviderId (string) optional: Override the default provider ID // SsoAppId (string) optional: Associate sign-in with a specific Federated App // Templates (NOTPTemplateIDs) optional: Override default WhatsApp message templates var response = await client.Auth.V1.Auth.Notp["whatsapp"].Signin.PostAsync( new NOTPSignInRequest { LoginId = "+15555555555", LoginOptions = new LoginOptions { /* ... */ }, Templates = new NOTPTemplateIDs { VerifyTemplateId = "my-verify-template", SuccessTemplateId = "my-success-template", ErrorTemplateId = "my-error-template" } }); // response.PendingRef — pass to PendingSession to poll for the completed session // response.LinkUrl — redirect URL to present to the user // response.Image — QR code image (base64) to display to the user ``` ## User Sign-Up-or-In Use the `SignUpOrIn` function to authenticate a user with nOTP (WhatsApp). If the user does not exist, `SignUpOrIn` will create a new user automatically. The Login ID should be a phone number or can be left empty. If left empty, the WhatsApp phone number will be used as the login ID during verification. After calling `SignUpOrIn`, the response will contain a redirect URL and/or a QR code image. Present the QR code or URL to the user, who should scan the QR code or follow the link in WhatsApp to start the authentication flow. ```javascript // Args: // loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery. const loginId = "email@company.com" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.notp.signUpOrIn(loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-Up or Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-Up or Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete authentication console.log(resp.data) } ``` ```go // Args: // ctx: context.Context - Application context for the request. // loginID: The login ID for the user, should be a phone number (or can be empty, in which case the WhatsApp phone number will be used as login ID during verification). // signUpOptions: (optional) *descope.SignUpOptions for advanced options (TenantID, CustomClaims, TemplateOptions, etc.). ctx := context.Background() loginID := "+15555555555" // The user's phone number. Can be "" (empty) to use WhatsApp phone number. // Example signUpOptions. For defaults, set to nil. signUpOptions := &descope.SignUpOptions{ TenantID: "tenant-123", CustomClaims: map[string]any{"role": "user"}, TemplateOptions: map[string]string{"greeting": "Hi"}, } // Call the SignUpOrIn function to initiate nOTP (WhatsApp) authentication. resp, err := descopeClient.Auth.NOTP.SignUpOrIn(ctx, loginID, signUpOptions) if err != nil { fmt.Println("Failed to start nOTP (WhatsApp) sign-up-or-in flow:", err) return } // resp will include a redirect URL and/or QR code image, plus a pending reference used to poll for the session. fmt.Println("Successfully started nOTP sign-in (WhatsApp).") fmt.Println("Redirect URL:", resp.RedirectURL) if resp.Image != "" { fmt.Println("QR Code image (base64):", resp.Image) } // Save resp.PendingRef and pass it to GetSession to complete the flow. fmt.Println("Pending reference:", resp.PendingRef) ``` ```csharp // Args: // LoginId (string): The login ID — should be a phone number, or empty to use the WhatsApp number // LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims // Provider (string) optional: The nOTP provider (e.g. "whatsapp") // ProviderId (string) optional: Override the default provider ID // SsoAppId (string) optional: Associate with a specific Federated App // Templates (NOTPTemplateIDs) optional: Override default WhatsApp message templates // Note: SignupIn uses the same NOTPSignInRequest as Signin var response = await client.Auth.V1.Auth.Notp["whatsapp"].SignupIn.PostAsync( new NOTPSignInRequest { LoginId = "+15555555555", LoginOptions = new LoginOptions { /* ... */ }, Templates = new NOTPTemplateIDs { VerifyTemplateId = "my-verify-template", SuccessTemplateId = "my-success-template", ErrorTemplateId = "my-error-template" } }); // response.PendingRef — pass to PendingSession to poll for the completed session // response.LinkUrl — redirect URL to present to the user // response.Image — QR code image (base64) to display to the user ``` ## Update User Use the `UpdateUser` function to update user details via nOTP (WhatsApp) authentication. The Login ID should be a phone number or can be left empty—if left empty, the WhatsApp phone number will be used as the login ID during verification. After calling `UpdateUser`, you’ll receive a response containing a redirect URL and/or QR code image. Present this QR code or URL to the user, who must scan the QR code or follow the link with WhatsApp to begin the authentication and update process. ```go // ctx: context.Context - Application context for the request (supports cancellation, etc.). // loginID: The login ID of the user to update, should be a phone number. // phone: The new phone number to set for the user (or leave empty to use the WhatsApp phone number during verification). // updateOptions: (optional) *descope.NOTPUpdateOptions for advanced update flows (custom claims, template options, etc.). // r: *http.Request - the incoming HTTP request, used to extract the user's refresh token (required for the update). ctx := context.Background() loginID := "+15555555555" // The login ID of the user being updated (a phone number). phone := "+15556667777" // The new phone number to set. Can be "" (empty) to allow the WhatsApp phone number to be used. // The user's refresh token is extracted from the incoming request, so pass the *http.Request from your handler. var r *http.Request = nil // Replace with your actual http.Request. // Example updateOptions. See SDK docs for all available options. // For defaults, set to nil. updateOptions := &descope.NOTPUpdateOptions{ // CustomClaims: map[string]any{"role": "user"}, // TemplateID: "your-custom-template", // TemplateOptions: map[string]string{"greeting": "Hi"}, } // Call the UpdateUser function to initiate the nOTP (WhatsApp) update flow. resp, err := descopeClient.Auth.NOTP.UpdateUser(ctx, loginID, phone, updateOptions, r) if err != nil { fmt.Println("Failed to start nOTP (WhatsApp) update user flow:", err) return } // resp will include a redirect URL and/or QR code image, plus a pending reference used to poll for the session. fmt.Println("Successfully started nOTP sign-in (WhatsApp).") fmt.Println("Redirect URL:", resp.RedirectURL) if resp.Image != "" { fmt.Println("QR Code image (base64):", resp.Image) } // Save resp.PendingRef and pass it to GetSession to complete the flow. fmt.Println("Pending reference:", resp.PendingRef) ``` ```csharp // Args: // LoginId (string): The login ID of the user to update — should be a phone number // Phone (string) optional: New phone number to associate with the user // Locale (string) optional: Locale for WhatsApp messages // Provider (string) optional: The nOTP provider (e.g. "whatsapp") // ProviderId (string) optional: Override the default provider ID // SsoAppId (string) optional: Associate with a specific Federated App // AddToLoginIDs (bool) optional: Add new phone as an additional login ID rather than replacing // OnMergeUseExisting (bool) optional: On login ID conflict, keep the existing user // Templates (NOTPTemplateIDs) optional: Override default WhatsApp message templates // TemplateOptions (UpdateUserNOTPRequest_templateOptions) optional: Dynamic template substitution values var response = await client.Auth.V1.Auth.Notp["whatsapp"].Update.PostAsync( new UpdateUserNOTPRequest { LoginId = "+15555555555", Phone = "+15556667777" }); // response.PendingRef — pass to PendingSession to poll for the completed session // response.LinkUrl — redirect URL to present to the user // response.Image — QR code image (base64) to display to the user ``` ## Get Session To complete the WhatsApp nOTP flow, after the user completes verification in WhatsApp, retrieve their JWT by invoking the `waitForSession` or `GetSession` function and passing in the `pendingRef` from your SignIn/SignUp/SignUpOrIn call. The function polls Descope until the user finishes verifying in WhatsApp, then returns the session and refresh tokens (setting them as cookies on your response). If the user doesn't complete verification in time, it returns an error instead of a session. On success, `waitForSession` resolves with `sessionResp.data` as a `JWTResponse`. Use `sessionJwt` for session validation and `refreshJwt` to renew the session: ```json { "sessionJwt": "eyJhbGciOiJSUzI...", "refreshJwt": "eyJhbGciOiJ...", "cookieDomain": "", "cookiePath": "/", "cookieMaxAge": 2419199, "cookieExpiration": 1685116422, "user": { "loginIds": ["+15555555555"], "userId": "U2abc123", "name": "Joe Person", "email": "email@company.com", "phone": "+15555555555", "verifiedEmail": true, "verifiedPhone": true, "roleNames": [], "userTenants": [], "status": "enabled", "externalIds": ["+15555555555"], "customAttributes": {}, "createdTime": 1682612331 }, "firstSeen": true } ``` ```javascript // Args: // pendingRef: the reference string returned from notp.signIn / signUp / signUpOrIn. const pendingRef = resp.data.pendingRef // config (optional WaitForSessionConfig): tune how long and how often to poll. const config = { "timeoutMs": 120000, // give up after 2 minutes (default applies if omitted) "pollingIntervalMs": 1000 // poll once per second (default applies if omitted) } const sessionResp = await descopeClient.notp.waitForSession(pendingRef, config); if (!sessionResp.ok) { // Note: a timeout does NOT throw - it resolves with ok: false, so check it here. console.log("Failed to complete NOTP authentication") console.log("Status Code: " + sessionResp.code) console.log("Error Code: " + sessionResp.error.errorCode) console.log("Error Description: " + sessionResp.error.errorDescription) console.log("Error Message: " + sessionResp.error.errorMessage) } else { const { sessionJwt, refreshJwt, user, firstSeen } = sessionResp.data console.log("Successfully authenticated via NOTP.") console.log("Session JWT:", sessionJwt) console.log("Refresh JWT:", refreshJwt) console.log("User ID:", user.userId) console.log("First seen (new user):", firstSeen) } ``` ```go // ctx: context.Context - Application context for the request (supports cancellation, etc.). // pendingRef: The pending reference returned by SignIn/SignUp/SignUpOrIn, used to poll for the session. // w: http.ResponseWriter - the outgoing HTTP response, used to set the session/refresh token cookies. ctx := context.Background() // The pendingRef value comes from the response of the initial SignIn/SignUp/SignUpOrIn call (resp.PendingRef). pendingRef := "" // Replace with the actual resp.PendingRef value. // Use the http.ResponseWriter from your handler so the SDK can set session cookies on the response. var w http.ResponseWriter = nil // Replace with your actual http.ResponseWriter if available. // Call GetSession to complete the nOTP (WhatsApp) flow once the user has sent the verification message. // This returns a valid session only after the user completes verification in WhatsApp; otherwise it errors. authInfo, err := descopeClient.Auth.NOTP.GetSession(ctx, pendingRef, w) if err != nil { fmt.Println("Failed to get nOTP (WhatsApp) session:", err) return } // authInfo contains the session and refresh tokens, plus user details. fmt.Println("Successfully retrieved nOTP session (WhatsApp).") fmt.Println("Session JWT:", authInfo.SessionToken.JWT) if authInfo.RefreshToken != nil { fmt.Println("Refresh JWT:", authInfo.RefreshToken.JWT) } fmt.Println("First seen (new user):", authInfo.FirstSeen) ``` ```csharp // Args: // PendingRef (string): The pending reference returned by Signup / Signin / SignupIn var response = await client.Auth.V1.Auth.Notp.PendingSession.PostAsync( new GetNOTPSessionRequest { PendingRef = "pending-ref-from-sign-in" }); // response is a JWTResponse containing session and refresh tokens once the user completes WhatsApp verification ``` # Client SDKs (/auth-methods/notp/with-sdks/client) Add nOTP login to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # nOTP (WhatsApp) Authentication with Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you would like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. nOTP allows users to log in via WhatsApp with just a single click, eliminating the need for codes, usernames, and typing. Unlike traditional OTP methods, nOTP doesn't require the company to connect to email servers or SMS providers, which can significantly reduce costs as it scales with the number of users. To get started with authentication using nOTP (WhatsApp), refer to our [nOTP Documentation](/auth-methods/notp/settings). Continue reading to learn how to integrate nOTP authentication into your application using our Client SDKs. The `notp` methods are exposed by the core Descope JavaScript SDK and are available through the Descope client SDKs: React, Next.js, Vue, Angular, WebJS, and plain HTML via the WebJS UMD bundle. The main differences are how you obtain the SDK instance, and that the Angular SDK wraps promise-returning SDK methods, including notp methods, as RxJS `Observable`s, so Angular examples use `.subscribe()` instead of `await`. ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ## User Sign-Up To implement nOTP (WhatsApp) authentication, the first step is user sign-up using the nOTP (WhatsApp) authentication method. Use the `signUp` function to create a new user via WhatsApp. The Login ID should ideally be a phone number or can be left empty, in which case the phone number from WhatsApp will be used as the Login ID during verification. After calling the sign-up function, you will receive a response that includes a redirect URL or a QR code image. Present this to the user, who will then use WhatsApp to scan the QR code or follow the link to begin the authentication process. ```javascript import { useDescope } from '@descope/react-sdk'; // Args: // loginId: phone - becomes the unique ID for the user from here on (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize NOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP signup.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete auth console.log(resp.data) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: phone - becomes the unique ID for the user from here on (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signUp(loginId, user); if (!resp.ok) { console.log("Failed to initialize NOTP signup") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP signup.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete auth console.log(resp.data) } ``` ```javascript // Inside your component's ``` ## User Sign-In To sign in a user with nOTP (WhatsApp) authentication, use the `signIn` function with the nOTP (WhatsApp) authentication method. The Login ID should be a phone number or can be left empty. If left empty, the phone number from WhatsApp will be used as the login ID during the verification process. Upon calling the sign-in function, the response will include a redirect URL and/or a QR code image. Present this information to the user; they should scan the QR code or follow the link in WhatsApp to start the authentication flow. ```javascript import { useDescope } from '@descope/react-sdk'; // Args: // loginId: phone - must be same as provided at the time of signup (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signIn(loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete sign-in console.log(resp.data) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: phone - must be same as provided at the time of signup (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signIn(loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete sign-in console.log(resp.data) } ``` ```javascript // Inside your component's ``` ## User Sign-Up-or-In Use the `signUpOrIn` function to authenticate a user with nOTP (WhatsApp). If the user does not exist, `signUpOrIn` will create a new user automatically. The Login ID should be a phone number or can be left empty. If left empty, the WhatsApp phone number will be used as the login ID during verification. After calling `signUpOrIn`, the response will contain a redirect URL and/or a QR code image. Present the QR code or URL to the user, who should scan the QR code or follow the link in WhatsApp to start the authentication flow. ```javascript import { useDescope } from '@descope/react-sdk'; // Args: // loginId: phone - becomes the unique ID for the user from here on (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signUpOrIn(loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-Up or Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-Up or Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete authentication console.log(resp.data) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: phone - becomes the unique ID for the user from here on (or leave empty to use the WhatsApp phone number). const loginId = "+15555555555" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.notp.signUpOrIn(loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize NOTP Sign-Up or Sign-In") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized NOTP Sign-Up or Sign-In.") // resp.data contains { pendingRef, redirectUrl, image } // Present the QR `image` or send the user to `redirectUrl` to complete authentication console.log(resp.data) } ``` ```javascript // Inside your component's ``` ## Get Session To complete the WhatsApp nOTP flow, after the user completes verification in WhatsApp, retrieve their JWT by invoking the `waitForSession` function and passing in the `pendingRef` from your `signIn` / `signUp` / `signUpOrIn` call. The function polls Descope until the user finishes verifying in WhatsApp, then returns the session and refresh tokens (and, with `persistTokens` enabled, persists them for you). By default, polling runs every 1 second and times out after 10 minutes. ```javascript import { useDescope } from '@descope/react-sdk'; // Args: // pendingRef: the reference string returned from notp.signIn / signUp / signUpOrIn. const pendingRef = resp.data.pendingRef // config (optional WaitForSessionConfig): tune how long and how often to poll. const config = { "timeoutMs": 120000, // give up after 2 minutes (default applies if omitted) "pollingIntervalMs": 1000 // poll once per second (default applies if omitted) } const descopeSdk = useDescope(); const sessionResp = await descopeSdk.notp.waitForSession(pendingRef, config); if (!sessionResp.ok) { // Note: a timeout does NOT throw - it resolves with ok: false, so check it here. console.log("Failed to complete NOTP authentication") console.log("Status Code: " + sessionResp.code) console.log("Error Code: " + sessionResp.error.errorCode) console.log("Error Description: " + sessionResp.error.errorDescription) console.log("Error Message: " + sessionResp.error.errorMessage) } else { console.log("Successfully authenticated via NOTP. " + JSON.stringify(sessionResp.data)) // sessionResp.data is a JWTResponse containing sessionJwt, refreshJwt, etc. } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // pendingRef: the reference string returned from notp.signIn / signUp / signUpOrIn. const pendingRef = resp.data.pendingRef // config (optional WaitForSessionConfig): tune how long and how often to poll. const config = { "timeoutMs": 120000, // give up after 2 minutes (default applies if omitted) "pollingIntervalMs": 1000 // poll once per second (default applies if omitted) } const descopeSdk = useDescope(); const sessionResp = await descopeSdk.notp.waitForSession(pendingRef, config); if (!sessionResp.ok) { // Note: a timeout does NOT throw - it resolves with ok: false, so check it here. console.log("Failed to complete NOTP authentication") console.log("Status Code: " + sessionResp.code) console.log("Error Code: " + sessionResp.error.errorCode) console.log("Error Description: " + sessionResp.error.errorDescription) console.log("Error Message: " + sessionResp.error.errorMessage) } else { console.log("Successfully authenticated via NOTP. " + JSON.stringify(sessionResp.data)) // sessionResp.data is a JWTResponse containing sessionJwt, refreshJwt, etc. } ``` ```javascript // Inside your component's ``` # OAuth Scopes and Provider Tokens (/auth-methods/oauth/customize/custom-scopes) This guide covers the implementation of customized OAuth scopes and utilizing the provider's access token within Descope. # OAuth Scopes and Provider Tokens Descope allows you to customize the scopes requested from the provider when you have configured your authentication account for the provider within the [OAuth Authentication Methods](https://app.descope.com/settings/authentication/social) for the applicable provider. This feature also pairs with the option to `Manage tokens from provider` for later use within your application when you would like to use the provider token to take action on the user's behalf or load further data for the user. ## Use Cases Using customized scopes offers a wide variety of use cases. These use cases can be as simple as getting further contact information for a user to make a FaceBook post on a user's behalf. Below are a few examples that you may be interested in using: - Adding the `pages_manage_posts` scope to your FaceBook provider to allow your application to post to Facebook on the user's behalf. - Adding the `read:org` and `read:project` scopes to your GitHub provider to capture detailed information about the user's org and associated projects. - Add the `calendar` scope to your Google provider to allow your application to read and write to the user's calendar. ## Example Use Case This example will cover adding the `calendar` scope to your Google provider to allow you to read and write to the user's calendar. ### Google Configuration #### Add the Scope If not completed, step through the guide for creating a [Custom Social Login With Google](/auth-methods/oauth/providers/setting-up-your-own-apps/google). When you create your consent screen, add `https://www.googleapis.com/auth/calendar` as a scope. If you've already created your social login within Google, ensure you add this scope to your current consent screen. Below is an example of what this configuration would look like within your Google consent flow. ![Descope - Google Consent Flow with calendar scope](/assets/custom-scopes-google-consent-flow.webp) #### Enable Google Calendar API Go to [Enabled APIs & Services](https://console.cloud.google.com/apis/) within Google, and click `+Enable APIs and Services` at the top of the page, search for and enable `Google Calendar API` ### Descope Configuration Click on the Google provider within the [Social Authentication Methods](https://app.descope.com/settings/authentication/social). If you've not already configured your provider, configure the provider per the [Custom Social Login With Google](/auth-methods/oauth/providers/setting-up-your-own-apps/google) guide, and then also configure the additional scope `https://www.googleapis.com/auth/calendar`. To utilize the provider's token later, select the toggle for `Manage tokens from provider`. Below, you can see a configured Google provider with the necessary configuration. ![Descope configuration of google provider with the calendar scope and managing tokens](/assets/custom-scopes-google-provider.webp) Once you have successfully configured the provider, log in with Google, and you will see the updated consent flow, which asks for your permission to allow the application access to the user's calendar. Below is an example of the consent flow. ![Descope - an example of a Google Consent flow asking for permission to the user's calendar](/assets/custom-scopes-google-example-flow.webp) ### Using Stored Provider Tokens #### Get the Provider Token After the user logs in, Descope securely stores the provider token. You can retrieve it using the [API Endpoint](/api/management/users/get-user-provider-token) or [Management SDK](/management/user-management/sdks#load-existing-users-provider-token). These requests require: * `loginId` - The user's login ID * `provider` - The provider name in lowercase (e.g., `google`, `apple`, `facebook`, `github`, `microsoft`) or your custom provider name Below is an example of the Descope API or Management SDK response. ```json { "provider": "google", "providerUserId": "xxxx", "accessToken": "xxxxxxxxxxxx", "expiration": 1695915480, "scopes": [ "openid", "email", "profile", "https://www.googleapis.com/auth/calendar" ] } ``` #### Use the Provider Token You can then utilize the provider token to interact with the user's Google Calendar. These actions are documented within the [Google Developer Guide](https://developers.google.com/calendar/api/guides/overview). The API endpoints available within CURL can also be found on the [Developer Google Docs](https://developers.google.com/calendar/api/v3/reference). The examples below are in CURL; however, you can also utilize various Google frontend and backend SDKs to perform these tasks. ##### Get a list of Calendars To start, you will want to get a list of the user's Calendars; below is an example of using an access key to list the user's calendars. The response will include the `id's` of the user's calendars for use in the following steps. These IDs can be the user's email, custom calendars, or generic calendars the user has added to their Google calendar. ```sh title="Terminal" curl -H "Authorization: Bearer " "https://www.googleapis.com/calendar/v3/users/me/calendarList" ``` ##### Get Events from a Calendar You can then utilize the access token to get the user's events with different query parameter flags. ```sh title="Terminal" curl -H "Authorization: Bearer " "https://www.googleapis.com/calendar/v3/calendars/user@email.com/events?maxResults=2&timeMin=2023-09-01T10:00:00-07:00" ``` ##### Create Events within Calendar You can utilize the access token to create an event on the user's calendar via the various supported methods. The below example uses the `quickAdd` method. This endpoint creates the event based on text data for the name and when. The created event is in the response. ```sh title="Terminal" curl -H "Authorization: Bearer " "https://www.googleapis.com/calendar/v3/calendars/user@email.com/events/quickAdd?text=Event%20Tomorrow%20at%2012pm" ``` # Enforce OAuth Login for Google Emails (/auth-methods/oauth/customize/force-oauth-google) Learn how to force OAuth for specific users based on their email domain, such as Google, within Descope flows. # Enforce OAuth for Google Emails Within flows, you might have included a condition block to force SSO on specific users that are enabled for it, as described [here](/auth-methods/sso/with-flows). Thinking along the same lines, if you would like to force a particular user to use OAuth by checking if their email address domain is from a specific MX domain like Google for example, you can do that with a special block in Flows. This guide will show you how to implement that. If you're unfamiliar with MX hostname records, you can read about them on Cloudflare's [docs](https://www.cloudflare.com/learning/dns/dns-records/dns-mx-record/). ## How to Check Email Address for Google Domain 1. Head over to your [flows](https://app.descope.com/flows), and select the one you want to integrate this feature with. 2. Select an Action block from the *Blue +*, called **Load Mail Provider Hostname**. This block will look up and return the DNS email exchange hostname record, which is used to direct email to a specific mail server. 3. Then, to utilize the information returned from the **Load Mail Provider** block, add a conditional block and search for the **mailProviderHost** attribute: ![Descope force google OAuth, configuring mail provider conditional within Descope 1](/assets/force-google-oauth-conditional-block-1.webp) 4. You can implement whatever logic you would like, however, if you would like to check for a *Google* hostname specifically, the block should look like this: ![Descope force google OAuth, configuring mail provider conditional within Descope 2](/assets/force-google-oauth-conditional-block-2.webp) If you would like to implement this feature, with other domains besides Google, you will need to know the MX domain hostname for your particular website. Then, in the conditional block, you can include your domain name here (you do not need to include a domain suffix such as .com). ![Descope force google OAuth, configuring mail provider conditional within Descope 3](/assets/force-google-oauth-conditional-block-3.webp) 5. Finally, connect the blocks together, add the rest of your flow logic, and you're done! ![Descope force google OAuth, configuring mail provider conditional location within Descope flow](/assets/force-google-oauth-location.webp) ## Final Result As an example, this flow forces a user to log in via Google if the email address is a Google domain, or sign them up using normal OTP if the email domain is something else: ![Descope force google OAuth, final flow example](/assets/force-google-oauth-example.webp) If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Handling OAuth Providers With Unverified Emails (/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails) Learn how to handle Social Authentication Provider That Provide Unverified Emails # Handling OAuth Providers with Unverified Emails Some OAuth providers do not consistently return verified email addresses, even after their verification process. This inconsistency can lead to security and trust issues for applications that rely on the verified email status. To address this, Descope takes a cautious approach and does not automatically merge social login identities with existing user accounts based solely on the email address. Merging accounts without a verified email could result in linking different individuals or exposing accounts to unauthorized access. ## Understanding Unverified Emails ### Example: Microsoft Authentication Here is an example of an unwanted outcome when a user signs up without email verification, specifically with Microsoft: ![Non-verified Microsoft Email](/assets/ms-unverified-email.webp) As shown above, the unverified email is marked with a warning. In this state, merging with existing user accounts is not possible, as well as any other operations that rely on a verified email. ## Email Verification Process When using Descope flows, you can verify email addresses using any kind of email-based authentication method, like OTP, Magic Link, etc. The following sections demonstrate how to do so using a Sign In or Sign Up flow, using Microsoft as the OAuth Provider. ### Sign In Flow This section assumes that user accounts have already been created by email, and you expect social login to work with the user's existing email address. Due to Microsoft's unverified email behavior, Descope creates a new user with the login ID `microsoft-xxxxxxx` when the email is unverified. This means that during sign in, the user won't be found automatically, as the email is not an additional login ID: ![Microsoft user not found](/assets/ms-sign-in-error.webp) To handle this situation, you can add error handling to the OAuth response within your flow, as shown below. The following flow is also available to use as a [template in the Flow Library](https://app.descope.com/flows?template=sign-in-allow-social-when-signups-not-allowed). #### Error Handling Process 1. **Expose the OAuth Raw Response** ![Raw OAuth](/assets/sign-in-oauth-raw.webp) 2. **Use Scriptlet to Set Overrides** ![scriptlet OAuth](/assets/scriptlet-oauth-set-id.webp) Add an email address override: ![scriptlet OAuth override](/assets/ms-oauth-scriptlet-override.webp) 3. **Verify the Email Address** ![OAuth verify](/assets/ms-oauth-verify-with-sign-in.webp) 4. **Merge the Accounts** ![OAuth merge](/assets/ms-oauth-merge-with-sign-in.webp) ### Sign Up Flow When allowing only sign ups, the user will be created with the Microsoft ID as the primary login ID. To handle this: 1. **Triage Based on Condition** ![Microsoft custom login triage](/assets/microsoft-custom-login-triage.webp) 2. **Merge Identity After Verification** ![Microsoft custom login triage](/assets/microsoft-custom-login-flow.webp) ## Best Practices ### Social Login Merging Settings When configuring "Social Login (OAuth/OIDC)" authentication: 1. Navigate to the "Email address handling" section for your Social Login Provider 2. Select "Promote the email to be a login ID" 3. Keep "Only if the email is verified" selected ![Microsoft merging tactic](/assets/microsoft-social-email-address-handling.webp) Selecting "Regardless of email verification status" will allow account takeover using the "NoAuth" attack vector mentioned [here](https://www.descope.com/blog/post/noauth). ### Blocking Self-Registration Settings When "Block self-registration sign up" is enabled in [project settings](https://app.descope.com/settings/project), it affects providers like Microsoft that return unverified emails. You must either: - Allow self-registration - Handle the error as described in the ["Sign In" section](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails#sign-in-flow) ### Email Verification Claims Microsoft provides the `xms_edov` (Email Domain Owner Verified) claim that indicates whether an email is domain-verified. Using this claim can bypass verification issues entirely. Refer to our [Microsoft OAuth Provider doc](/auth-methods/oauth/providers/setting-up-your-own-apps/microsoft#implementing-email-verification-with-microsoft-claims) to learn how to implement it in your flow. Other providers may offer similar claims with different names. Check your provider's documentation for available verification claims. # Utilizing A User Picture from OAuth Provider (/auth-methods/oauth/customize/picture-oauth-login) Learn how to use the picture attribute from a successful OAuth login response to display a user's profile picture in your web application. # Utilizing A User Picture from OAuth Provider When you successfully log in via OAuth, you will retrieve a URL link to the user's picture as part of the API response, if it exists. You can utilize this to display a profile picture for users in your web application or any other use case where you would like to associate a user with their picture, such as on an admin dashboard. This guide will explain how to use the picture attribute. ## Getting the Picture from the API Response It is very simple to use, you just need to embed the URL that comes from a successful OAuth API response, into your website somewhere. This picture is always accessible via the URL that is returned from the API. *This response is from a successful Google social login:* ```json { "sessionJwt": "eyJhbGciOiJSUzI...", "refreshJwt": "eyJhbGciOiJ...", "cookieDomain": "", "cookiePath": "/", "cookieMaxAge": 2419199, "cookieExpiration": 1685116422, "user": { "loginIds": [ "google-12345678", "example@descope.com" ], "userId": "ABCD1234", "name": "John Smith", "email": "example@descope.com", "phone": "", "verifiedEmail": true, "verifiedPhone": false, "roleNames": [], "userTenants": [], "status": "enabled", "externalIds": [ "google-12345678", "example@descope.com" ], "picture": "https://lh3.googleusercontent.com/a/AGNmyxbo5GOSgMt6yloUhLPrlqHwN-bdMxQR89M1ESze=s96-c", "test": false, "customAttributes": {}, "createdTime": 1682612331 }, "firstSeen": false } ``` *You can see that `picture` contains a URL to the profile picture.* An example using HTML would be something like this in the front end of a web application to show the profile picture: ```html

Check out my cool image!

JS ``` If the user is coming from OAuth, you cannot set this picture attribute either in the [Console](https://app.descope.com/users) or through any of the [user management APIs](/api/management/users), as this comes from Google and not Descope. If you have any other questions about Descope or OAuth user attributes, feel free to reach out to [us](/support)! # Unsupported WebView for OAuth (/auth-methods/oauth/customize/unsupported-webview-oauth) Learn how to handle any disallowed useragent error coming from providers like Google using Descope Flows. # Unsupported WebView for OAuth In scenarios where you are accessing a company's page or a website via LinkedIn or any other native app and try to log in using Google, you may encounter an '**Access Blocked**' or a '**403 disallowed_useragent**' error. This is a known, common, and very strict limitation imposed by some providers (e.g., Google). It occurs because you cannot run OAuth from a WebView, which goes against Google's 'Use secure browser' policy. ## Steps to reproduce 1. Search for any company on native LinkedIn App which redirects to a website with a Google sign in. 2. Click on "Visit Website" button or company's website link provided on the company's page. 3. Sign in via Google. You will see the error pop up as shown below. ![Error 403](/assets/disallowed-useragent-error.webp) ## How does Descope handle this? Descope supports a conditional key `device.oauthSupport` which can be used to identify an unsupported WebView for Google OAuth. 1. Head over to your [flows](https://app.descope.com/flows), and select the one you want to integrate this condition with. 2. Select a Condition block from the *Blue +*. 3. Add an if else condition to handle routes if webview is identified. ![Webview condition](/assets/webview-condition.webp) 4. You can implement whatever logic you'd like for the else condition which encounters the "access blocked" situation. 5. Finally, connect the blocks together, add the rest of your flow logic, and you're done! ## Test the condition As an example, this flow below handles the error by telling the user to sign in directly from the website using a browser therefore avoiding the "access blocked" error. ![Webview flow](/assets/webview-flow.webp) Either host your [own](/identity-federation/auth-hosting#self-hosted) application to use this flow example or use Descope Explorer (*https://explorer.descope.com/?project=YOUR_DESCOPE_PROJECT_ID&flow=FLOW_ID*) and provide your project and flow ID in the link. Send this link as a message on LinkedIn and test the flow. There are several ways of handling this once the condition is set in Descope Flows as shown above. Users can either choose not to show the Google button in WebView or provide the button but display a screen/message guiding the user to copy the link and open the page in a native browser (Safari, Chrome, etc.). If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Configuring OAuth Providers (/auth-methods/oauth/providers) Learn how to configure default and custom OAuth providers with Descope for your application's social login. # Configuring OAuth Providers For **production use**, we enforce [setting up your own OAuth providers](/auth-methods/oauth/providers) with custom branding and settings. Descope supports multiple **OAuth (Social Login)** providers out of the box, such as Google, Apple, Microsoft, Facebook, and GitHub. These default providers are designed to allow you to get off the ground quickly and start testing social login in your application. This guide explains how to use your own [OAuth applications](/auth-methods/oauth#what-is-an-oauth-provider-and-application) with your own credentials, as well as how to integrate **custom OAuth providers** that are not available by default. When you configure your own OAuth application, Descope will use your own client ID, secrets, and redirect URI — giving you full control over branding, permissions, and app ownership. ## Default Providers Each provider card below contains detailed setup instructions for replacing Descope's default OAuth application with your own application: } href="/auth-methods/oauth/providers/setting-up-your-own-apps/apple" title="Apple" description="Configure Apple OAuth with your own credentials" /> } href="/auth-methods/oauth/providers/setting-up-your-own-apps/facebook" title="Facebook" description="Configure Facebook OAuth with your own credentials" /> } href="/auth-methods/oauth/providers/setting-up-your-own-apps/github" title="GitHub" description="Configure GitHub OAuth with your own credentials" /> } href="/auth-methods/oauth/providers/setting-up-your-own-apps/google" title="Google" description="Configure Google OAuth with your own credentials" /> } href="/auth-methods/oauth/providers/setting-up-your-own-apps/microsoft" title="Microsoft" description="Configure Microsoft OAuth with your own credentials" /> ## Custom OAuth Providers If you want to integrate an OAuth provider not included by default (for example, **[Login.gov](/auth-methods/oauth/providers/custom-providers/logingov)**, **[Spotify](/auth-methods/oauth/providers/custom-providers/spotify)**, **LinkedIn**, or any other custom identity provider), you can set it up as a custom OAuth provider. Visit the [Custom OAuth Provider](/auth-methods/oauth/providers/custom-providers) guide to learn how to connect any generic OAuth provider and sign in with it, using Descope. # Backend SDKs (/auth-methods/oauth/with-sdks/backend) Add OAuth social logins to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Social Login (OAuth) with Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/oauth/with-sdks/client). To get started with authentication using Social Login (OAuth), refer to our [Social Login Documentation](/auth-methods/oauth). Continue reading to learn how to integrate Social Login into your application using our Backend SDKs. If you are securing a **Model Context Protocol (MCP) server**, use the [Descope Python MCP SDK](/mcp/sdks/python) (`descope-mcp`) instead of the standard backend SDK. It handles OAuth 2.1 token validation, scope enforcement, and connection token retrieval specifically for MCP servers. See the [Agentic Identity Hub](/agentic-identity-hub/core-components/mcp-servers) for full setup instructions. ## Start OAuth To initiate the OAuth process, call the OAuth initiation function after the user clicks the social login button. This function returns a pre-formatted URL that the client can use to redirect the user and begin the login flow with the selected Identity Provider (e.g., Google, Facebook, Microsoft). ```javascript // Args: // provider: social identity provider for authenticating the user. Supported values include "facebook", "github", "google", "microsoft", "gitlab" and "apple". The current list can be found at https://github.com/descope/descope-js/blob/main/packages/sdks/core-js-sdk/src/sdk/oauth/types.ts in the OAuthProviders array. const provider = "facebook" // redirect_url: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirect_url = "https://auth.company.com/token_exchange" // login_hint: Optional hint (e.g., user email) to pre-fill the identity provider's login form. const login_hint = "user@example.com" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.oauth.start[provider](redirect_url, login_hint, loginOptions); if (!resp.ok) { console.log("Failed to start oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const provider_url = resp.data.url console.log("Successfully started oauth. URL: " + provider_url) } ``` ```python # Args: # provider: social identity provider for authenticating the user. Supported values include "facebook", "github", "google", "microsoft", "gitlab" and "apple". The current list can be found at https://github.com/descope/python-sdk/blob/main/descope/common.py in the OAuthProvider array. provider = "facebook" # redirect_url: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. redirect_url = "https://auth.company.com/token_exchange" # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa try: resp = descope_client.oauth.start(provider=provider, return_url=redirect_url, login_options=login_options) print ("Successfully started Oauth flow") print (resp) except AuthException as error: print ("Failed to start Oauth flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // provider: This is the OAuthProvider such as facebook, google, etc provider := descope.OAuthFacebook // returnURL: url for redirecting the user after authentication with social oauth provider. This value will override the value in the console settings. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. returnURL := "https://auth.company.com/token_exchange" // loginHint: Optional hint (e.g., user email) to pre-fill the identity provider's login form. loginHint := "user@example.com"; // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } // w: ResponseWriter to update with correct redirect url. You can return this to your client for redirect. // Use either the SignUpOrIn, SignUp, or SignIn function res, err := descopeClient.Auth.OAuth.SignUpOrIn(ctx, provider, returnURL, loginHint, r, loginOptions, w) if (err != nil){ fmt.Println("Failed to initialize oauth flow: ", err) } else { fmt.Println("Successfully started Oauth flow: ", res) } ``` ```java // Choose an oauth provider out of the supported providers // If configured globally, the return URL is optional. If provided however, it will be used // instead of any global configuration. // Redirect the user to the returned URL to start the OAuth redirect chain OAuthService oas = descopeClient.getAuthenticationServices().getOAuthService(); try { String returnUrl = "https://my-app.com/handle-oauth"; oas.start("google", returnUrl, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.oauth_start( provider: 'google', # Choose an oauth provider out of the supported providers return_url: 'https://my-app.com/handle-oauth', # Can be configured in the console instead of here ) ``` ```csharp // Args: // provider (string): The OAuth provider (e.g., "google", "facebook", "microsoft"). var provider = "google"; // redirectUrl (string?): Optional redirect URL; if null, the default redirect URL in Descope console will be used. string? redirectUrl = "https://auth.company.com/token_exchange"; // loginOptions (LoginOptions): Step-up / MFA options (passed as the request body). var loginOptions = new LoginOptions { Stepup = false, Mfa = false, }; // Sign up a new user try { var response = await descopeClient.Auth.V1.Oauth.Authorize.Signup.PostAsync( loginOptions, conf => { conf.QueryParameters.Provider = provider; conf.QueryParameters.RedirectUrl = redirectUrl; }); var url = response?.Url; } catch (DescopeException ex) { // Handle the error } // Sign in an existing user try { var response = await descopeClient.Auth.V1.Oauth.Authorize.Signin.PostAsync( loginOptions, conf => { conf.QueryParameters.Provider = provider; conf.QueryParameters.RedirectUrl = redirectUrl; }); var url = response?.Url; } catch (DescopeException ex) { // Handle the error } // Sign up or sign in depending on whether the user exists try { var response = await descopeClient.Auth.V1.Oauth.Authorize.PostAsync( loginOptions, conf => { conf.QueryParameters.Provider = provider; conf.QueryParameters.RedirectUrl = redirectUrl; }); var url = response?.Url; } catch (DescopeException ex) { // Handle the error } ``` ## Finish OAuth (Exchange Token) After the user authenticates with the OAuth provider, they will be redirected to the `redirect_url` you specified. However, to complete the login process with Descope, you'll need to extract the code from the URL and perform the token exchange which will complete the OAuth flow: ```javascript // Args: // code: code extracted from the url after user is redirected to redirect_url. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const response = await descopeClient.oauth.exchange(code); if (!resp.ok) { console.log("Failed to finish oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished oauth.") console.log(resp) } ``` ```python # Args: # code: code extracted from the url after user is redirected to redirect_url. The code is in the url as a query parameter "code" of the page. code = "xxxxx" try: resp = descope_client.oauth.exchange_token(code=code) print ("Successfully Finished Oauth flow") print (resp) except AuthException as error: print ("Failed to finish Oauth flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // code (string): code should be extracted from the redirect URL of OAuth authentication from the query parameter `code`. code := "xxxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation authInfo, err := descopeClient.Auth.OAuth.ExchangeToken(ctx, code, w) if (err != nil){ fmt.Println("Failed to finish Oauth flow: ", err) } else { fmt.Println("Successfully Finished Oauth flow: ", authInfo) } ``` ```java OAuthService oas = descopeClient.getAuthenticationServices().getOAuthService(); try { AuthenticationInfo info = oas.exchangeToken(code); } catch (DescopeException de) { // Handle the error } ``` ```ruby jwt_response = descope_client.oauth_exchange_token(code) session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```csharp // Args: // code (string): code extracted from the url after user is redirected to redirect_url. The code is in the url as a query parameter "code" of the page. var code = "authorization-code"; try { var authInfo = await descopeClient.Auth.V1.Oauth.Exchange.PostAsync( new ExchangeTokenRequest { Code = code }); } catch (DescopeException ex) { // Handle the error } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/oauth/with-sdks/client) Add OAuth social logins to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Social Login (OAuth) with Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. Descope supports many social logins such as Google, Facebook, Microsoft, etc. You can find the currently supported list of social logins in the Descope console at [Settings>Authentication Methods>Social Login (OAuth)](https://app.descope.com/settings/authentication/social). The Descope console has the defaults set for all social logins. You can customize these by [configuring the social logins](/auth-methods/oauth) with your company account. ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### Start OAuth To initiate the OAuth process, call the OAuth initiation function after the user clicks the social login button. This function automatically redirects the user to the selected OAuth provider's login screen. ```javascript // Args: // provider: social identity provider for authenticating the user. Supported values include "facebook", "github", "google", "microsoft", "gitlab" and "apple". The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. const provider = "facebook" // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"}, "loginHint": "user@example.com" // (optional) login_hint for pre-filling user info } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.oauth.start[provider](redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const provider_url = resp.data.url console.log("Successfully started oauth. URL: " + provider_url) } ``` ```javascript // Args: // provider: social identity provider for authenticating the user. Supported values include "facebook", "github", "google", "microsoft", "gitlab" and "apple". The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. const provider = "facebook" // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"}, "loginHint": "user@example.com" // (optional) login_hint for pre-filling user info } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.oauth.start[provider](redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const provider_url = resp.data.url console.log("Successfully started oauth. URL: " + provider_url) } ``` ```html ``` ### Finish OAuth After successful authentication with your IdP the user is redirected to the redirectURL that you provide in the `oauth start` function above. Your application should extract the code from the redirectURL and perform token exchange as shown below. ```javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const descopeSdk = useDescope(); const response = await descopeSdk.oauth.exchange(code); if (!resp.ok) { console.log("Failed to finish oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished oauth.") console.log(resp) } ``` ```javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const response = await descopeSdk.oauth.exchange(code); if (!resp.ok) { console.log("Failed to finish oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished oauth.") console.log(resp) } ``` ```html ``` # Mobile SDKs (/auth-methods/oauth/with-sdks/mobile) Add OAuth social logins to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Social Login (OAuth) with Mobile SDKs Descope supports many social logins such as Google, Facebook, Microsoft, etc. You can find the currently supported list of social logins in the Descope console at [Settings>Authentication Methods>Social Login (OAuth)](https://app.descope.com/settings/authentication/social). The Descope console has the defaults set for all social logins. You can customize these by [configuring the social logins](/auth-methods/oauth) with your company account. ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## Start OAuth To initiate the OAuth process, call the OAuth initiation function after the user clicks the social login button. This function automatically opens the selected OAuth provider's login screen in a browser webview. ```swift // Args: // provider: social identity provider for authenticating the user. Supported values include OAuthProvider.facebook, OAuthProvider.github, OAuthProvider.google, OAuthProvider.microsoft, OAuthProvider.gitlab and OAuthProvider.apple. The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. let provider = OAuthProvider.facebook // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. let redirectURL = "exampleauthschema://auth.company.com/handle-oauth" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { let authURL = try await Descope.oauth.start(provider: provider, redirectURL: redirectURL, options: signInOptions) guard let authURL = URL(string: authURL) else { return } print("Successfully Initiated OAuth Authentication") } catch { print("Failed to Initiate OAuth Authentication") print(error) } ``` ```kotlin // Args: // provider: social identity provider for authenticating the user. Supported values include OAuthProvider.facebook, OAuthProvider.github, OAuthProvider.google, OAuthProvider.microsoft, OAuthProvider.gitlab and OAuthProvider.apple. The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { val provider = OAuthProvider.facebook val redirectURL = "exampleauthschema://auth.company.com/handle-oauth" // Use either the signUpOrIn, signUp, or signIn function Descope.oauth.signUpOrIn( provider, redirectURL, options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart / Args: // provider: social identity provider for authenticating the user. Supported values include OAuthProvider.facebook, OAuthProvider.github, OAuthProvider.google, OAuthProvider.microsoft, OAuthProvider.gitlab and OAuthProvider.apple. The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. const provider = OAuthProvider.facebook; // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = 'exampleauthschema://my-app.com/handle-oauth'; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // Choose an oauth provider out of the supported providers // If configured globally, the redirect URL is optional. If provided however, it will be used // instead of any global configuration. final authUrl = await Descope.oauth.start( provider: provider, redirectUrl: redirectURL, options: options); ``` ``` javascript // Args: // provider: social identity provider for authenticating the user. Supported values include "facebook", "github", "google", "microsoft", "gitlab" and "apple". The current list can be found at https://github.com/descope/core-js-sdk/blob/main/src/sdk/oauth/types.ts in the OAuthProviders array. const provider = "facebook" // redirectURL: URL to return to after successful authentication with the social identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "exampleauthschema://auth.company.com/handle-oauth" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.oauth.start[provider](redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const provider_url = resp.data.url console.log("Successfully started oauth. URL: " + provider_url) } ``` ## Finish OAuth After successful authentication with your IdP the user is redirected to the redirect_url that you provide in the `oauth start` function above. Your application should extract the code from the redirect_url and perform token exchange as shown below. ```swift // Args: // authURL: the authURL generated from the Start OAuth let authURL = "xxxxx" do { let session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "exampleauthschema") { callbackURL, error in guard let url = callbackURL else {return} let component = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let code = component?.queryItems?.first(where: {$0.name == "code"})?.value else { return } // Exchange code for session Task { do { let descopeSession = try await Descope.oauth.exchange(code: code) print("Successfully completed OAuth Authentication") print(descopeSession as Any) } catch { print("Failed to complete OAuth Authentication") print(error) } } } session.presentationContextProvider = self session.prefersEphemeralWebBrowserSession = true session.start() } catch { print("Failed to complete OAuth Authentication") print(error) } ``` ```kotlin // Args: // authURL: the authURL generated from the Start OAuth val code = "xxxxxx" // Code from authURL try { if (code != null) { val descopeSession = Descope.oauth.exchange(code) println("Successfully completed OAuth Authentication") println(descopeSession) } } catch (exception: Exception) { println("Failed to complete OAuth Authentication") println(exception) } ``` ```dart // Args: // authURL: the authURL generated from the Start OAuth const authURL = "xxxxx" // Redirect the user to the returned URL to start the OAuth redirect chain final result = await FlutterWebAuth.authenticate( url: authUrl, callbackUrlScheme: 'exampleauthschema'); // Extract the returned code final code = Uri.parse(result).queryParameters['code']; // Exchange code for an authentication response final authResponse = await Descope.oauth.exchange(code: code!); ``` ``` javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const descopeSdk = useDescope(); const response = await descopeSdk.oauth.exchange(code); if (!resp.ok) { console.log("Failed to finish oauth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished oauth.") console.log(resp) } ``` ## Native OAuth When running in iOS or Android, you can leverage the [Sign in with Apple](https://developer.apple.com/sign-in-with-apple/) and [Sign in with Google](https://developer.android.com/training/sign-in/credential-manager) features to show a native authentication view that allows the user to login using the account they are already logged into on their device. Before you can use these features, you will need to configure your application to support them. For iOS, you will need to complete the following Sign in with Apple [configuration steps](https://developer.apple.com/sign-in-with-apple/get-started/). For Android, you will need to complete the following [configuration steps](https://developer.android.com/training/sign-in/credential-manager). ### Configure Google OAuth for Android Go to the **Google Cloud Console** and you'll need to create two Client IDs: 1. **Android** - Used for native Android authentication 2. **Web Application** - Used for web-based OAuth flows #### Android App Configuration - Check the Package Name in the Client ID for Android to be `com.descopereactnativeapp` - Ensure that the SHA-1 certificate fingerprint is present in the Client ID for Android #### Web Application Configuration 1. **Configure the Authorized Redirect URI:** - Set the authorized redirect URI to: `https://api.descope.com/v1/oauth/callback` 2. **Copy the Client ID and Client Secret:** - Go to the [Descope Console](https://app.descope.com) - Navigate to **Settings -> Authentication Methods -> Social Login (OAuth)** - Under the Google Provider, choose **"Use my own account"** - Paste the Client ID and Client Secret - In the allowed grant types make sure to add **Implicit** grant type for **Native experience** After configuration, you can use the following code to initiate the native authentication flow: ```swift // Swift currently supports only iOS native authentication do { showLoading(true) let authResponse = try await Descope.oauth.native(provider: .apple, options: []) let session = DescopeSession(from: authResponse) Descope.sessionManager.manageSession(session) showHomeScreen() } catch DescopeError.oauthNativeCancelled { showLoading(false) print("Authentication canceled") } catch { showError(error) } ``` ```kotlin try { val context: Context = this@MyActivity val provider = OAuthProvider.Google val authRes = Descope.oauth.native( context, provider, options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) println("Successfully completed OAuth Authentication") println(authRes) } catch (exception: Exception) { println("Failed to complete OAuth Authentication") println(exception) } ``` ```dart void loginWithOAuth() async { AuthenticationResponse response; if (!kIsWeb && Platform.isIOS) { // created a custom Apple provider using the app bundle identifier as the Client ID response = await Descope.oauth.native(provider: OAuthProvider.named("apple")); } else if (!kIsWeb && Platform.isAndroid) { // created a custom Google provider for implicit authentication response = await Descope.oauth.native(provider: OAuthProvider.named("google")); } else { // regular web OAuth } final session = DescopeSession.fromAuthenticationResponse(response) // ... } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # Backend SDKs (/auth-methods/otp/with-sdks/backend) Add one-time password (OTP) authentication to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # OTP Authentication with Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/otp/with-sdks/client). A one-time password (OTP) is an automatically generated string sent to the user during the onboarding (sign-up or sign-in) process to authenticate that user. The OTP can be sent to an email address or a mobile phone as a voice call or SMS (text message). A typical method for implementing OTP has two sets of functionality you need to program: user interaction and session verification. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/otp/with-sdks/backend#user-sign-up) then [User Verification](/auth-methods/otp/with-sdks/backend#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/otp/with-sdks/backend#user-sign-in) then [User Verification](/auth-methods/otp/with-sdks/backend#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/otp/with-sdks/backend#user-sign-up-or-sign-in) then [User Verification](/auth-methods/otp/with-sdks/backend#user-verification) ## User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the delivery_method to sms within the below example. Note that signup is not complete without the user verification step below. ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.otp.signUp[deliveryMethod](loginId, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```python # Args: # user: Optional user object to populate new user information. user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # login_id: email or phone - becomes the loginId for the user from here on and also used for delivery login_id = "email@company.com" # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: descope_client.otp.sign_up(method=delivery_method, login_id=login_id, user=user, signup_options=signup_options) print ("Successfully initialized signup flow") except AuthException as error: print ("Failed to initialize signup flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) -------------- or (use a Python Decorator) -------------- from descope.flask import descope_signup_otp_by_email # You can pass email in the request body of the endpoint @app.route('/signup/email', methods=['POST']) @descope_signup_otp_by_email(descope_client) def signup_email(): return "Signup email sent" ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - becomes the loginId for the user from here on and also used for delivery loginID := "email@company.com" // user: Optional user object to populate new user information. user := &descope.User{ Name: "Joe Person", Phone: "+15555555555", Email: loginID, } // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } err := descopeClient.Auth.OTP().SignUp(ctx, deliveryMethod, loginID, user, signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup flow: ", err) } else { fmt.Println("Successfully initialized signup flow") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String maskedAddress = otps.signUp(DeliveryMethod.EMAIL, loginId, user, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Every user must have a login ID. All other user information is optional # For sign up either phone or email is required email = 'desmond@descope.com' user = {'name': 'Desmond Copeland', 'phone': '+15555555555', 'email': email} masked_address = descope_client.otp_sign_up(method: Descope::Mixins::Common::DeliveryMethod::EMAIL , login_id: 'someone@example.com', user: user) ``` ```csharp // Args: // loginId (string): email or phone - becomes the loginId for the user from here on and also used for delivery. var loginId = "email@company.com"; // request (OTPSignUpEmailRequest): Signup payload. For SMS / Voice / WhatsApp, use OTPSignUpPhoneRequest // and call Signup.Sms / Signup.Voice / Signup.Whatsapp instead of Signup.Email. var request = new OTPSignUpEmailRequest { LoginId = loginId, Email = loginId, User = new SignUpUser { Name = "Desmond Copeland", GivenName = "Desmond", FamilyName = "Copeland", Phone = "+15555555555", Email = loginId, }, }; try { // Returns EmailOperationResponse with MaskedEmail (or PhoneOperationResponse.MaskedPhone for phone channels). var response = await descopeClient.Auth.V1.Otp.Signup.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; } catch (DescopeException ex) { // Handle the error } ``` ## User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the OTP verification will be sent by email to `email@company.com`. Note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.otp.signIn[deliveryMethod](loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```python # Args: # login_id: email or phone - login_id for the user, also used for delivery login_id = "email@company.com" # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa try: descope_client.otp.sign_in(method=delivery_method, login_id=login_id, login_options=login_options) print ("Successfully initialized signin flow") except AuthException as error: print ("Failed to initialize signin flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. This can also be nil // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } res, err := descopeClient.Auth.OTP().SignIn(ctx, deliveryMethod, loginID, nil, loginOptions) if (err != nil){ fmt.Println("Failed to initialize signin flow: ", err) } else { fmt.Println("Successfully initialized signin flow", res) } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var loginOptions = LoginOptions.builder() .stepUp(true) .mfa(true) .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String maskedAddress = otps.signIn(DeliveryMethod.EMAIL, loginId, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Sign in (log in) an existing user with the unique login_id you provide. # The login_id field is used to identify the user. It can be an email address or a phone number. # Provide the DeliveryMethod required for this user. If the login_id value cannot be used for the # DeliverMethod selected (for example, 'login_id = 4567qq445km' and 'DeliveryMethod = email') email = 'desmond@descope.com' user = {'name': 'Desmond Copeland', 'phone': '+15555555555', 'email': email} masked_address = descope_client.otp_sign_in(method: Descope::Mixins::Common::DeliveryMethod::EMAIL , login_id: 'someone@example.com') ``` ```csharp // Args: // loginId (string): The user’s login ID. var loginId = "email@company.com"; // request (OTPSignInRequest): Sign-in payload. For SMS / Voice / WhatsApp, call Signin.Sms / Signin.Voice / Signin.Whatsapp instead. var request = new OTPSignInRequest { LoginId = loginId, LoginOptions = new LoginOptions { Stepup = false, Mfa = false, }, }; try { // Returns EmailOperationResponse with MaskedEmail (or PhoneOperationResponse.MaskedPhone for phone channels). var response = await descopeClient.Auth.V1.Otp.Signin.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; } catch (DescopeException ex) { // Handle the error } ``` ## User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the delivery_method to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.otp.signUpOrIn[deliveryMethod](loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```python # Args: # login_id: email or phone - login_id for the user, also used for delivery login_id = "email@company.com" # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # signup_options (SignUpOptions): this allows you to configure behavior during the authentication process. signup_options = { "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } try: descope_client.otp.sign_up_or_in(method=delivery_method, login_id=login_id, signup_options=signup_options) print ("Successfully initialized signup or in flow") except AuthException as error: print ("Failed to initialize signup or in flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // signUpOptions: this allows you to configure behavior during the authentication process. signUpOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } err := descopeClient.Auth.OTP().SignUpOrIn(ctx, deliveryMethod, loginID, signUpOptions) if (err != nil){ fmt.Println("Failed to initialize signup or in flow: ", err) } else { fmt.Println("Successfully initialized signup or in flow") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; var signUpOptions = SignupOptions.builder() .customClaims( new HashMap() {{ put("custom-key1", "custom-value1");}} ) .templateOptions( new HashMap() {{ put("option", "Value1");}} ) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String maskedAddress = otps.signUpOrIn(DeliveryMethod.EMAIL, loginId, signUpOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Sign_up_or_in lets you handle both sign up and sign in with a single call. # The login_id field is used to identify the user. It can be an email address or a phone number. # Sign-up_or_in will first determine if login_id is a new or existing end user. # If login_id is new, a new end user user will be created and then authenticated using the OTP DeliveryMethod (either email, sms, or voice) specified. # If login_id exists, the end user will be authenticated using the OTP DeliveryMethod specified. masked_method = descope_client.otp_sign_up_or_in(method: Descope::Mixins::Common::DeliveryMethod::SMS , login_id: phone) ``` ```csharp // Args: // loginId (string): The user’s login ID. var loginId = "email@company.com"; // request (OTPSignInRequest): Signup-or-signin payload (same shape as sign-in). // For SMS / Voice / WhatsApp, call SignupIn.Sms / SignupIn.Voice / SignupIn.Whatsapp instead. var request = new OTPSignInRequest { LoginId = loginId, LoginOptions = new LoginOptions { Stepup = false, Mfa = false, }, }; try { // Returns EmailOperationResponse with MaskedEmail (or PhoneOperationResponse.MaskedPhone for phone channels). var response = await descopeClient.Auth.V1.Otp.SignupIn.Email.PostAsync(request); var maskedEmail = response?.MaskedEmail; } catch (DescopeException ex) { // Handle the error } ``` ## User Verification The next step in authenticating the user is to verify the code entered by the user, using `OTP verify code` function. The function will return all the necessary JWT tokens, claims and user information. You can use the JWT tokens for session validation in your application middleware or app server for every route needs an authenticated user. ```javascript // Args: // loginId (str): The loginId of the user being validated const loginId = "email@company.com" // code (str): The authorization code entered by the end user during signup/signin const code = "xxxxxx" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" const resp = await descopeClient.otp.verify[deliveryMethod](loginId, code); if (!resp.ok) { console.log("Failed to verify OTP code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified OTP ") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user being validated login_id = "email@company.com" # code (str): The authorization code enter by the end user during signup/signin code = "xxxxxx" # delivery_method: Method used to deliver the OTP. Supported delivery methods - DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: jwt_response = descope_client.otp.verify_code(method=delivery_method, login_id=login_id, code=code, audience=audience) print ("Successfully verified user") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to verify user") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Method used to deliver the OTP. Supported delivery methods - descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodSMS // loginID: email or phone - unique ID for the user loginID := "email@company.com" // code (string): The authorization code enter by the end user during sign-up/sign-in code := "xxxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation. This code should go into your application server route handling the verification of code. authInfo, err := descopeClient.Auth.OTP().VerifyCode(ctx, method, loginID, code, w) if (err != nil){ fmt.Println("Failed to verify user: ", err) } else { fmt.Println("Successfully verified user", authInfo) } ``` ```java // Will throw DescopeException if there is an error with update OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { AuthenticationInfo info = otps.verifyCode(DeliveryMethod.EMAIL, loginId, code); } catch (DescopeException de) { // Handle the error } ``` ```ruby #The requested_method can be either email, sms, or voice. #Login_id is the user's login id and value is the verification code. #jwt_response = descope_client.otp_verify_code( method: Descope::Mixins::Common::DeliveryMethod::EMAIL SMS or VOICE, login_id: 'someone@example.com', code: '123456') jwt_response = descope_client.otp_verify_code(method: Descope::Mixins::Common::DeliveryMethod::EMAIL, login_id: login_id, code: value) ``` ```csharp // Args: // loginId (string): The loginId of the user being validated. var loginId = "user@example.com"; // code (string): The authorization code entered by the end user during signup/signin. var code = "xxxxxx"; // request (OTPVerifyCodeRequest): Verify payload. For SMS / Voice / WhatsApp, call Verify.Sms / Verify.Voice / Verify.Whatsapp instead. var request = new OTPVerifyCodeRequest { LoginId = loginId, Code = code, }; try { // Returns JWTResponse with SessionJwt, RefreshJwt, and User. var authInfo = await descopeClient.Auth.V1.Otp.Verify.Email.PostAsync(request); } catch (DescopeException ex) { // Handle the error } ``` ## Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the OTP Code, you will need to host a page to verify the OTP code using the [OTP Verify Function](/auth-methods/otp/with-sdks/backend#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.otp.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP email update") console.log(resp.data) } ``` ```python # Args: # login_id (str): The login_id of the user being updated login_id = "email@company.com" # email (str): The new email address. If an email address already exists for this end user, it will be overwritten email = "newEmail@company.com" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxx" # add_to_login_ids (boolean): if true, the email will be appended to the login ID array. add_to_login_ids = True # on_merge_use_existing (boolean): if true, on merge, the existing user's information (roles, tenants, etc) will be retained on_merge_use_existing = True # template_options (dict): email template options template_options = {"option": "Value1"} try: jwt_response = descope_client.otp.update_user_email(login_id=login_id, email=email, refresh_token=refresh_token, add_to_login_ids=add_to_login_ids, on_merge_use_existing=on_merge_use_existing, template_options=template_options) print ("Successfully started OTP email update") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to start OTP email update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginId (str): The loginId of the user being updated loginID := "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten email := "newEmail@company.com" // updateOptions (&descope.UpdateOptions): this allows you to configure behavior during the authentication process. updateOptions := &descope.UpdateOptions{} updateOptions.AddToLoginIDs = false updateOptions.OnMergeUseExisting = false updateOptions.TemplateOptions = map[string]any{"option": "Value1"} // request (*http.Request): Request is needed to obtain JWT and send it to Descope, for verification res, err := descopeClient.Auth.OTP().UpdateUserEmail(ctx, loginID, email, updateOptions, request) if (err != nil){ fmt.Println("Failed to start OTP email update: ", err) } else { fmt.Println("Successfully started OTP email update", res) } ``` ```java // Will throw DescopeException if there is an error with update OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { AuthenticationInfo info = otps.updateUserEmail(loginId, email, refreshToken, UpdateOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.otp_update_user_email(method: ?, login_id: login_id, email: user_email) ``` ```csharp // Args: // loginId (string): The user’s current login ID. var loginId = "user-login-id"; // newEmail (string): The new email. If an email already exists for this end user, it will be overwritten. var newEmail = "email@company.com"; // refreshJwt (string): The session's refresh token (used for verification). var refreshJwt = "refresh-jwt"; // request (UpdateUserEmailOTPRequest): Update email payload with optional merge flags. var request = new UpdateUserEmailOTPRequest { LoginId = loginId, Email = newEmail, AddToLoginIDs = true, // also treat email as an additional login ID OnMergeUseExisting = true // keep existing user if merge needed }; try { // Returns EmailOperationResponse with MaskedEmail (e.g., "x***@company.com"). var response = await descopeClient.Auth.V1.Otp.Update.Email.PostWithJwtAsync(request, refreshJwt); var maskedEmail = response?.MaskedEmail; } catch (DescopeException ex) { // Handle the error } ``` ## Update Phone This function allows you to update the user's phone number address via SMS. This requires a valid refresh token. Once the user has received the OTP Code, you will need to host a page to verify the OTP code using the [OTP Verify Function](/auth-methods/otp/with-sdks/backend#user-verification). ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeClient.otp.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP phone update") console.log(resp.data) } ``` ```python # Args: # delivery_method: Method used to deliver the OTP. delivery_method = DeliveryMethod.SMS # login_id (str): The login_id of the user being updated login_id = "phone@company.com" # phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten phone = "+12223334455" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxx" # add_to_login_ids (boolean): if true, the phone will be appended to the login ID array. add_to_login_ids = True # on_merge_use_existing (boolean): if true, on merge, the existing user's information (roles, tenants, etc) will be retained on_merge_use_existing = True # template_options (dict): email template options template_options = {"option": "Value1"} try: jwt_response = descope_client.otp.update_user_phone(delivery_method=delivery_method, login_id=login_id, phone=phone, refresh_token=refresh_token, add_to_login_ids=add_to_login_ids, on_merge_use_existing=on_merge_use_existing, template_options=template_options) print ("Successfully started OTP phone update") print(json.dumps(jwt_response, indent=4)) except AuthException as error: print ("Failed to start OTP phone update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Method used to deliver the OTP. deliveryMethod := descope.MethodSMS // loginId (str): The loginId of the user being updated loginID := "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten phone := "+12223334455" // updateOptions (&descope.UpdateOptions): this allows you to configure behavior during the authentication process. updateOptions := &descope.UpdateOptions{} updateOptions.AddToLoginIDs = false updateOptions.OnMergeUseExisting = false updateOptions.TemplateOptions = map[string]any{"option": "Value1"} // request (*http.Request): Request is needed to obtain JWT and send it to Descope, for verificatio res, err := descopeClient.Auth.OTP().UpdateUserPhone(ctx, deliveryMethod, loginID, phone, updateOptions, request) if (err != nil){ fmt.Println("Failed to start OTP phone update: ", err) } else { fmt.Println("Successfully started OTP phone update", res) } ``` ```java // Will throw DescopeException if there is an error with update OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { AuthenticationInfo info = otps.updateUserPhone(deliveryMethod, loginId, phone, refreshToken, UpdateOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.otp_update_user_phone(method: ?, login_id: login_id, phone: user_phone) ``` ```csharp // Args: // loginId (string): The user’s current login ID. var loginId = "user-login-id"; // newPhone (string): The new phone number. If a phone number already exists for this end user, it will be overwritten. var newPhone = "+15555555555"; // refreshJwt (string): The session's refresh token (used for verification). var refreshJwt = "refresh-jwt"; // request (UpdateUserPhoneOTPRequest): Update phone payload with optional merge flags. // For Voice / WhatsApp, call Update.Phone.Voice / Update.Phone.Whatsapp instead of Update.Phone.Sms. var request = new UpdateUserPhoneOTPRequest { LoginId = loginId, Phone = newPhone, AddToLoginIDs = true, // also treat phone as an additional login ID OnMergeUseExisting = true // keep existing user if merge needed }; try { // Returns PhoneOperationResponse with MaskedPhone (e.g., "+1******555"). var response = await descopeClient.Auth.V1.Otp.Update.Phone.Sms.PostWithJwtAsync(request, refreshJwt); var maskedPhone = response?.MaskedPhone; } catch (DescopeException ex) { // Handle the error } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/otp/with-sdks/client) Add one-time password (OTP) authentication to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # OTP Authentication with Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. A one-time password (OTP) is an automatically generated string sent to the user during the onboarding (sign-up or sign-in) process to authenticate that user. The OTP can be sent to an email address or a mobile phone as a voice call or SMS (text message). A typical method for implementing OTP has two sets of functionality you need to program: user interaction and session verification. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/otp/with-sdks/client#user-sign-up) then [User Verification](/auth-methods/otp/with-sdks/client#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/otp/with-sdks/client#user-sign-in) then [User Verification](/auth-methods/otp/with-sdks/client#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/otp/with-sdks/client#user-sign-up-or-sign-in) then [User Verification](/auth-methods/otp/with-sdks/client#user-verification) ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the deliveryMethod to sms within the below example. Note that signup is not complete without the user verification step below. ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signUp[deliveryMethod](loginId, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.otp.signUp[deliveryMethod](loginId, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ```html ``` ### User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number). In this sample code, the OTP verification will be sent by email to `email@company.com`. Note that signin is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signIn[deliveryMethod](loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.otp.signIn[deliveryMethod](loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ```html ``` ### User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the deliveryMethod to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signUpOrIn[deliveryMethod](loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.otp.signUpOrIn[deliveryMethod](loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ```html ``` ### User Verification The next step in authenticating the user is to verify the code entered by the user, using `OTP verify code` function. The function will return all the necessary JWT tokens, claims and user information. You can use the JWT tokens for session validation in your application middleware or app server for every route needs an authenticated user. ```javascript // Args: // loginId (str): The loginId of the user being validated const loginId = "email@company.com" // code (str): The authorization code enter by the end user during signup/signin const code = "xxxxxx" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" const descopeSdk = useDescope(); const resp = await descopeSdk.otp.verify[deliveryMethod](loginId, code); if (!resp.ok) { console.log("Failed to verify OTP code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified OTP ") console.log(resp.data) } ``` ```javascript // Args: // loginId (str): The loginId of the user being validated const loginId = "email@company.com" // code (str): The authorization code enter by the end user during signup/signin const code = "xxxxxx" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" const resp = await descopeSdk.otp.verify[deliveryMethod](loginId, code); if (!resp.ok) { console.log("Failed to verify OTP code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified OTP ") console.log(resp.data) } ``` ```html ``` ### Update Email This function allows you to update the user's email address via email. This requires a valid refresh token. Once the user has received the OTP Code, you will need to host a page to verify the OTP code using the [OTP Verify Function](/auth-methods/otp/with-sdks/client#user-verification). ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP email update") console.log(resp.data) } ``` ```javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.otp.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP email update") console.log(resp.data) } ``` ```html ``` ### Update Phone This function allows you to update the user's phone number address via SMS. This requires a valid refresh token. Once the user has received the OTP Code, you will need to host a page to verify the OTP code using the [OTP Verify Function](/auth-methods/otp/with-sdks/client#user-verification). ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP phone update") console.log(resp.data) } ``` ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const resp = await descopeSdk.otp.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP phone update") console.log(resp.data) } ``` ```html ``` # Mobile SDKs (/auth-methods/otp/with-sdks/mobile) Add one-time password (OTP) authentication to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # OTP Authentication with Mobile SDKs A one-time password (OTP) is an automatically generated string sent to the user during the onboarding (sign-up or sign-in) process to authenticate that user. The OTP can be sent to an email address or a mobile phone as a text. A typical method for implementing OTP has two sets of functionality you need to program: user interaction and session verification. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/otp/with-sdks/mobile#user-sign-up) then [User Verification](/auth-methods/otp/with-sdks/mobile#user-verification) 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/otp/with-sdks/mobile#user-sign-in) then [User Verification](/auth-methods/otp/with-sdks/mobile#user-verification) 3. **Sign-Up or Sign-In (Signs up a new user or signs in an existing user)**: The following actions must be completed, first [User Sign-Up or Sign-In](/auth-methods/otp/with-sdks/mobile#user-sign-up-or-sign-in) then [User Verification](/auth-methods/otp/with-sdks/mobile#user-verification) ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## User Sign-Up For registering a new user, your application should accept user information, including an email or phone number used for verification. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the deliveryMethod to sms within the below example. Note that signup is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery let loginId = "email@company.com" // user: Optional user object to populate new user information. let user = User("name": "Joe Person", "phone": "+15555555555", "email": "email@company.com") do { try await Descope.otp.signUp(with: deliveryMethod, loginId: loginId, user: user) print("Successfully initiated OTP Sign Up") } catch { print("Failed to initiate OTP Sign Up") print(error) } ``` ```kotlin try { Descope.otp.signUp( method = DeliveryMethod.Email, loginId = "email@company.com", // Optional object to populate new user information. details = SignUpDetails( name = "firstName lastName", email = "email@company.com", phone = "+15555555555", givenName = "firstName", middleName = "middleName", familyName = "lastName" ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart Descope.otp.signUp(method: DeliveryMethod.email, loginId: loginId); ``` ``` javascript // Args: // user: Optional user object to populate new user information. const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signUp[deliveryMethod](loginId, user, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signup flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signup flow") } ``` ## User Sign-In For authenticating a user, your application should accept the user's identity (typically an email address or phone number). In this sample code, the OTP verification will be sent by email to `email@company.com`. Note that signin is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - the loginId of the user let loginId = "email@company.com" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { try await Descope.otp.signIn(with: deliveryMethod, loginId: loginId, options: signInOptions) print("Successfully initiated OTP Sign In") } catch { print("Failed to initiate OTP Sign In") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId: email or phone - the loginId of the user // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.otp.signIn( method = DeliveryMethod.Email, loginId = "email@company.com", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); Descope.otp.signIn(method: method, loginId: loginId, options: options); ``` ``` javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signIn[deliveryMethod](loginId, loginOptions); if (!resp.ok) { console.log("Failed to initialize signin flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signin flow") } ``` ## User Sign-Up or Sign-In For signing up a new user or signing in an existing user, you can utilize the `signUpOrIn` functionality. Only user loginId is necessary for this function. In this sample code, the OTP verification will be sent by email to `email@company.com`. To change the delivery method to send the OTP verification as a Text Message (SMS), you would change the deliveryMethod to sms within the below example. Note that signUpOrIn is not complete without the user verification step below. ```swift // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId: email or phone - the loginId of the user let loginId = "email@company.com" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { try await Descope.otp.signUpOrIn(with: deliveryMethod, loginId: loginId, options: signInOptions) print("Successfully initiated OTP Sign Up or In") } catch { print("Failed to initiate OTP Sign Up or In") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId: email or phone - the loginId of the user // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response try { Descope.otp.signUpOrIn( method = DeliveryMethod.Email, loginId = "email@company.com", options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); Descope.otp.signUpOrIn(method:method, loginId: loginId, options: options); ``` ``` javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process. const signUpOptions = { "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.signUpOrIn[deliveryMethod](loginId, signUpOptions); if (!resp.ok) { console.log("Failed to initialize signUpOrIn flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized signUpOrIn flow") } ``` ## User Verification The next step in authenticating the user is to verify the code entered by the user, using `OTP verify code` function. The function will return all the necessary JWT tokens, claims and user information. You can use the JWT tokens for session validation in your application middleware or app server for every route needs an authenticated user. ```swift // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms let deliveryMethod = DeliveryMethod.email // loginId (str): The loginId of the user being validated let loginId = "email@company.com" // code (str): The authorization code enter by the end user during signup/signin let code = "xxxx" do { let descopeSession = try await Descope.otp.verify(with: deliveryMethod, loginId: loginId, code: code) print("Successfully verified OTP Code") print(descopeSession as Any) } catch DescopeError.wrongOTPCode { print("Failed to verify OTP Code: ") print("Wrong code entered") } catch { print("Failed to verify OTP Code: ") print(error) } ``` ```kotlin // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms // loginId (str): The loginId of the user being validated // code (str): The authorization code enter by the end user during signup/signin let code = "xxxx" try { val authResponse = Descope.otp.verify( method = DeliveryMethod.Email, loginId = "email@company.com", code = "" ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include DeliveryMethod.email or DeliveryMethod.sms const deliveryMethod = DeliveryMethod.email; // loginId (str): The loginId of the user being validated const loginId = "email@company.com"; // code (str): The authorization code enter by the end user during signup/signin const code = "xxxx"; final authResponse = await Descope.otp .verify(method: method, loginId: loginId, code: code); ``` ``` javascript // Args: // loginId (str): The loginId of the user being validated const loginId = "email@company.com" // code (str): The authorization code enter by the end user during signup/signin const code = "xxxxxx" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" const descopeSdk = useDescope(); const resp = await descopeSdk.otp.verify[deliveryMethod](loginId, code); if (!resp.ok) { console.log("Failed to verify OTP code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified OTP ") console.log(resp.data) } ``` ## Update Email The Descope SDK allows for you to update user's email address. With this function, you will pass the user's `loginId` and the new email address you want associated to the user. In order to verify the email address, the OTP code will be sent via the email delivery method. Once the update email function has been called, the user will need to verify with the sent OTP code before the email address will be updated. ```swift // Args: // email: the new email address you want to associate with the user let email = "newEmail@company.com" // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // refreshJwt: The refreshJwt of the user to be updated let refreshJwt = "xxxxxx" do { try await Descope.otp.updateEmail(email, loginId: loginId, refreshJwt: refreshJwt) print("Successfully initiated OTP Email Update") } catch { print("Failed to initiate OTP Email Update") print(error) } ``` ```kotlin // Args: // email: the new email address you want to associate with the user // loginId: email or phone - the loginId of the user // refreshJwt: The refreshJwt of the user to be updated // options: optional options for loginId and merging behavior try { Descope.otp.updateEmail( email = "email2@gompany.com", loginId = "email@company.com", refreshJwt = "", options = UpdateOptions( addToLoginIds = true, onMergeUseExisting = true ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // email: the new email address you want to associate with the user const email = "newEmail@company.com"; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // refreshJwt: The refreshJwt of the user to be updated const refreshJwt = "xxxxxx"; /// You can optionally pass the [options] parameter to add the new phone number /// as a `loginId` for the existing user, and to determine how to resolve conflicts /// if another user already exists with the same `loginId`. Check out the // Update Options (https://github.com/descope/descope-flutter/blob/main/lib/src/types/others.dart) type for more details. final options = UpdateOptions( addToLoginIds: true, onMergeUseExisting: true ); Descope.otp .updateEmail(loginId: loginId, email: "email", refreshJwt: Descope.sessionManager.session!.refreshJwt, options: options); ``` ``` javascript // Args: // loginId (str): The loginId of the user being updated const loginId = "email@company.com" // email (str): The new email address. If an email address already exists for this end user, it will be overwritten const email = "newEmail@company.com" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await descopeSdk.otp.update.email(loginId, email, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP email update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP email update") console.log(resp.data) } ``` ## Update Phone The Descope SDK allows for you to update user's phone number. With this function, you will pass the user's `loginId` and the new phone number you want associated to the user. In order to verify the phone number, the OTP code will be sent via the sms delivery method. Once the update phone function has been called, the user will need to verify with the sent OTP code before the phone number will be updated. ```swift // Args: // phone: the new phone number you want to associate with the user let phone = "+12222222222" // loginId: email or phone - the loginId of the user let loginId = "email@company.com" // refreshJwt: The refreshJwt of the user to be updated let refreshJwt = "xxxxxx" do { try await Descope.otp.updatePhone(phone, with: .sms, loginId: loginId, refreshJwt: refreshJwt) print("Successfully initiated OTP Phone Update") } catch { print("Failed to initiate OTP Phone Update") print(error) } ``` ```kotlin // Args: // phone: the new phone number you want to associate with the user // deliveryMethod: the delivery method of verification otp // loginId: email or phone - the loginId of the user // refreshJwt: The refreshJwt of the user to be updated // options: optional options for loginId and merging behavior try { Descope.otp.updatePhone( phone = "+11231231234", method = "sms", loginId = "email@company.com", refreshJwt = "" options = UpdateOptions( addToLoginIds = true, onMergeUseExisting = true ) ) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Args: // deliveryMethod: the delivery method of verification otp const deliveryMethod = DeliveryMethod.email; // phone: the new phone number you want to associate with the user const phone = "+12222222222"; // loginId: email or phone - the loginId of the user const loginId = "email@company.com"; // refreshJwt: The refreshJwt of the user to be updated const refreshJwt = "xxxxxx"; /// You can optionally pass the [options] parameter to add the new phone number /// as a `loginId` for the existing user, and to determine how to resolve conflicts /// if another user already exists with the same `loginId`. Check out the // Update Options (https://github.com/descope/descope-flutter/blob/main/lib/src/types/others.dart) type for more details. final options = UpdateOptions( addToLoginIds: true, onMergeUseExisting: true ); Descope.otp .updatePhone(method: deliveryMethod, loginId: loginId, phone: phone, refreshJwt: refreshJwt, options: options); ``` ``` javascript // Args: // deliveryMethod: Delivery method to use to send OTP. const deliveryMethod = "sms" // loginId (str): The loginId of the user being updated const loginId = "phone@company.com" // phone (str): The new phone number. If a phone number already exists for this end user, it will be overwritten const phone = "+12223334455" // refreshToken (str): The session's refresh token (used for verification) const refreshToken = "xxxxx" // updateOptions (UpdateOptions): this allows you to configure behavior during the authentication process. const updateOptions = { "addToLoginIDs": true, "onMergeUseExisting": true, "templateOptions": {"option": "Value1"} } const descopeSdk = useDescope(); const resp = await useDescope.otp.update.phone(deliveryMethod, loginId, phone, refreshToken, updateOptions); if (!resp.ok) { console.log("Failed to start OTP phone update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started OTP phone update") console.log(resp.data) } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # Backend SDKs (/auth-methods/passkeys/with-sdks/backend) Add WebAuthn biometrics to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Passkey Authentication with Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/passkeys/with-sdks/client). Passkeys let users authenticate with phishing-resistant credentials based on WebAuthn. These credentials can be created and used through built-in device authenticators, such as fingerprint, facial recognition, or device PIN, as well as external security keys, such as YubiKeys or other FIDO-compatible hardware authenticators. When implementing passkey authentication with Descope Backend SDKs, your application is responsible for coordinating the browser or native app passkey ceremony and then sending the resulting WebAuthn response to Descope. A typical backend SDK implementation includes the following flows: - [Start and finish passkey sign-up](#start-sign-up) - [Start and finish passkey sign-in](#start-sign-in) - [Add a passkey to an existing user](#add-a-passkey-to-an-existing-user) - [Validate the resulting Descope session](#validate-the-resulting-descope-session) ## Start Sign-Up Start the passkey sign-up flow by calling the sign-up start function. This function requires a unique `loginId`, such as an email address or phone number. Descope uses this value as the user's login ID and associates the passkey credentials with it. The function also requires an `origin` value. This should be the value of `window.location.origin` from your application client. Descope validates this origin against the domain configured in the Descope console. The origin must match the configured domain or be a valid subdomain. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. const origin = "https://example.com" // displayName: Display name to utilize for the user const displayName = "Joe Person" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeClient.auth.webauthn.signUp.start(loginId, origin, displayName, loginOptions); if (!resp.ok) { console.log("Unable to start webauthn sign-up") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started webauthn sign-up") console.log(resp) } ``` ```python # Args: # login_id: email or phone - becomes the loginId for the user from here on and also used for delivery login_id = "email@company.com" # origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin = "http://example.com" # user: Optional user object to populate new user information. user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # login_options (LoginOptions): this allows you to configure behavior during the authentication process. login_options = { "stepup": false, "mfa": false, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} } # refresh_token (optional): the user's current refresh token in the event of stepup/mfa try: resp = descope_client.webauthn.sign_up_start(login_id=login_id, origin=origin, user=user, login_options=login_options) print ("Successfully started webauthn sign-up") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to start webauthn sign-up") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - becomes the loginId for the user from here on and also used for delivery loginID := "email@company.com" // user: Optional user object to populate new user information. user := &descope.User{Name:"Joe", Email:"email@company.com", Phone:"+15555555555"} // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin := "https://example.com" // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } res, err := descopeClient.Auth.WebAuthn().SignUpStart(ctx, loginID, user, origin, nil, loginOptions) if (err != nil){ fmt.Println("Unable to start webauthn sign-up: ", err) } else { fmt.Println("Successfully started webauthn sign-up: ", res) } ``` ```java // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery String loginId = "email@company.com"; // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. String origin = "https://example.com"; // user: Optional user object to populate new user information. User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { WebAuthnTransactionResponse resp = webAuthn.signUpStart(loginId, user, origin); // Return resp.getTransactionId() and resp.getOptions() to your client } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID for the user — becomes their permanent login ID // Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting // User (SignUpUser) optional: Additional user metadata // PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON) var response = await client.Auth.V1.Webauthn.Signup.Start.PostAsync( new WebauthnSignUpStartRequest { LoginId = "email@company.com", Origin = "https://example.com", User = new SignUpUser { Name = "Joe Person", Email = "email@company.com" }, PasskeyOptions = new PasskeyOptions { /* ... */ } }); // response.TransactionId — pass to Finish Sign-Up along with the browser's credential response ``` ## Finish Sign-Up After starting the sign-up flow, Descope returns a `transactionId`. Your frontend must complete the passkey creation ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend. Use the `transactionId` and the credential `response` to finish sign-up. ```javascript // Args: // transactionId: The transaction ID returned by the sign_up_start function const transactionId = "xxxxxx" // response: The response returned by successful biometric authorization in the browser const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' const resp = await descopeClient.auth.webauthn.signUp.finish(transactionId, response); if (!resp.ok) { console.log("Unable to finish webauthn sign-up") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished webauthn sign-up") console.log(resp) } ``` ```python # Args: # transactionID: The transaction ID returned by the sign_up_start function transactionID = "xxxxxx" # response: The response returned by successful biometric authorization in the browser response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.webauthn.sign_up_finish(transactionID=transactionID, response=response, audience=audience) print ("Successfully finished webauthn sign-up") print(resp) except AuthException as error: print ("Unable to finish webauthn sign-up") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // r: HttpRequest for the action - built by successful biometric authorization in the browser // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation res, err := descopeClient.Auth.WebAuthn().SignUpFinish(ctx, r, w) if (err != nil){ fmt.Println("Unable to finish webauthn sign-up: ", err) } else { fmt.Println("Successfully finished webauthn sign-up: ", res) } ``` ```java // Args: // transactionId: The transaction ID returned by the signUpStart function String transactionId = "xxxxxx"; // response: The response returned by successful biometric authorization in the browser String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"; WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder() .transactionId(transactionId) .response(response) .build(); WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { AuthenticationInfo authInfo = webAuthn.signUpFinish(finishRequest); String sessionJwt = authInfo.getToken().getJwt(); String refreshJwt = authInfo.getRefreshToken().getJwt(); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // TransactionId (string): The transaction ID returned by Start Sign-Up // Response (string): The JSON credential response from the browser's WebAuthn API var response = await client.Auth.V1.Webauthn.Signup.Finish.PostAsync( new WebauthnSignUpFinishRequest { TransactionId = "xxxxxx", Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"attestationObject\":\"\",\"clientDataJSON\":\"\"}}" }); ``` ## Start Sign-In Start the passkey sign-in flow by calling the sign-in start function. This function requires the user's `loginId`, such as their email address or phone number. It also requires an `origin` value, which should be the value of `window.location.origin` from your application client. Descope validates the origin against the domain configured in the Descope Console. The origin must match the configured domain or be a valid subdomain. ```javascript // Args: // loginId: email or phone - the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. const origin = "https://example.com" const resp = await descopeClient.auth.webauthn.signIn.start(loginId, origin); if (!resp.ok) { console.log("Unable to start webauthn sign-in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started webauthn sign-in") console.log(resp) } ``` ```python # Args: # login_id: email or phone - the loginId for the user login_id = "email@company.com" # origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin = "http://example.com" # loginOptions: (optional) - see login options - /api/overview#user-login-options loginOptions = None # refreshToken: (optional) - refresh token of user logging in refreshToken = None try: resp = descope_client.webauthn.sign_in_start(login_id=login_id, origin=origin, loginOptions=loginOptions, refreshToken=refreshToken) print ("Successfully started webauthn sign-in") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to start webauthn sign-in") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - the loginId for the user loginID := "email@company.com" // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin := "https://example.com" res, err := descopeClient.Auth.WebAuthn().SignInStart(ctx, loginID, origin) if (err != nil){ fmt.Println("Unable to start webauthn sign-in: ", err) } else { fmt.Println("Successfully started webauthn sign-in: ", res) } ``` ```java // Args: // loginId: email or phone - the loginId for the user String loginId = "email@company.com"; // origin: This is the origin of the sign-in request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. String origin = "https://example.com"; // token: (optional) the user's current session token in the event of step-up/mfa String token = null; // loginOptions: (optional) configure behavior during the authentication process LoginOptions loginOptions = null; WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { WebAuthnTransactionResponse resp = webAuthn.signInStart(loginId, origin, token, loginOptions); // Return resp.getTransactionId() and resp.getOptions() to your client } catch (DescopeException de) { // Handle the error } // For MFA or step-up authentication: LoginOptions mfaOptions = LoginOptions.builder() .mfa(true) .build(); WebAuthnTransactionResponse mfaResp = webAuthn.signInStart(loginId, origin, sessionJwt, mfaOptions); ``` ```csharp // Args: // LoginId (string): The login ID of the user // Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting // LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims // PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON) var response = await client.Auth.V1.Webauthn.Signin.Start.PostAsync( new WebauthnSignInStartRequest { LoginId = "email@company.com", Origin = "https://example.com", LoginOptions = new LoginOptions { /* ... */ }, PasskeyOptions = new PasskeyOptions { /* ... */ } }); // response.TransactionId — pass to Finish Sign-In along with the browser's credential response ``` ## Finish Sign-In After starting the sign-in flow, Descope returns a `transactionId`. Your frontend must complete the passkey authentication ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend. Use the `transactionId` and credential `response` to finish sign-in. ```javascript // Args: // transactionId: The transaction ID returned by the sign in start function const transactionId = "xxxxxx" // response: The response returned by successful biometric authorization in the browser const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' const resp = await descopeClient.auth.webauthn.signIn.finish(transactionId, response); if (!resp.ok) { console.log("Unable to finish webauthn sign-in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished webauthn sign-in") console.log(resp) } ``` ```python # Args: # transactionID: The transaction ID returned by the sign_in_start function transactionID = "xxxxxx" # response: The response returned by successful biometric authorization in the browser response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.webauthn.sign_up_finish(transactionID=transactionID, response=response, audience=audience) print ("Successfully finished webauthn sign-in") print(resp) except AuthException as error: print ("Unable to finish webauthn sign-in") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // r: HttpRequest for the action. - built by successful biometric authorization in the browser // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation res, err := descopeClient.Auth.WebAuthn().SignInFinish(ctx, r, w) if (err != nil){ fmt.Println("Unable to finish webauthn sign-in: ", err) } else { fmt.Println("Successfully finished webauthn sign-in: ", res) } ``` ```java // Args: // transactionId: The transaction ID returned by the signInStart function String transactionId = "xxxxxx"; // response: The response returned by successful biometric authorization in the browser String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"; WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder() .transactionId(transactionId) .response(response) .build(); WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { AuthenticationInfo authInfo = webAuthn.signInFinish(finishRequest); String sessionJwt = authInfo.getToken().getJwt(); String refreshJwt = authInfo.getRefreshToken().getJwt(); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // TransactionId (string): The transaction ID returned by Start Sign-In // Response (string): The JSON credential response from the browser's WebAuthn API var response = await client.Auth.V1.Webauthn.Signin.Finish.PostAsync( new WebauthnSignInFinishRequest { TransactionId = "xxxxxx", Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}" }); ``` ## Sign-Up or Sign-In Use `SignUpOrInStart` when you want a single passkey flow and do not need to decide upfront whether the user is signing up or signing in. Descope checks whether a user with the given `loginID` already has a passkey registered and returns the appropriate WebAuthn ceremony options. The response includes a `create` field that tells your application how to complete the flow: - `create: true` — the user is new. On the client, call `navigator.credentials.create` with the returned `options`, then call `SignUpFinish`. - `create: false` — the user already exists. On the client, call `navigator.credentials.get` with the returned `options`, then call `SignInFinish`. There is no `SignUpOrInFinish` method. The finish step always uses `SignUpFinish` or `SignInFinish`, depending on the value of `create`. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // origin: This is the origin of the request and the value should be window.location.origin from the client. const origin = "https://example.com" // loginOptions (LoginOptions): (optional) configure behavior during the authentication process, such as tenantId for tenant user isolation. const loginOptions = { "tenantId": "tenant1", } const resp = await descopeClient.auth.webauthn.signUpOrIn.start(loginId, origin, undefined, loginOptions); if (!resp.ok) { console.log("Unable to start webauthn sign-up or sign-in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started webauthn sign-up or sign-in") console.log(resp) } // Return resp to your client. The client should: // - parse resp.data.options as the publicKey option for navigator.credentials.create or .get, based on resp.data.create // - POST the resulting credential back to your server // ---- Complete the flow (signUp.finish or signIn.finish, depending on resp.data.create) ---- // transactionId comes from signUpOrIn.start; response is the credential response from the browser. const transactionId = resp.data.transactionId const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' let authInfo if (resp.data.create) { authInfo = await descopeClient.auth.webauthn.signUp.finish(transactionId, response) } else { authInfo = await descopeClient.auth.webauthn.signIn.finish(transactionId, response) } if (!authInfo.ok) { console.log("Unable to finish webauthn sign-up or sign-in") console.log("Status Code: " + authInfo.code) console.log("Error Code: " + authInfo.error.errorCode) console.log("Error Description: " + authInfo.error.errorDescription) console.log("Error Message: " + authInfo.error.errorMessage) } else { console.log("Successfully finished webauthn sign-up or sign-in") console.log(authInfo) } ``` ```python # Args: # login_id: email or phone - becomes the loginId for the user from here on and also used for delivery login_id = "email@company.com" # origin: This is the origin of the request and the value should be window.location.origin from the client. origin = "https://example.com" try: resp = descope_client.webauthn.sign_up_or_in_start(login_id=login_id, origin=origin) print ("Successfully started webauthn sign-up or sign-in") print(json.dumps(resp, indent=4)) except AuthException as error: print ("Unable to start webauthn sign-up or sign-in") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) # Return resp to your client. The client should: # - parse resp["options"] as the publicKey option for navigator.credentials.create or .get, based on resp["create"] # - POST the resulting credential back to your server # ---- Complete the flow (sign_up_finish or sign_in_finish, depending on resp["create"]) ---- # transaction_id comes from sign_up_or_in_start; response is the credential response from the browser. transaction_id = resp["transactionId"] response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' try: if resp["create"]: auth_info = descope_client.webauthn.sign_up_finish(transaction_id=transaction_id, response=response) else: auth_info = descope_client.webauthn.sign_in_finish(transaction_id=transaction_id, response=response) print ("Successfully finished webauthn sign-up or sign-in") print(auth_info) except AuthException as error: print ("Unable to finish webauthn sign-up or sign-in") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - becomes the loginId for the user from here on and also used for delivery loginID := "email@company.com" // origin: This is the origin of the request and the value should be window.location.origin from the client. origin := "https://example.com" // loginOptions: (optional) configure behavior during the authentication process, such as TenantID for tenant user isolation. loginOptions := &descope.LoginOptions{ TenantID: "tenant1", } res, err := descopeClient.Auth.WebAuthn().SignUpOrInStart(ctx, loginID, origin, loginOptions) if err != nil { fmt.Println("Unable to start webauthn sign-up or sign-in: ", err) return } fmt.Println("Successfully started webauthn sign-up or sign-in: ", res) // Return res to your client. The client should: // - parse res.Options as the publicKey option for navigator.credentials.create or .get, based on res.Create // - POST the resulting credential back to your server as a WebAuthnFinishRequest // ---- Complete the flow (SignUpFinish or SignInFinish, depending on res.Create) ---- // finishRequest contains the transactionId from SignUpOrInStart and the credential response from the browser. finishRequest := &descope.WebAuthnFinishRequest{ TransactionID: res.TransactionID, Response: credentialResponseFromBrowser, } var authInfo *descope.AuthenticationInfo if res.Create { authInfo, err = descopeClient.Auth.WebAuthn().SignUpFinish(ctx, finishRequest, w) } else { authInfo, err = descopeClient.Auth.WebAuthn().SignInFinish(ctx, finishRequest, w) } if err != nil { fmt.Println("Unable to finish webauthn sign-up or sign-in: ", err) return } fmt.Println("Successfully finished webauthn sign-up or sign-in: ", authInfo) ``` ```java // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery String loginId = "email@company.com"; // origin: This is the origin of the request and the value should be window.location.origin from the client. String origin = "https://example.com"; WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { WebAuthnTransactionResponse resp = webAuthn.signUpOrInStart(loginId, origin); // Return resp.getTransactionId(), resp.getOptions(), and resp.isCreate() to your client } catch (DescopeException de) { // Handle the error } // Return resp to your client. The client should: // - parse resp.getOptions() as the publicKey option for navigator.credentials.create or .get, based on resp.isCreate() // - POST the resulting credential back to your server // ---- Complete the flow (signUpFinish or signInFinish, depending on resp.isCreate()) ---- // transactionId comes from signUpOrInStart; response is the credential response from the browser. String transactionId = resp.getTransactionId(); String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"; WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder() .transactionId(transactionId) .response(response) .build(); try { AuthenticationInfo authInfo; if (resp.isCreate()) { authInfo = webAuthn.signUpFinish(finishRequest); } else { authInfo = webAuthn.signInFinish(finishRequest); } String sessionJwt = authInfo.getToken().getJwt(); String refreshJwt = authInfo.getRefreshToken().getJwt(); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID for the user — becomes their permanent login ID // Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting // LoginOptions (LoginOptions) optional: Configure step-up, MFA, or custom claims // PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options var start = await client.Auth.V1.Webauthn.SignupIn.Start.PostAsync( new WebauthnSignUpOrInStartRequest { LoginId = "email@company.com", Origin = "https://example.com", }); // start.Create == true → use Signup.Finish; false → use Signin.Finish // start.TransactionId and start.Options — return to the client for the WebAuthn ceremony var transactionId = start?.TransactionId; var credentialResponse = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"; JWTResponse? authInfo; if (start?.Create == true) { authInfo = await client.Auth.V1.Webauthn.Signup.Finish.PostAsync( new WebauthnSignUpFinishRequest { TransactionId = transactionId, Response = credentialResponse, }); } else { authInfo = await client.Auth.V1.Webauthn.Signin.Finish.PostAsync( new WebauthnSignInFinishRequest { TransactionId = transactionId, Response = credentialResponse, }); } ``` ## Start Add User Device Use `Start Add User Device` to add a new passkey or authenticator to an existing user account. This flow is useful when a user has already authenticated with another method and wants to register a passkey for future sign-ins. The function requires a valid [refresh token](/sessions/validation) for the authenticated user. ```javascript // Args: // loginId: email or phone - the loginId for the user const loginId = "email@company.com" // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. const origin = "https://example.com" // refreshToken: Valid refresh_token for this user from another authentication method. This is required and should be extracted from query. const refreshToken = "xxxxx" const resp = await descopeClient.auth.webauthn.update.start(loginId, origin, refreshToken); if (!resp.ok) { console.log("Unable to start webauthn update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully started webauthn update") console.log(resp) } ``` ```python # Args: # login_id: email or phone - the loginId for the user login_id = "email@company.com" # refresh_token: a refresh token for the user you are wanting to add a device for refresh_token = "xxxxxx" # origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin = "http://example.com" try: resp = descope_client.webauthn.update_start(login_id=login_id, refresh_token=refresh_token, origin=origin) print ("Successfully started webauthn update") print(resp) except AuthException as error: print ("Unable to start webauthn update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: email or phone - the loginId for the user loginID := "email@company.com" // origin: This is the origin of the signup request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. origin := "https://example.com" // r: HttpRequest for the action. This request should contain refresh token for the authenticated user. res, error := descopeClient.Auth.WebAuthn().UpdateUserDeviceStart(ctx, loginID, origin, r) if (err != nil){ fmt.Println("Unable to start webauthn update: ", err) } else { fmt.Println("Successfully stared webauthn update: ", res) } ``` ```java // Args: // loginId: email or phone - the loginId for the user String loginId = "email@company.com"; // origin: This is the origin of the request and the value should be window.location.origin from the client. This value is essential to protect against replay attacks where the start request and finish request can be validated to be from same domain. String origin = "https://example.com"; // refreshToken: Valid refresh token for this user from another authentication method. This is required. String refreshToken = "xxxxx"; WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { WebAuthnTransactionResponse resp = webAuthn.updateUserDeviceStart(loginId, origin, refreshToken); // Return resp.getTransactionId() and resp.getOptions() to your client } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // LoginId (string): The login ID of the authenticated user // Origin (string): The origin of the request (window.location.origin from the client); validated against your Descope console domain setting // PasskeyOptions (PasskeyOptions) optional: Advanced WebAuthn options (Attestation, AuthenticatorSelection, UserVerification, ExtensionsJSON) // refreshJwt (string): The session's refresh token (required — user must be authenticated) var refreshJwt = "xxxxx"; var response = await client.Auth.V1.Webauthn.Update.Start.PostWithJwtAsync( new WebauthnAddDeviceStartRequest { LoginId = "email@company.com", Origin = "https://example.com", PasskeyOptions = new PasskeyOptions { /* ... */ } }, refreshJwt); // response.TransactionId — pass to Finish Add User Device along with the browser's credential response ``` ## Finish Add User Device After starting the add-device flow, Descope returns a `transactionId`. Your frontend must complete the passkey registration ceremony in the browser or native app, then send the resulting WebAuthn credential response back to your backend. Use the `transactionId` and credential `response` to finish adding the passkey to the user's account. ```javascript // Args: // transactionId: The transaction ID returned by the sign in start function const transactionId = "xxxxxx" // response: The response returned by successful biometric authorization in the browser const response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' const resp = await descopeClient.auth.webauthn.update.finish(transactionId, response); if (!resp.ok) { console.log("Unable to finish webauthn update") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully finished webauthn update") console.log(resp) } ``` ```python # Args: # transactionID: The transaction ID returned by the sign_in_start function transactionID = "xxxxxx" # response: The response returned by successful biometric authorization in the browser response = '{"id":"","rawId":"","type":"public-key","response":{"authenticatorData":"","clientDataJSON":"","signature":"","userHandle":""}}' try: resp = descope_client.webauthn.update_finish(transactionID=transactionID, response=response) print ("Successfully finished webauthn update") print(resp) except AuthException as error: print ("Unable to finish webauthn update") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // r: HttpRequest for the action. - built by successful biometric authorization in the browser _, err := descopeClient.Auth.WebAuthn().UpdateUserDeviceFinish(ctx, r) if (err != nil){ fmt.Println("Unable to finish webauthn update: ", err) } else { fmt.Println("Successfully finished webauthn update: ", res) } ``` ```java // Args: // transactionId: The transaction ID returned by the updateUserDeviceStart function String transactionId = "xxxxxx"; // response: The response returned by successful biometric authorization in the browser String response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"authenticatorData\":\"\",\"clientDataJSON\":\"\",\"signature\":\"\",\"userHandle\":\"\"}}"; WebAuthnFinishRequest finishRequest = WebAuthnFinishRequest.builder() .transactionId(transactionId) .response(response) .build(); WebAuthnService webAuthn = descopeClient.getAuthenticationServices().getWebAuthnService(); try { webAuthn.updateUserDeviceFinish(finishRequest); } catch (DescopeException de) { // Handle the error } ``` ```csharp // Args: // TransactionId (string): The transaction ID returned by Start Add User Device // Response (string): The JSON credential response from the browser's WebAuthn API await client.Auth.V1.Webauthn.Update.Finish.PostAsync( new WebauthnAddDeviceFinishRequest { TransactionId = "xxxxxx", Response = "{\"id\":\"\",\"rawId\":\"\",\"type\":\"public-key\",\"response\":{\"attestationObject\":\"\",\"clientDataJSON\":\"\"}}" }); ``` ## Session Validation After completing passkey sign-up or sign-in, validate the user session on your backend. Descope provides [session management](/sessions/validation) capabilities, including session validation, configurable session timeouts, and logout support. For backend session validation examples, see [Session Validation with Backend SDKs](/sessions/validation/backend). # Client SDKs (/auth-methods/passkeys/with-sdks/client) Add WebAuthn \ biometrics \ passkeys to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Passkey Authentication with Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. WebAuthn lets you authenticate end users using the strong authenticators that are now often built right into devices, including biometrics (fingerprint, facial, or iris recognition) and secure hardware keys (for example, Yubico, CryptoTrust, or Thedis). These secure hardware keys, also known as passkeys, can be USB tokens or embedded security features in smartphones or computers. A typical method for implementing WebAuthn has two sets of functionality to program: user onboarding and session validation. The `webauthn` methods are exposed by the web-js-sdk and are available through the Descope client SDKs: React, Next.js, Vue, Angular, WebJS, and plain HTML via the WebJS UMD bundle. The main differences are how you obtain the SDK instance, and that the Angular SDK wraps promise-returning SDK methods, including webauthn methods, as RxJS `Observable`s, so Angular examples use `.subscribe()` instead of `await`. ### User Sign-Up The first step for implementing WebAuthn authentication is Sign-Up. Within the web-js-sdk, the Sign-Up function is one call to the Descope Service. This defers from the backend SDKs which require a start call and stop call for each of the tasks covered here, as the backend must push the information to the browser and receive further information back from the browser. The new end user will be registered after the full WebAuthn flow has been completed. The below sample code demonstrates how to implement WebAuthn Sign-Up within your client application. ```javascript // Args: // loginId: email or phone - becomes the externalID for the user from here on and also used for delivery const loginId = "xxxxx" // name: User's name. Ex: firstName lastName const name = "Joe Persons" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signUp(loginId, name) if (resp.ok != true) { console.log("Failed to complete WebAuthn sign-up") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed WebAuthn sign-up") console.log(resp) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: email or phone - becomes the externalID for the user from here on and also used for delivery const loginId = "xxxxx" // name: User's name. Ex: firstName lastName const name = "Joe Persons" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signUp(loginId, name) if (resp.ok != true) { console.log("Failed to complete WebAuthn sign-up") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed WebAuthn sign-up") console.log(resp) } ``` ```javascript // Inside your component's ``` ### User Sign-In For signing in with an existing user via WebAuthn, you will utilize the `signIn` function. Within the web-js-sdk, the Sign-In function is one call. This defers from the backend SDKs which require a start call and stop call for each of the tasks covered here, as the backend must push the information to the browser and receive further information back. Upon successful verification of the Sign-In, the user will be logged in and the response will include the JWT information. The below sample code demonstrates how to implement WebAuthn Sign-In within your client application. ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "xxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signIn(loginId) if (resp.ok != true) { console.log("Failed to sign-in via WebAuthn") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via WebAuthn") console.log(resp) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "xxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signIn(loginId) if (resp.ok != true) { console.log("Failed to sign-in via WebAuthn") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via WebAuthn") console.log(resp) } ``` ```javascript // Inside your component's ``` ### User Sign-Up or In Within the web-js-sdk, the Sign-Up Or In function is one call to the Descope Service. This defers from the backend SDKs which require a start call and stop call for each of the tasks covered here, as the backend must push the information to the browser and receive further information back from the browser. The new end user will be registered after the full WebAuthn flow has been completed. The below sample code demonstrates how to implement WebAuthn Sign-Up or in within your client application. ```javascript // Args: // loginId: email or phone - becomes the externalID for the user from here on and also used for delivery const loginId = "xxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signUpOrIn(loginId) if (resp.ok != true) { console.log("Failed to complete WebAuthn sign-up or in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed WebAuthn sign-up or in") console.log(resp) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: email or phone - becomes the externalID for the user from here on and also used for delivery const loginId = "xxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.signUpOrIn(loginId) if (resp.ok != true) { console.log("Failed to complete WebAuthn sign-up or in") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully completed WebAuthn sign-up or in") console.log(resp) } ``` ```javascript // Inside your component's ``` ### Add User Device The `update` function within the web-js-sdk adds a new biometric signature or a device to an existing user account. You should use this function in scenarios where a user has already authenticated (signup complete) with your service via another method. This function requires a valid refresh token, either passed explicitly or retrieved automatically when stored in cookies. When using cookie-based sessions, the SDK will automatically use the refresh token, so you don't need to pass the token parameter. Within the web-js-sdk, the update function is one call. This differs from the backend SDKs which require a start call and stop call for each of the tasks covered here, as the backend must push the information to the browser and receive further information back. The below sample code demonstrates how to implement WebAuthn update within your client application. ```javascript // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "xxxxx" // token: a refresh token for the user you are wanting to add a device for const token = "xxxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.update(loginId, token) if (resp.ok != true) { console.log("Failed to add device via WebAuthn") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added device via WebAuthn") console.log(resp) } ``` ```javascript 'use client'; import { useDescope } from '@descope/nextjs-sdk/client'; // Args: // loginId: email or phone - must be same as provided at the time of signup. const loginId = "xxxxx" // token: a refresh token for the user you are wanting to add a device for const token = "xxxxxx" const descopeSdk = useDescope(); let resp = await descopeSdk.webauthn.update(loginId, token) if (resp.ok != true) { console.log("Failed to add device via WebAuthn") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully added device via WebAuthn") console.log(resp) } ``` ```javascript // Inside your component's ``` The `token` parameter is optional. If you are using cookie-based sessions, the SDK will automatically pick up the refresh token and you can omit this parameter. If you manage tokens manually (e.g., in memory), pass the refresh token explicitly as shown in the examples above. # Mobile SDKs (/auth-methods/passkeys/with-sdks/mobile) Add WebAuthn \ biometrics \ passkeys to your application using Descope mobile SDKs. Read the detailed implementation guide with sample code. # Passkeys with Mobile SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. Descope supports passkeys which allow for users to authenticate via the FIDO Alliance's WebAuthn standard. This standard allows for users to authenticate using a variety of methods including biometrics, hardware tokens, and more. ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ### Import and initialize SDK Parameters: - `baseUrl`: Custom domain that must be configured to manage token response in cookies. This makes sure every request to our service is through your custom domain, preventing accidental domain blockages. ```swift import DescopeKit func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Descope.setup(projectId: "__ProjectID__") return true } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() Descope.setup(this, projectId = "__ProjectID__") } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ### Setup and Authentication Users can authenticate by creating a new passkey or using an existing one. To enable passkey authentication, first configure your Passkey/WebAuthn settings in the [Descope Console](https://app.descope.com/settings/authentication/webauthn). Make sure Passkey/WebAuthn authentication is enabled and that the top-level domain is configured correctly. For Android, follow Google's official [Add support for Digital Asset Links](https://developer.android.com/training/sign-in/passkeys#add-support-dal) setup guide. Complete the required asset links and manifest configuration so your app can be associated with your domain. For iOS and macOS, refer to Apple's [Supporting Passkeys](https://developer.apple.com/documentation/authenticationservices/supporting-passkeys) guide. Make sure your app has an associated domain configured with the `webcredentials` service type, and that the value matches the top-level domain configured in the Descope Console. #### Passkeys in Your Own WebView on Android If your Android app loads a Descope flow in a `WebView` that you manage yourself, such as one using `androidx.webkit` with `setWebAuthenticationSupport`, the app finishes the passkey operation under its own APK key hash origin rather than the origin of the page it loaded. If passkey registration or sign-in fails in that setup, register the app so Descope accepts its origin: add the SHA-256 fingerprint of the keystore used to sign your app under **Android Fingerprints** in [Passkeys Settings](/auth-methods/passkeys/settings). An empty **Android Fingerprints** list places no restriction on Android apps. Once you add one fingerprint, the list covers every Android app in the project, so add a fingerprint for each of your apps and for each signing key they use. #### Check Passkey Support Before presenting passkey options to users, check whether passkeys are supported on the current device: Passkeys require iOS 15 and above. You can check availability at runtime using: ```swift if #available(iOS 15, *) { // Passkeys are supported; show passkey UI } else { // Fall back to another authentication method } ``` The Flutter SDK provides an explicit `isSupported()` method on `Descope.passkey` that checks whether passkeys are supported on the current device: - **iOS**: returns `true` on iOS 15 and above - **Android**: returns `true` on API 28 (Android 9) and above - **Web**: checks for WebAuthn browser support ```dart Flutter try { final supported = await Descope.passkey.isSupported(); if (supported) { // Passkeys are supported; show passkey UI } else { // Fall back to another authentication method } } on DescopeException catch (e) { // Handle errors checking passkey support } ``` #### Authenticate with a Passkey The passkey operations are all suspending functions that perform network requests before and after displaying the modal authentication view. It is thus recommended to switch the user interface to a loading state before calling them, otherwise the user might accidentally interact with the app when the authentication view is not being displayed. ```swift Swift do { showLoading(true) let authResponse = try await Descope.passkey.signUpOrIn(loginId: "andy@example.com", options: []) let session = DescopeSession(from: authResponse) Descope.sessionManager.manageSession(session) showHomeScreen() } catch DescopeError.oauthNativeCancelled { showLoading(false) print("Authentication canceled") } catch { showError(error) } ``` ```kotlin Kotlin // Enter loading state... try { val authResponse = Descope.passkey.signUpOrIn(this@MyActivity, loginId) val session = DescopeSession(authResponse) Descope.sessionManager.manageSession(session) } catch (e: DescopeException) { // Handle errors here } // Exit loading state... ``` ```dart Flutter try { showLoading(true); final authResponse = await Descope.passkey.signUpOrIn(loginId: loginId); final session = DescopeSession.fromAuthenticationResponse(authResponse); Descope.sessionManager.manageSession(session); showHomeScreen() } on DescopeException catch (e) { if (e == DescopeException.passkeyCancelled) { showLoading(false) print("Authentication canceled") } else { showError(error) } } ``` # Backend SDKs (/auth-methods/passwords/with-sdks/backend) Add password authentication to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Password Authentication with Backend SDKs This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. If you'd like to use our Client SDKs, refer to our [Client SDK docs](/auth-methods/passwords/with-sdks/client). The Password-based authentication method lets you authenticate end users using a secret string of characters known only to the user. Descope recommends using an email address as the user identifier; this allows you to utilize passwordless methods like Magic Link in addition to passwords. These methods could be used for authentication when users forget their password or need to reset it easily. ## Use Cases 1. **New user signup**: [User Sign-Up](/auth-methods/passwords/with-sdks/backend#user-sign-up) returns a jwt for the user. 2. **Existing user signin**: [User Sign-In](/auth-methods/passwords/with-sdks/backend#user-sign-in) returns a jwt for the user. ## User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. The application client should then send this information to your application server. Signing up via password returns the user's JWT. ```javascript // Args: // loginId (str): The login ID of the user being signed up const loginId = "email@company.com" // password (str): The new user's password const password = "xxxxxx" // user (dict) optional: Preserve additional user metadata in the form of const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const resp = await descopeClient.password.signUp(loginId, password, user); if (!resp.ok) { console.log("Failed to sign up via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed up via password") console.log(resp); } ``` ```python # Args: # login_id (str): The login ID of the user being signed up login_id = "email@company.com" # password (str): The new user's password password = "xxxxxx" # user (dict) optional: Preserve additional user metadata in the form of user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.password.sign_up(login_id=login_id, password=password, user=user, audience=audience) print ("Successfully signed up via password") print (resp) except AuthException as error: print ("Failed to sign up via password") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID (str): The login ID of the user being signed up loginID := "email@company.com" // user (descope.User): Optional user object to populate new user information. user := &descope.User{ Name: "Joe Person", Phone: "+15555555555", Email: loginID, } // password (str): The new user's password password := "xxxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation. This code should go into your application server route handling the verification of code. resp, err := descopeClient.Auth.Password().SignUp(ctx, loginID, user, password, w) if (err != nil){ fmt.Println("Failed to sign up via password: ", err) } else { fmt.Println("Successfully signed up via password", resp) } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); String password = "qYlvi65KaX"; LoginOptions lo = { customClaims: { claim1: 'yes' } } PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); try { AuthenticationInfo info = ps.signUp(loginId, user, password, lo); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Every user must have a login_id and a password. All other user information is optional login_id = 'desmond@descope.com' password = 'qYlvi65KaX' user = { name: 'Desmond Copeland', email: login_id, } jwt_response = descope_client.password_sign_up(login_id:, password:, user:) session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```php $response = $descopeSDK->auth->password->signUp("loginId", "password123"); print_r($response); ``` ```csharp // Args: // LoginId (string): The login ID of the user being signed up // Password (string): The new user's password // User (SignUpUser) optional: Additional user metadata // LoginOptions (SignupLoginOptions) optional: Set custom claims, locale, or template options var response = await client.Auth.V1.Auth.Password.Signup.PostAsync( new PasswordSignUpRequest { LoginId = "email@company.com", Password = "xxxxxx", User = new SignUpUser { Name = "Joe Person", Phone = "+15555555555", Email = "email@company.com" }, LoginOptions = new SignupLoginOptions { /* ... */ } }); ``` ## User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number) and password. The application client should send this information to your application server. Signing in via password returns the user's JWT. ```javascript // Args: // loginId (str): The login ID of the user being signed in const loginId = "email@company.com" // password (str): The user's password const password = "xxxxxx" const resp = await descopeClient.password.signIn(loginId, password); if (!resp.ok) { console.log("Failed to sign in via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via password") console.log(resp); } ``` ```python # Args: # login_id (str): The login ID of the user being signed in login_id = "email@company.com" # password (str): The user's password password = "xxxxxx" # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.password.sign_in(login_id=login_id, password=password, audience=audience) print ("Successfully signed in via password") print (resp) except AuthException as error: print ("Failed to sign in via password") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: The login ID of the user being signed in loginID := "email@company.com" // password (str): The user's password password := "xxxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation. This code should go into your application server route handling the verification of code. resp, err := descopeClient.Auth.Password().SignIn(ctx, loginID, password, w) if (err != nil){ fmt.Println("Failed to sign in via password: ", err) } else { fmt.Println("Successfully signed in via password", resp) } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); String password = "qYlvi65KaX"; PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); LoginOptions lo = { customClaims: { claim1: 'yes' } } try { AuthenticationInfo info = ps.signIn(loginId, password, lo); } catch (DescopeException de) { // Handle the error } ``` ```ruby jwt_response = descope_client.password_sign_in(login_id:, password:) session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```php $response = $descopeSDK->auth->password->signIn("loginId", "password123"); print_r($response); ``` ```csharp // Args: // LoginId (string): The login ID of the user being signed in // Password (string): The user's password // LoginOptions (LoginOptions) optional: Configure step-up, MFA, custom claims, or template options // SsoAppId (string) optional: Associate sign-in with a specific Federated App var response = await client.Auth.V1.Auth.Password.Signin.PostAsync( new PasswordSignInRequest { LoginId = "email@company.com", Password = "xxxxxx", LoginOptions = new LoginOptions { /* ... */ }, SsoAppId = "my-sso-app-id" }); ``` ## Update Password Update a password for an existing logged in user using their refresh token. ```javascript // Args: // loginId (str): The login ID of the user who's information is being updated const loginId = "email@company.com" // newPassword (str): The new password to use const newPassword = "xxxxxx" // token (str): The session's refresh token (used for verification) const token = "xxxxxx" const resp = await descopeClient.password.update(loginId, newPassword, token); if (!resp.ok) { console.log("Failed to update password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated password") } ``` ```python # Args: # login_id (str): The login ID of the user who's information is being updated login_id = "email@company.com" # new_password (str): The new password to use new_password = "xxxxxx" # refresh_token (str): The session's refresh token (used for verification) refresh_token = "xxxxxx" try: descope_client.password.update(login_id=login_id, new_password=new_password, refresh_token=refresh_token) print ("Successfully updated password") except AuthException as error: print ("Failed to update password") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: The login ID of the user who's information is being updated loginID := "email@company.com" // newPassword (str): The new password to use newPassword := "xxxxxx" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. err := descopeClient.Auth.Password().UpdateUserPassword(ctx, loginID, newPassword, r) if (err != nil){ fmt.Println("Failed to update password: ", err) } else { fmt.Println("Successfully updated password") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; String refreshToken = "xxxxx"; String newPassword = "qYlvi65KaXwegsd"; PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); try { AuthenticationInfo info = ps.updateUserPassword(loginId, newPassword, r); } catch (DescopeException de) { // Handle the error } ``` ```ruby # The refresh token is required to make sure the user is authenticated. err = descope_client.password_update(login_id:, new_password: 'xyz123', token: 'token-here') ``` ```php $response = $descopeSDK->auth->password->update("loginId", "newPassword123", "refreshToken"); print_r($response); ``` ```csharp // Args: // LoginId (string): The login ID of the user whose password is being updated // NewPassword (string): The new password to use // refreshJwt (string): The session's refresh token (required for verification) await client.Auth.V1.Auth.Password.Update.PostWithJwtAsync( new PasswordUpdateRequest { LoginId = "email@company.com", NewPassword = "xxxxxx" }, refreshJwt); ``` ## Replace Password Replace a password with a new one. The old password is used to authenticate the user before replacing the password. If the user cannot be authenticated, this operation will fail. ```javascript // Args: // loginId (str): The login ID of the user who's information is being replaced const loginId = "email@company.com" // oldPassword (str): The user's current active password const oldPassword = "xxxxxx" // newPassword (str): The new password to use const newPassword = "xxxxxx" const resp = await descopeClient.password.replace(loginId, oldPassword, newPassword); if (!resp.ok) { console.log("Failed to replace password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully replaced password") } ``` ```python # Args: # login_id (str): The login ID of the user who's information is being replaced login_id = "email@company.com" # old_password (str): The user's current active password old_password = "xxxxxx" # new_password (str): The new password to use new_password = "xxxxxx" # audience (str | Iterable[str] | None): Optional audience to validate against the session token's aud claim audience = "xxxx" try: resp = descope_client.password.replace(login_id=login_id, old_password=old_password, new_password=new_password, audience=audience) print ("Successfully replaced password") except AuthException as error: print ("Failed to replace password") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: The login ID of the user who's information is being replaced loginID := "email@company.com" // oldPassword (str): The user's current active password oldPassword := "xxxxxx" // newPassword (str): The new password to use newPassword := "xxxxxx" err := descopeClient.Auth.Password().ReplaceUserPassword(ctx, loginID, oldPassword, newPassword) if (err != nil){ fmt.Println("Failed to replace password: ", err) } else { fmt.Println("Successfully replaced password") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; String password = "qYlvi65KaX"; String newPassword = "sdgoh90" PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); try { AuthenticationInfo info = ps.replaceUserPassword(loginId, password, newPassword); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Replaces the user's current password with a new one jwt_response = descope_client.password_replace(login_id: 'login', old_password: '1234', new_password: '4567') session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```php $response = $descopeSDK->auth->password->replace("loginId", "oldPassword123", "newPassword123"); print_r($response); ``` ```csharp // Args: // LoginId (string): The login ID of the user whose password is being replaced // OldPassword (string): The user's current active password // NewPassword (string): The new password to use // RevokeOtherSessions (bool) optional: Revoke all other active sessions on password change // RevokeOtherSessionsTypes (List) optional: Session types to revoke (e.g. "session", "refresh") var response = await client.Auth.V1.Auth.Password.Replace.PostAsync( new PasswordReplaceRequest { LoginId = "email@company.com", OldPassword = "xxxxxx", NewPassword = "yyyyyy", RevokeOtherSessions = true, RevokeOtherSessionsTypes = new List { "session", "refresh" } }); ``` ## Reset Password Sends a password reset prompt to the user with the given login id according to the password settings defined in the Descope console. The user's email must be verified in order for the password reset method to complete. ```javascript // Args: // loginId (str): The login ID of the user who's password is being reset const loginId = "email@company.com" // redirectURL (str): Optional parameter that is used by Magic Link. const redirectURL = "http://auth.company.com/api/verify_magiclink" // templateOptions (TemplateOptions): Password reset email template options const templateOptions = {"option": "Value1"} const resp = await descopeClient.password.sendReset(loginId, redirectURL, templateOptions); if (!resp.ok) { console.log("Failed to send password reset") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully sent password reset") } ``` ```python # Args: # login_id (str): The login ID of the user who's password is being reset login_id = "email@company.com" # redirect_url (str): Optional parameter that is used by Magic Link. redirect_url = "http://auth.company.com/api/verify_magiclink" # template_options (dict): email template options template_options = {"option": "Value1"} try: resp = descope_client.password.send_reset(login_id=login_id, redirect_url=redirect_url, template_options=template_options) print ("Successfully sent password reset") print (resp) except AuthException as error: print ("Failed to send password reset") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // loginID: The login ID of the user who's password is being reset loginID := "email@company.com" // redirectURL (str): Optional parameter that is used by Magic Link. redirectURL := "http://auth.company.com/api/verify_magiclink" // templateOptions (TemplateOptions): Password reset email template options templateOptions := map[string]any{"option": "Value1"} err := descopeClient.Auth.Password().SendPasswordReset(ctx, loginID, redirectURL) if (err != nil){ fmt.Println("Failed to send password reset: ", err) } else { fmt.Println("Successfully sent password reset") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; String redirectUrl = ""; PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); try { AuthenticationInfo info = ps.sendPasswordReset(loginId, redirectUrl); } catch (DescopeException de) { // Handle the error } ``` ```ruby # Start the reset process by sending a password reset prompt. In this example we'll assume # that magic link is configured as the reset method. The optional redirect URL is used in the # same way as in regular magic link authentication. login_id = 'desmond@descope.com' redirect_url = 'https://myapp.com/password-reset' descope_client.password_reset(login_id:, redirect_url:) ``` ```php $redirectURL = "https://example.com/reset"; $response = $descopeSDK->auth->password->sendReset("loginId", $redirectURL); print_r($response); ``` ```csharp // Args: // LoginId (string): The login ID of the user whose password is being reset // RedirectUrl (string) optional: Used by Magic Link as the post-verification redirect URL // ProviderId (string) optional: The provider ID to use for the reset flow // TemplateId (string) optional: Override the default email template // Locale (string) optional: Locale for the reset email (e.g. "en-US") // TemplateOptions (PasswordResetSendRequest_templateOptions) optional: Dynamic template substitution values await client.Auth.V1.Auth.Password.Reset.PostAsync( new PasswordResetSendRequest { LoginId = "email@company.com", RedirectUrl = "http://auth.company.com/api/verify_magiclink", ProviderId = "my-provider-id", TemplateId = "my-template-id", Locale = "en-US", TemplateOptions = new PasswordResetSendRequest_templateOptions { /* ... */ } }); ``` ## Get Password Policy Get the configured password policy for the project. ```javascript // Args: // None const resp = await descopeClient.password.policy(); if (!resp.ok) { console.log("Failed to get password policy") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully retrieved password policy") console.log(resp) } ``` ```python # Args: # None try: resp = descope_client.password.get_policy() print ("Successfully returned password policy") print (resp) except AuthException as error: print ("Failed to return password policy") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() resp, err := descopeClient.Auth.Password().GetPasswordPolicy(ctx) if (err != nil){ fmt.Println("Failed to return password policy: ", err) } else { fmt.Println("Successfully returned password policy", resp) } ``` ```java PasswordService ps = descopeClient.getAuthenticationServices().getPasswordService(); try { AuthenticationInfo info = ps.getPasswordPolicy(); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.get_password_policy(refresh_token) ``` ```php $response = $descopeSDK->auth->password->getPolicy(); print_r($response); ``` ```csharp // Args: // None var policy = await client.Auth.V1.Auth.Password.Policy.GetAsync(); ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for backend session validation [here](/sessions/validation/backend). # Client SDKs (/auth-methods/passwords/with-sdks/client) Add password authentication to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Password Authentication with Client SDKs This guide is meant for developers that are NOT using Descope Flows to design login screens and authentication methods. If you'd like to use Descope Flows, [Quick Start](/getting-started) should be your starting point. The Password-based authentication method lets you authenticate end users using a secret string of characters known only to the user. Descope recommends using an email address as the user identifier; this allows you to utilize passwordless methods like Magic Link in addition to passwords. These methods could be used for authentication when users forget their password or need to reset it easily. ## Use Cases 1. **New user signup**: The following actions must be completed, first [User Sign-Up](/auth-methods/passwords/with-sdks/client#user-sign-up) this returns a jwt for the user. 2. **Existing user signin**: The following actions must be completed, first [User Sign-In](/auth-methods/passwords/with-sdks/client#user-sign-in) this returns a jwt for the user. ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ### User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. The application client should then send this information to your application server. Signing up via password returns the user's JWT. ```javascript // Args: // loginId (str): The login ID of the user being signed up const loginId = "email@company.com" // password (str): The new user's password const password = "xxxxxx" // user (dict) optional: Preserve additional user metadata in the form of const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const descopeSdk = useDescope(); const resp = await descopeSdk.password.signUp(loginId, password, user); if (!resp.ok) { console.log("Failed to sign up via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed up via password") console.log(resp); } ``` ```javascript // Args: // loginId (str): The login ID of the user being signed up const loginId = "email@company.com" // password (str): The new user's password const password = "xxxxxx" // user (dict) optional: Preserve additional user metadata in the form of const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const resp = await descopeSdk.password.signUp(loginId, password, user); if (!resp.ok) { console.log("Failed to sign up via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed up via password") console.log(resp); } ``` ```html ``` ### User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number) and password. The application client should send this information to your application server. Signing in via password returns the user's JWT. ```javascript // Args: // loginId (str): The login ID of the user being signed in const loginId = "email@company.com" // password (str): The user's password const password = "xxxxxx" const descopeSdk = useDescope(); const loginOptions = { customClaims: { claim1: "yes" } } const resp = await descopeSdk.password.signIn(loginId, password, loginOptions); if (!resp.ok) { console.log("Failed to sign in via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via password") console.log(resp); } ``` ```javascript // Args: // loginId (str): The login ID of the user being signed in const loginId = "email@company.com" // password (str): The user's password const password = "xxxxxx" const loginOptions = { customClaims: { claim1: "yes" } } const resp = await descopeSdk.password.signIn(loginId, password, loginOptions); if (!resp.ok) { console.log("Failed to sign in via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via password") console.log(resp); } ``` ```html ``` ### Update Password Update a password for an existing logged in user using their refresh token. ```javascript // Args: // loginId (str): The login ID of the user who's information is being updated const loginId = "email@company.com" // newPassword (str): The new password to use const newPassword = "xxxxxx" // token (str): The session's refresh token (used for verification) const token = "xxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.password.update(loginId, newPassword, token); if (!resp.ok) { console.log("Failed to update password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated password") } ``` ```javascript // Args: // loginId (str): The login ID of the user who's information is being updated const loginId = "email@company.com" // newPassword (str): The new password to use const newPassword = "xxxxxx" // token (str): The session's refresh token (used for verification) const token = "xxxxxx" const resp = await descopeSdk.password.update(loginId, newPassword, token); if (!resp.ok) { console.log("Failed to update password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated password") } ``` ```html ``` ### Replace Password Replace a password with a new one. The old password is used to authenticate the user before replacing the password. If the user cannot be authenticated, this operation will fail. ```javascript // Args: // loginId (str): The login ID of the user who's information is being replaced const loginId = "email@company.com" // oldPassword (str): The user's current active password const oldPassword = "xxxxxx" // newPassword (str): The new password to use const newPassword = "xxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.password.replace(loginId, oldPassword, newPassword); if (!resp.ok) { console.log("Failed to replace password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully replaced password") } ``` ```javascript // Args: // loginId (str): The login ID of the user who's information is being replaced const loginId = "email@company.com" // oldPassword (str): The user's current active password const oldPassword = "xxxxxx" // newPassword (str): The new password to use const newPassword = "xxxxxx" const resp = await descopeSdk.password.replace(loginId, oldPassword, newPassword); if (!resp.ok) { console.log("Failed to replace password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully replaced password") } ``` ```html ``` ### Reset Password Sends a password reset prompt to the user with the given login id according to the password settings defined in the Descope console. NOTE: The user's email must be verified in order for the password reset method to complete. ```javascript // Args: // loginId (str): The login ID of the user who's password is being reset const loginId = "email@company.com" // redirectURL (str): Optional parameter that is used by Magic Link. const redirectURL = "http://auth.company.com/api/verify_magiclink" // templateOptions (TemplateOptions): Password reset email template options const templateOptions = {"option": "Value1"} const descopeSdk = useDescope(); const resp = await descopeSdk.password.sendReset(loginId, redirectURL, templateOptions); if (!resp.ok) { console.log("Failed to send password reset") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully sent password reset") } ``` ```javascript // Args: // loginId (str): The login ID of the user who's password is being reset const loginId = "email@company.com" // redirectURL (str): Optional parameter that is used by Magic Link. const redirectURL = "http://auth.company.com/api/verify_magiclink" // templateOptions (TemplateOptions): Password reset email template options const templateOptions = {"option": "Value1"} const resp = await descopeSdk.password.sendReset(loginId, redirectURL, templateOptions); if (!resp.ok) { console.log("Failed to send password reset") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully sent password reset") } ``` ```html ``` ### Get Password Policy Get the configured password policy for the project. ```javascript // Args: // None const descopeSdk = useDescope(); const resp = await descopeSdk.password.policy(); if (!resp.ok) { console.log("Failed to get password policy") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully retrieved password policy") console.log(resp) } ``` ```javascript // Args: // None const resp = await descopeSdk.password.policy(); if (!resp.ok) { console.log("Failed to get password policy") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully retrieved password policy") console.log(resp) } ``` ```html ``` # Mobile SDKs (/auth-methods/passwords/with-sdks/mobile) Add password authentication to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Password Authentication with Mobile SDKs The Passwords Authentication Method lets you authenticate end users using a secret string of characters known only to the user. Descope recommends using an email address as the user identifier; this allows you to utilize passwordless methods like Magic Link in addition to passwords. These methods could be used for authentication when users forget their password or need to reset it easily. ## Use Cases 1. **New user signup**: [User Sign-Up](/auth-methods/passwords/with-sdks/backend#user-sign-up) returns a jwt for the user. 2. **Existing user signin**: [User Sign-In](/auth-methods/passwords/with-sdks/backend#user-sign-in) returns a jwt for the user. ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and initialize SDK Parameters: - `baseUrl`: Custom domain that must be configured to manage token response in cookies. This makes sure every request to our service is through your custom domain, preventing accidental domain blockages. ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## User Sign-Up For registering a new user, your application client should accept user information, including an email or phone number used for verification. The application client should then send this information to your application server. Signing up via password returns the user's JWT. ```swift // Creates a new user that can later sign in with a password. // // - Parameters: // - loginId: What identifies the user when logging in, typically // an email, phone, or any other unique identifier. // - details: Optional details about the user signing up. // - password: The user's password. // // - Returns: An AuthenticationResponse value upon successful authentication.let loginId = "email@company.com" // user: Optional user object to populate new user information. let details = SignUpDetails(name: "Joe Person", phone: "+15555555555", email: "email@company.com") // password (str): The user's password let password = "xxxxxx" do { let resp = try await Descope.password.signUp(loginId: loginId, password: password, details: details) print("Successfully signed up via password") completionHandler(true, nil) } catch let descopeErr as DescopeError { print(descopeErr) completionHandler(false, descopeErr) } ``` ```kotlin // - Parameters: // - loginId: What identifies the user when logging in, typically // an email, phone, or any other unique identifier. // - details: Optional details about the user signing up. // - password: The user's password. val details = SignUpDetails( name = "firstName lastName", email = "email@company.com", phone = "+15555555555", givenName = "firstName", middleName = "middleName", familyName = "lastName" ) try { Descope.password.signUp(loginId = "email@company.com", password = "xxxxxx", details = details) } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Creates a new user that can later sign in with a password. // // - Parameters: // - loginId: What identifies the user when logging in, typically // an email, phone, or any other unique identifier. // - details: Optional details about the user signing up. // - password: The user's password. // const loginId = "loginId"; const details = SignUpDetails(name: "Name"); const password = "password"; Descope.password.signUp(loginId: loginId, password: password); ``` ``` javascript // Args: // loginId (str): The login ID of the user being signed up const loginId = "email@company.com" // password (str): The new user's password const password = "xxxxxx" // user (dict) optional: Preserve additional user metadata in the form of const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"} const descopeSdk = useDescope(); const resp = await descopeSdk.password.signUp(loginId, password, user); if (!resp.ok) { console.log("Failed to sign up via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed up via password") console.log(resp); } ``` ## User Sign-In For authenticating a user, your application client should accept the user's identity (typically an email address or phone number) and password. The application client should send this information to your application server. Signing in via password returns the user's JWT. ```swift // Authenticates an existing user using a password. // // - Parameters: // - loginId: What identifies the user when logging in, // typically an email, phone, or any other unique identifier. // - password: The user's password. // // - Returns: An ``AuthenticationResponse`` value upon successful authentication. let loginId = "email@company.com" // password (str): The user's password let password = "xxxxxx" do { let resp = try await Descope.password.signIn(loginId: loginId, password: password) completionHandler(true, nil) } catch let descopeErr as DescopeError { print(descopeErr) completionHandler(false, descopeErr) } ``` ```kotlin // - Parameters: // - loginId: What identifies the user when logging in, // typically an email, phone, or any other unique identifier. // - password: The user's password. try { Descope.password.signIn(loginId = "email@company.com", password = "xxxxxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Authenticates an existing user using a password. // // - Parameters: // - loginId: What identifies the user when logging in, // typically an email, phone, or any other unique identifier. // - password: The user's password. // // - Returns: An ``AuthenticationResponse`` value upon successful authentication. const loginId = "loginId"; const details = SignUpDetails(name: "Name"); const password = "password"; Descope.password.signIn(loginId: loginId, password: password); ``` ``` javascript // Args: // loginId (str): The login ID of the user being signed in const loginId = "email@company.com" // password (str): The user's password const password = "xxxxxx" const descopeSdk = useDescope(); const loginOptions = { customClaims: { claim1: "yes" } } const resp = await descopeSdk.password.signIn(loginId, password, loginOptions); if (!resp.ok) { console.log("Failed to sign in via password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully signed in via password") console.log(resp); } ``` ## Update Password Update a password for an existing logged in user using their refresh token. ```swift // In order to do this, the user must have an active ``DescopeSession`` whose // `refreshJwt` should be passed as a parameter to this function. // // The value for `newPassword` must conform to the password policy defined in the // password settings in the Descope console // // - Parameters: // - loginId: The existing user's loginId. // - newPassword: The new password to set for the user. // - refreshJwt: The existing user's `refreshJwt` from an active ``DescopeSession``. let loginId = "email@company.com" // password (str): The user's password let newPassword = "xxxxxx" let refreshJwt = "xxxxxxxxx" do { let resp = try await Descope.password.update(loginId: loginId, newPassword: newPassword, refreshJwt: refreshJwt) completionHandler(true, nil) } catch let descopeErr as DescopeError { print(descopeErr) completionHandler(false, descopeErr) } ``` ```kotlin // - Parameters: // - loginId: The existing user's loginId. // - newPassword: The new password to set for the user. // - refreshJwt: The existing user's `refreshJwt` from an active ``DescopeSession``. try { Descope.password.update(loginId = "email@company.com", newPassword = "xxx", refreshJwt = "xxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // In order to do this, the user must have an active ``DescopeSession`` whose // `refreshJwt` should be passed as a parameter to this function. // // The value for `newPassword` must conform to the password policy defined in the // password settings in the Descope console // // - Parameters: // - loginId: The existing user's loginId. // - newPassword: The new password to set for the user. // - refreshJwt: The existing user's `refreshJwt` from an active ``DescopeSession``. const loginId = "email@company.com"; // password (str): The user's password const newPassword = "xxxxxx"; final refreshJwt = "xxxxxxxxx" Descope.password.update( loginId: loginId, newPassword: newPassword, refreshJwt: refreshJwt); ``` ``` javascript // Args: // loginId (str): The login ID of the user who's information is being updated const loginId = "email@company.com" // newPassword (str): The new password to use const newPassword = "xxxxxx" // token (str): The session's refresh token (used for verification) const token = "xxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.password.update(loginId, newPassword, token); if (!resp.ok) { console.log("Failed to update password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully updated password") } ``` ## Replace Password Replace a password with a new one. The old password is used to authenticate the user before replacing the password. If the user cannot be authenticated, this operation will fail. ```swift /// Replaces a user's password by providing their current password. /// /// The value for `newPassword` must conform to the password policy defined in the /// password settings in the Descope console /// /// - Parameters: /// - loginId: The existing user's loginId. /// - oldPassword: The user's current password. /// - newPassword: The new password to set for the user.let loginId = "email@company.com" // password (str): The user's password let loginId = "" let newPassword = "xxxxxx" let oldPassword = "xxxxxxxxx" do { let resp = try await Descope.password.replace(loginId: loginId, oldPassword: oldPassword, newPassword: newPassword) completionHandler(true, nil) } catch let descopeErr as DescopeError { print(descopeErr) completionHandler(false, descopeErr) } ``` ```kotlin /// - Parameters: /// - loginId: The existing user's loginId. /// - oldPassword: The user's current password. /// - newPassword: The new password to set for the user.let loginId = "email@company.com" // password (str): The user's password try { Descope.password.replace(loginId = "email@company.com", oldPassword = "xxx", newPassword = "xxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart /// Replaces a user's password by providing their current password. /// /// The value for `newPassword` must conform to the password policy defined in the /// password settings in the Descope console /// /// - Parameters: /// - loginId: The existing user's loginId. /// - oldPassword: The user's current password. /// - newPassword: The new password to set for the user.let loginId = "email@company.com" // password (str): The user's password const loginId = "example@mail.com"; const oldPassword = "xxxxx"; const newPassword = "xxxxx"; Descope.password.replace(loginId: loginId, oldPassword: password, newPassword: newPassword); ``` ``` javascript // Args: // loginId (str): The login ID of the user who's information is being replaced const loginId = "email@company.com" // oldPassword (str): The user's current active password const oldPassword = "xxxxxx" // newPassword (str): The new password to use const newPassword = "xxxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.password.replace(loginId, oldPassword, newPassword); if (!resp.ok) { console.log("Failed to replace password") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully replaced password") } ``` ## Reset Password Sends a password reset prompt to the user with the given login id according to the password settings defined in the Descope console. NOTE: The user's email must be verified in order for the password reset method to complete. ```swift // Sends a password reset email to the user. // // This operation starts a Magic Link or Enchanted Link flow depending on the // configuration in the Descope console. After the authentication flow is finished // use the `refreshJwt` to call `update` and change the user's password. // // - Important: The user must be verified according to the configured // password reset method. // // - Parameters: // - loginId: The existing user's loginId. // - redirectURL: Optional URL that is used by Magic Link or Enchanted Link // if those are the chosen reset methods.let loginId = "email@company.com" let loginId = "xxxxxxxxx" do { let resp = try await Descope.password.sendReset(loginId: loginId, redirectURL: nil) completionHandler(true, nil) } catch let descopeErr as DescopeError { print(descopeErr) completionHandler(false, descopeErr) } ``` ```kotlin // - Parameters: // - loginId: The existing user's loginId. // - redirectURL: Optional URL that is used by Magic Link or Enchanted Link // if those are the chosen reset methods.let loginId = "email@company.com" try { Descope.password.sendReset(loginId = "email@company.com", redirectUrl = "xxx") } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } ``` ```dart // Sends a password reset email to the user. // // This operation starts a Magic Link or Enchanted Link flow depending on the // configuration in the Descope console. After the authentication flow is finished // use the `refreshJwt` to call `update` and change the user's password. // // - Important: The user must be verified according to the configured // password reset method. // // - Parameters: // - loginId: The existing user's loginId. // - redirectURL: Optional URL that is used by Magic Link or Enchanted Link // if those are the chosen reset methods.let loginId = "email@company.com" const loginId = "example@mail.com"; Descope.password.sendReset(loginId: loginId); ``` ``` javascript // Args: // loginId (str): The login ID of the user who's password is being reset const loginId = "email@company.com" // redirectURL (str): Optional parameter that is used by Magic Link. const redirectURL = "http://auth.company.com/api/verify_magiclink" // templateOptions (TemplateOptions): Password reset email template options const templateOptions = {"option": "Value1"} const descopeSdk = useDescope(); const resp = await descopeSdk.password.sendReset(loginId, redirectURL, templateOptions); if (!resp.ok) { console.log("Failed to send password reset") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully sent password reset") } ``` ## Session Validation The final step of completing the authentication with Descope is to validate the user session. Descope provides rich [session management](/sessions/validation) capabilities, including configurable session timeouts and logout functions. You can find the details and sample code for client session validation [here](/sessions/management/mobile). # SSO with OIDC (/auth-methods/sso/oidc) SSO (Single Sign-on) with OIDC Provider Configuration # SSO (Single Sign-on) with OIDC We highly recommend having your customer/tenant set up SSO for their own organization using our [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). The SSO Setup Suite walks your customers through the entire SSO Configuration process, with templates for all common IdPs, and allows them to test the connection, minimizing any back and forth or setup errors. If your customer is unable to use the SSO Setup Suite for any reason, you can utilize the instructions below to configure SSO with your customer's OIDC Provider. The below configuration can be done under **Authentication Methods --> SSO** when you select a tenant from the [Tenant tab](https://app.descope.com/tenants) of the Descope Console. ## Configuring SSO with OIDC ### Before You Start To configure OIDC SSO, you'll need to gather some information from your IdP. The exact requirements depend on which OAuth 2.0 flow you're using: #### Authorization Code Flow (Recommended) You'll need: - Client ID and Client Secret - All OIDC endpoints (Authorization, Token, User Info, JWKs) - Optional: Issuer URL and Prompt type #### Implicit Flow with Form Post You'll need: - Client ID - Authorization and User Info endpoints - Optional: Issuer URL and Prompt type Some IdPs may require additional configuration: - The Issuer URL for validating tokens - A specific Prompt type for controlling the login experience - The JWKs endpoint for token verification ### All Settings Here's a comprehensive overview of all the OIDC settings you can configure: #### Tenant Details - **SSO Domains**: Email domain(s) that will use this SSO configuration - **JIT Provisioning**: Controls whether user attributes and groups are updated from your IdP just in time as users log in, instead of being updated through SCIM provisioning. #### Account Settings - **Provider Name**: The name of your IdP (e.g., "Okta", "Azure AD") - **Client ID**: Your application's unique identifier from the IdP - **Client Secret**: A confidential key from your IdP (not needed for Implicit Flow). You can select **Private Key** under **Client Authentication** instead to use Private Key JWT with the Authorization Code flow. - **Scopes**: Permissions your app requests from the IdP (e.g., `email`, `profile`, `groups`) - **Grant Type**: The OAuth 2.0 flow to use: - `Authorization Code` - `Implicit Flow with Form Post` #### Connection Settings - **Issuer**: Your IdP's unique identifier (required by some providers) - **Authorization Endpoint**: Where users are sent to log in - **Token Endpoint**: Where your app requests access and ID tokens - **User Info Endpoint**: Where your app gets user profile information - **JWKs Endpoint**: Where your app gets public keys for token verification #### Prompt - **Prompt**: Controls the login experience at your IdP: - `login`: Force users to log in - `consent`: Force users to grant permissions - `none`: Use existing session if available #### SSO Mapping - **User Attribute Mapping**: Map IdP attributes to Descope user attributes: - `email` → Email - `name` → Display Name - `picture` → Profile Picture - etc. #### SCIM Provisioning You're able to automatically generate a SCIM Bearer token to configure SCIM with your OIDC SSO IdP in this section. The SCIM URL will automatically be populated in the **SCIM URL** field. ![SCIM provisioning in SSO setup](/assets/scim-provisioning-in-saml.webp) #### Advanced Settings - **Manage tokens from provider**: If enabled, Descope will manage the OAuth tokens from your IdP. - **Callback Domain**: The domain for SSO callback responses - **Callback URL**: The URL your IdP will call after authentication - **Redirect URL**: Where users go after successful login The Callback URL will automatically update if you change your custom domain in the Descope Console. User claims and attributes are retrieved from the **User Info Endpoint** (`/userinfo`) that you configure in the Connection Settings. This endpoint provides detailed user profile information including email, name, picture, and other attributes that can be mapped to Descope user attributes. ### Account Settings This section is where you will configure your Client ID, Client Secret (if applicable), and all of the necessary scopes needed for your OIDC request to your IdP. #### Private Key JWT Descope will automatically generate a private key for your project. You cannot use your own private key with this method. With the Authorization Code grant type, select **Private Key** under **Client Authentication** in the Account Settings section to use a signed JWT instead of a client secret, per [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523). With this method, Descope creates a signed JWT using your project's private key during token exchange. Your IdP validates this JWT instead of a client secret. Provide your project's JWK endpoint to your IdP: __BaseURL__/__ProjectID__/.well-known/jwks.json Replace `api.descope.com` with your [custom domain](/how-to-deploy-to-production/custom-domain) if applicable. #### Example Token Request When exchanging the authorization code for tokens, Descope sends a request like this to your IdP's token endpoint with a signed JWT as the `client_assertion`: ```http POST /token HTTP/1.1 Host: idp.example.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTHORIZATION_CODE &redirect_uri=__BaseURL__/v1/oauth/callback &client_id=YOUR_CLIENT_ID &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... ``` ### Connection Settings Here is where you will need to provide all of your IdP related endpoint locations. For more information on Descope's endpoints, as an OIDC provider, you can visit this [docs page](/getting-started/oidc-endpoints). ### Prompt The `Prompt` option allows you to specify the type of user interaction required at the IdP. For instance, `Login` will force the user to enter their credentials regardless of current session status. For more information on how Prompt works, and what you can do with this option, you can read about it under our [Custom Provider page](/auth-methods/oauth#prompt). ### Attribute Mapping Best Practices User Attribute Mapping maps the attributes supported by Descope to the attribute label you defined when setting up your application with your IdP. Configure as many of the supported attributes as you require. The Descope attribute `Email` is required. OIDC has no standard groups claim, but **group → role** and **group → FGA** maps still apply if your IdP sends groups (or a custom claim). Configure those in the tenant's **Roles & Groups** tab, the same place SAML tenants use. See [SSO user and group mapping](/sso/sso-mapping) for details. ### Group Claims (OIDC) OIDC itself doesn't define a `groups` claim. Whether Descope receives groups depends entirely on the IdP: 1. In the IdP, enable group claims (or a custom claim) on the OIDC app / authorization server for your Descope connection. Common patterns: a `groups` claim, or a namespaced claim that lists group names or IDs. 2. In Descope, under the tenant's OIDC **SSO Mapping**, set the **Groups attribute name** to whatever claim name the IdP actually sends (for example `groups`). 3. In the tenant's **Roles & Groups** tab, map those group values to Descope roles and/or FGA relations, the same way as SAML. See [SSO mapping](/sso/sso-mapping). If your project marks **Groups** as a [mandatory attribute](/auth-methods/sso/settings#user-attributes), tenant admins configuring OIDC through the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite#attribute-mapping-user-and-group) must fill in **Groups attribute name** before they can save. SAML works the same way. That only confirms the field is filled in. To confirm the IdP is actually sending group values, run the [SSO Setup Suite connection test](/auth-methods/sso/sso-setup-suite#testing) with JIT provisioning enabled. If the token comes back with no groups, the test fails with `E062028` (see [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting#sso-error-codes)), even when Groups attribute name looks correctly mapped. If roles never appear after OIDC SSO: - Decode the ID token (or check Audits) and confirm a groups-like claim is present. - Confirm the claim name matches **Groups attribute name** exactly (case-sensitive). - Confirm the group values match your map keys (display name vs object ID is a common Entra mismatch). For most customers, the [SSO Setup Suite](/auth-methods/sso/sso-setup-suite) walks through attribute and group mapping with a connection test, so prefer that over hand-editing claim names. Descope also allows you to map attributes from your IdP to [custom user attributes](/management/user-management#custom-user-attributes) when configuring your attribute mapping. | Provider user identifier | Descope user attribute | | ------------------- | ----------------- | | email | Login ID (**Required**) | | name | Display Name | | email | Email | | picture | Picture | # SSO with SAML (/auth-methods/sso/saml) SSO (Single Sign-on) with SAML Provider Configuration # SSO (Single Sign-on) with SAML We highly recommend having your customer/tenant set up SSO for their own organization using our [SSO Setup Suite](/auth-methods/sso/sso-setup-suite). The SSO Setup Suite walks your customers through the entire SSO Configuration process, with templates for all common IdPs, and allows them to test the connection, minimizing any back and forth or setup errors. If your customer is unable to use the SSO Setup Suite for any reason, you can utilize the instructions below to configure SSO with your customer's SAML Provider. The below configuration can be done under **Authentication Methods --> SSO** when you select a tenant from the [Tenant tab](https://app.descope.com/tenants) of the Descope Console. ## Configuring SSO with SAML ### Before You Start To configure SSO within a tenant, you'll need to provide Descope with your IdP's configuration details. You can provide these details in two ways: #### Option 1: Metadata URL (Recommended) If your IdP provides a metadata URL, use this method. Descope will automatically: - Retrieve all required configuration details - Update settings if your IdP configuration changes - Ensure your SSO connection stays up-to-date #### Option 2: Manual Configuration Unlike the metadata URL method, manual configuration does not automatically pick up IdP changes. If your IdP rotates its signing certificate or updates other settings, you'll need to update Descope manually. See [Certificate and metadata rotation](/management/tenant-management/sso/cert-and-metadata-rotation). Alternatively, you can manually copy and paste each required piece of information from your IdP into Descope. ### All Settings Here's a comprehensive overview of all the settings you can configure for SSO: #### Tenant Details - **SSO Domains**: Email domain(s) that will use this SSO configuration - **JIT Provisioning**: Controls whether user attributes and groups are updated from your IdP just in time as users log in, instead of being updated through SCIM provisioning. #### Identity Provider (IdP) Settings - **Metadata URL**: A URL containing all your IdP's connection details. This is the easiest way to configure SSO as it automatically updates if your IdP settings change - **Login URL**: The URL where users are redirected to sign in (also called "SSO URL" or "Single Sign-on URL") - **Entity ID**: A unique identifier for your IdP - **Certificate**: The certificate used to verify the authenticity of messages between your IdP and Descope #### Post-Authentication Redirect URL - **Redirect URL**: The default URL where users are sent after successful sign-in. This is an optional argument in the API and SDKs, and the URL provided in the API or SDK will take precedence over the URL entered here. #### Service Provider (SP) Settings These are the settings you'll need to configure in your IdP: - **Descope Entity ID**: Your unique identifier in the SAML communication - **Descope ACS URL**: The endpoint where your IdP sends authentication responses - **Descope XML URL**: Metadata URL for your Descope configuration (read-only) #### SSO Keys By default Descope signs the SAML request using our internal private key. However, if you prefer, you can upload your custom private key instead. You can also override the key pair for handling the IdP SAML response encryption, with your custom private key as well. ![SSO keys](/assets/sso-keys.webp) #### SSO Mapping - **User Attribute Mapping**: Maps IdP attributes (like email and phone) to Descope user attributes - **Group Mapping**: - **Groups Attribute Name**: The attribute your IdP uses to identify groups. Required when your project marks **Groups** as a [mandatory attribute](/auth-methods/sso/settings#user-attributes). RBAC group mapping, default roles, and FGA group mapping are configured in the **Roles & Groups** tab, not per SSO method, and apply to both SSO logins and SCIM provisioning. See [SSO user and group mapping](/sso/sso-mapping). #### SCIM Provisioning You're able to automatically generate a SCIM Bearer token to configure SCIM with your SAML SSO IdP in this section. The SCIM URL will automatically be populated in the **SCIM URL** field. ![SCIM provisioning in SSO setup](/assets/scim-provisioning-in-saml.webp) ### Identity Provider (IdP) The Identity Provider (IdP) section contains all the application information you registered with your IdP, including the Login URL, Entity ID, and Certificate. Descope needs these details so we can act as the SP on your behalf. Paste the information from your IdP into the console, or enter the Metadata URL if your IdP provides it. ### Post Authentication Redirect URL When using IdP-Initiated Authentication, you must provide a [Post Authentication Redirect URL](/sso/idp-initiated#what-you-must-configure). The Redirect URL is the default URL an end user is redirected to after a successful SSO authentication. You can also set this as an optional argument in the API and SDKs, and the URL provided in the API or SDK will take precedence over the URL entered here. ### Service Provider (SP) The Service Provider (SP) section contains all the application information necessary to configure your application within your IDP. The data presented here are specific to the tenant you are configuring SSO for. ### SSO User Mapping SSO User Mapping maps the attributes in Descope to the IdP attribute name you defined when setting up your application with your IdP. You can map as many default or custom attributes as your IdP requires. Descope also allows you to map attributes from your IdP to [custom user attributes](/management/user-management#custom-user-attributes) when configuring your attribute mapping. #### User Attribute Mapping In the example below, we demonstrate how to map user attributes between your Identity Provider (IdP) and Descope. As an example, the table below illustrates three attributes `email`, `login`, and `phone`, and shows how they are defined in both the IdP console and the Descope console. To store specific attributes about your end users, first configure them in your IdP console, then add the corresponding mappings in the Descope console. | IdP's Attribute Name | Descope Attribute | | ------------------- | ----------------- | | email | Email | | login | Display Name| | phone | Phone Number | By default, in Microsoft Entra ID (formerly Azure AD), the user attributes within the assertion are sent in a **link format** (e.g., as URIs). An example of this would be `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress` for the user's email address. Be sure to configure them correctly to ensure proper mapping with Descope. ### Group Mapping Group mapping connects IdP groups from the SAML assertion to Descope authorization: - **RBAC group mapping.** IdP group to Descope [roles](/authorization/role-based-access-control) on the tenant. - **FGA group mapping.** IdP group to [FGA / ReBAC](/authorization) relation tuples. The two are independent: a group can map to roles, FGA relations, or both. Full model, API field names (`fgaMappings` vs `rebacGroupsMappings`), defaults, and helper APIs: [SSO user and group mapping](/sso/sso-mapping). ### How RBAC Group Mapping Works 1. **Define groups in your IdP** (org units, roles, departments, for example Engineering, HR, Sales). 2. **Map groups to roles in Descope.** For example, IdP `HR` becomes Descope `HR Team`. 3. **On SSO login**, Descope reads group claims from the assertion and assigns the mapped roles. 4. Your app enforces access using those [roles](/authorization/role-based-access-control). ### Configuring RBAC Group Mapping 1. In the Descope Console, open the tenant → define roles under **Roles & Permissions** that match your IdP groups. 2. On the tenant’s **SSO Configuration**, open **Group Mapping** and set the **Groups Attribute Name** (the IdP attribute that carries groups, e.g. `groups` for Entra ID). 3. Open the **Roles & Groups** tab for that tenant and map each IdP group name to a Descope role. ![RBAC Roles and Groups mapping](/assets/rbac-roles-and-groups-mapping.webp) If your project marks **Groups** as a [mandatory attribute](/auth-methods/sso/settings#user-attributes), **Groups Attribute Name** becomes a required field on this tenant's SAML configuration. If you leave it empty, saving fails with a "Missing required mapping" error, so you catch a missing group mapping before it causes login or role-assignment problems. That check only confirms the field is filled in. It doesn't confirm the IdP actually sends group values. For that, run the [SSO Setup Suite connection test](/auth-methods/sso/sso-setup-suite#testing) with JIT provisioning enabled: if the IdP's assertion comes back with no groups, the test fails with `E062028`, even when Groups Attribute Name looks correctly mapped. Test with a known user after saving. For FGA maps, follow [Groups → FGA](/sso/sso-mapping#groups-to-fga-relations) (requires an FGA schema on the project). ### Using Group Mapping with Other IdPs Enable group claims in the IdP’s SAML (or OIDC) settings, then map the same way. Examples: - **Entra ID**: [Configuring group claims](https://learn.microsoft.com/en-us/entra/identity-platform/saml-claims-customization) - **Okta**: [Group attribute statements](https://help.okta.com/en-us/content/topics/apps/define-group-attribute-statements.htm) - **Descope how-to**: [SSO user and group mapping](/sso/sso-mapping) # Backend SDKs (/auth-methods/sso/with-sdks/backend) Add single sign-on (SSO) to your application using Descope Backend SDKs. Read the detailed implementation guide with sample code. # Single Sign-On (SSO) with Backend SDKs This is the preferred way to add SSO when you keep your own sessions: your server starts SSO and exchanges the code, so the frontend mostly redirects. For the full walkthrough (configure a tenant, then wire start + callback), see [Getting Started with SSO](/auth-methods/sso/getting-started). If you're building with Descope Flows instead, start with the [Flows quickstart](/getting-started). Use the backend SDK when you want your server to drive the SSO handshake. Your backend starts the login, exchanges the authorization code (so it never sits in the browser), and validates the session afterward. SSO is configured per tenant (SAML or OIDC). See [tenant management](/management/tenant-management) for how to set that up. Before the code below will work, [enable SSO at the project level](/auth-methods/sso/settings) and configure a SAML or OIDC connection for at least one [tenant](https://app.descope.com/tenants). For a full walkthrough, see [Getting Started with SSO](/auth-methods/sso/getting-started), or [test without your own IdP](/management/tenant-management/sso/mock-saml-testing). ## How the Flow Works There are three steps: 1. **Start SSO.** Your backend calls `sso.start` and gets back a URL. Redirect the user's browser to it. Descope sends them to the tenant's identity provider, and after they authenticate, the browser returns to your `redirect_url` with a one-time `code`. 2. **Exchange the code.** Your backend calls `sso.exchange` with that `code` and receives the session and refresh tokens. 3. **Validate the session.** Set the session as a cookie (or return it to your client), then validate it on later requests. Descope acts as the SAML/OIDC service provider toward the identity provider, so you don't build the federation yourself. ## Install and Initialize ## Step 1: Start SSO When the user clicks sign in with SSO, call the start function from your backend. It returns a URL you redirect the browser to, which kicks off login with the tenant's identity provider. ### Identifying the Tenant (`tenantIdOrEmail`) The first argument is a tenant ID, a tenant name, or the user's email: - **Email:** Descope matches the email domain to a tenant [SSO Domain](/auth-methods/sso#option-1-sso-domains) and starts that tenant's SSO connection. For example, the user enters `alex@acme.com`. You call `sso.start('alex@acme.com', …)`, and Descope returns the Acme IdP URL. - **Tenant ID or name:** Skip the domain lookup when your app already knows the org, whether from a subdomain, an org picker, or an invite. Configure SSO Domains before relying on email. See [Tenant identification methods](/auth-methods/sso#tenant-identification-methods). For several IdPs under one tenant, also set domains per connection or pass `ssoId` ([Multiple SSO providers](/sso/multi-sso)). ### Start Options | Parameter | Applies to | Purpose | | --------- | ---------- | ------- | | `tenantIdOrEmail` (required) | All | Tenant ID, tenant name, or email for SSO Domain lookup | | `redirectUrl` / `return_url` | All | Where Descope sends the browser after IdP auth, with `?code=…` | | `loginHint` / `login_hint` | OIDC (often); some SAML IdPs | Hint to the IdP about which account to use, usually the same email the user typed | | `ssoId` / `sso_id` | Multi-SSO tenants | Select a specific SSO configuration on the tenant | | `prompt` | OIDC | IdP prompt (`login`, `consent`, `none`, …). See [Prompt](/auth-methods/oauth#prompt). | | `forceAuthn` / `force_authn` | SAML | Require fresh authentication at the IdP even if a session exists | | `enforceInitiatedEmail` | SAML & OIDC | Require the IdP's response to match the email you started with; blocks sign-in otherwise. See [below](#verify-the-idp-email-matches-the-initiated-email) | | `loginOptions` | All | `stepup`, `mfa`, `customClaims`, `templateOptions` | | refresh / MFA token | When step-up or MFA | Current refresh JWT so Descope can elevate the existing session | Node / Web JS positional order for `sso.start`: `tenantIdOrEmail`, `redirectUrl`, `loginOptions`, `token`, `ssoId`, `forceAuthn`, `loginHint`, `enforceInitiatedEmail`. Python uses named args (`tenant`, `return_url`, `login_options`, `prompt`, `sso_id`, `login_hint`, `force_authn`, …). Check your SDK version for exact names. `enforceInitiatedEmail` can be used in the Node.js/TypeScript backend SDK and the client-side JS SDKs (React, Web Component, Web JS). If you're using a different SDK that doesn't support it, call the REST API's `initiatedEmail` query parameter directly instead (see below). ### Verify the IdP Email Matches the Initiated Email If a user starts SSO with their own email (SP-initiated), you can require Descope to confirm the identity provider authenticated that same person before it completes sign-in. The IdP session in the user's browser might belong to a different account than the one they typed into your app. Pass `enforceInitiatedEmail: true` (or `initiatedEmail=` on the REST API) alongside the email you're starting with: ```javascript const email = 'alex@acme.com'; const resp = await descopeClient.sso.start( email, // SSO Domain → tenant + IdP URL, and the value enforceInitiatedEmail checks against redirectUrl, undefined, // loginOptions undefined, // refresh token undefined, // ssoId false, // forceAuthn email, // loginHint true, // enforceInitiatedEmail ); ``` REST equivalent: add `initiatedEmail=alex@acme.com` to the [Start SSO](/api/sso/start-sso) query parameters. This check: - Compares the email you passed in against the SAML email attribute or NameID (or, for OIDC, the email claim), case-insensitively. - Fails `sso.exchange` with `E062020` and blocks sign-in if they don't match. See [SSO troubleshooting](/other-troubleshooting/sso-troubleshooting#sso-login-and-federation). - This only applies when SSO is started with an email address. It doesn't apply to SSO started by tenant ID or tenant name, or to [IdP-initiated logins](/sso/idp-initiated), as in both cases there's no email to compare against. The same option is available from a Descope Flow via the SSO action's **Verify initiated email matches IdP response** toggle. See [SSO with Flows](/auth-methods/sso/with-flows#verify-initiated-email-matches-idp-response). #### Example: Email and Login Hint (OIDC-Friendly) ```javascript const email = 'alex@acme.com'; const redirectUrl = 'https://app.example.com/auth/sso/callback'; const resp = await descopeClient.sso.start( email, // SSO Domain → tenant + IdP URL redirectUrl, undefined, // loginOptions undefined, // refresh token undefined, // ssoId false, // forceAuthn email, // loginHint ); ``` ```python url = descope_client.sso.start( tenant="alex@acme.com", return_url="https://app.example.com/auth/sso/callback", login_hint="alex@acme.com", ) ``` #### Example: Known Tenant with a Multi-SSO Profile ```python url = descope_client.sso.start( tenant="acme-tenant-id", return_url="https://app.example.com/auth/sso/callback", sso_id="contractors", # which IdP on that tenant ) ``` REST equivalent: [Start SSO](/api/sso/start-sso) (`tenant`, `redirectUrl`, `loginHint`, `forceAuthn`, `prompt`, …). ### Start Code Samples ```ts title="app/api/auth/sso/route.ts" import { NextRequest, NextResponse } from 'next/server'; import { sdk } from '@/lib/descope'; // POST /api/auth/sso body: { "email": "alex@acme.com" } export async function POST(req: NextRequest) { const { email } = await req.json(); const redirectUrl = 'https://app.example.com/api/auth/sso/callback'; // Descope matches the email domain to the tenant's SSO Domain and returns // that tenant's IdP authorization URL. loginHint pre-fills the user at OIDC IdPs. const resp = await sdk.sso.start(email, redirectUrl, undefined, undefined, undefined, false, email); if (!resp.ok) { return NextResponse.json(resp.error, { status: 400 }); } // Redirect the user's browser to the returned URL. return NextResponse.redirect(resp.data.url); } ``` ```javascript // Args: // tenantIdOrEmail: tenant ID, tenant name, OR user email (email → SSO Domain lookup) const tenant_name_id_or_email = "alex@acme.com" // redirect_url: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirect_url = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa // ssoId (optional): SSO configuration ID when the tenant has multiple IdPs // forceAuthn (optional): SAML only; force re-auth at the IdP // loginHint (optional): OIDC; hint or pre-fill the user at the IdP (often the same email) const resp = await descopeClient.sso.start( tenant_name_id_or_email, redirect_url, loginOptions, undefined, // refreshToken undefined, // ssoId false, // forceAuthn tenant_name_id_or_email.includes("@") ? tenant_name_id_or_email : undefined, // loginHint ); if (!resp.ok) { console.log("Failed to start sso auth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const url = resp.data.url console.log("Successfully started sso auth. URL: " + url) } ``` ```python # Args: # tenant: tenant ID, tenant name, OR user email (email → SSO Domain lookup) tenant_name_id_or_email = "alex@acme.com" # redirect_url: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. # login_options (LoginOptions): this allows you to configure behavior during the authentication process. # refresh_token (optional): the user's current refresh token in the event of stepup/mfa # prompt (optional): OIDC-only prompt value (e.g., "login", "consent") # sso_id (optional): SSO configuration ID to use # login_hint (optional): Hint about the user's login identifier (OIDC) # force_authn (optional): SAML-only value that can force authentication even if the user already has a session try: resp = descope_client.sso.start( tenant=tenant_name_id_or_email, return_url="https://auth.company.com/token_exchange", login_options={ "stepup": False, "mfa": False, "custom_claims": {"claim": "Value1"}, "template_options": {"option": "Value1"} }, prompt=None, sso_id=None, login_hint=tenant_name_id_or_email if "@" in tenant_name_id_or_email else None, force_authn=False ) print("Successfully started sso auth. URL: ") print(resp) except AuthException as error: print("Failed to start sso auth") print("Status Code: " + str(error.status_code)) print("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // tenant: Name of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. tenant = "xxxx" // returnURL: url for redirecting the user after authentication with social oauth provider. This value will override the value in the console settings. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. returnURL := "https://auth.company.com/token_exchange" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: this allows you to configure behavior during the authentication process. loginOptions := &descope.LoginOptions{ Stepup: true, MFA: true, CustomClaims: map[string]any{}{"test": "testClaim"}, TemplateOptions: map[string]any{"option": "Value1"} } // w: ResponseWriter to update with correct redirect url. You can return this to your client for redirect. redirectURL, err:= descopeClient.Auth.SSO.Start(ctx, tenant, returnURL, r, loginOptions, w) if (err != nil){ fmt.Println("Failed to initialize SSO flow: ", err) } else { fmt.Println("Successfully started SSO flow: ", redirectURL) } ``` ```java // Choose which tenant to log into // Redirect the user to the returned URL to start the SSO redirect chain SAMLService ss = descopeClient.getAuthenticationServices().getSAMLService(); try { String returnURL = "https://my-app.com/handle-sso"; String url = ss.start("my-tenant-ID", returnURL, loginOptions); } catch (DescopeException de) { // Handle the error } ``` ```ruby descope_client.saml_sign_in( tenant: 'my-tenant-ID', # Choose which tenant to log into return_url: 'https://my-app.com/handle-saml', # Can be configured in the console instead of here prompt: 'custom prompt here' ) ``` ```php $response = $descopeSDK->auth->sso->signIn( "tenant", "https://example.com/callback", "prompt", true, true, ["custom" => "claim"], "ssoAppId" ); print_r($response); ``` ```csharp // Args: // tenant (string): ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. var tenant = "my-tenant-ID"; // redirectUrl (string?): An optional parameter to generate the SSO link. If not given, the project default will be used. string? redirectUrl = "https://my-app.com/handle-saml"; // prompt (string[]?): OIDC-only prompt values (e.g., "login", "consent"). string[]? prompt = null; // forceAuthn (bool?): SAML-only value that can force auth even if the user already has a session. bool? forceAuthn = false; // loginOptions (LoginOptions): Step-up / MFA options (passed as the request body). var loginOptions = new LoginOptions { Stepup = false, Mfa = false, }; try { var response = await descopeClient.Auth.V1.Sso.Authorize.PostWithQueryParamsAsync( loginOptions, tenant: tenant, redirectUrl: redirectUrl, prompt: prompt, forceAuthn: forceAuthn); var url = response?.Url; } catch (DescopeException ex) { // Handle the error } ``` ## Step 2: Exchange the Code After the user authenticates with the IdP, they're sent back to the `redirect_url` you passed to `sso.start`. Pull the `code` query parameter from that URL and exchange it for tokens: ```ts title="app/api/auth/sso/callback/route.ts" import { NextRequest, NextResponse } from 'next/server'; import { sdk } from '@/lib/descope'; // GET /api/auth/sso/callback?code=... export async function GET(req: NextRequest) { const code = req.nextUrl.searchParams.get('code'); if (!code) { return NextResponse.json({ error: 'missing code' }, { status: 400 }); } const resp = await sdk.sso.exchange(code); if (!resp.ok) { return NextResponse.json(resp.error, { status: 401 }); } // resp.data has sessionJwt, refreshJwt, and user. Validate it, then start your session. return NextResponse.redirect('https://app.example.com/'); } ``` ```javascript // Args: // code: code extracted from the url after user is redirected to redirect_url. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const resp = await descopeClient.sso.exchange(code); if (!resp.ok) { console.log("Failed to verify sso code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified sso code.") } ``` ```python # Args: # code: code extracted from the url after user is redirected to redirect_url. The code is in the url of the page. code = "xxxxx" try: resp = descope_client.sso.exchange_token(code=code) print ("Successfully verified sso code.") print (resp) except AuthException as error: print ("Failed to verify sso code") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // code (string): code should be extracted from the redirect URL of OAuth authentication from the query parameter `code`. code := "xxxxxx" // w: ResponseWriter to update with correct session details. You can return this to your client for setting the cookies which are used for session validation authInfo, err := descopeClient.Auth.SSO.ExchangeToken(ctx, code, w) if (err != nil){ fmt.Println("Failed to verify sso code: ", err) } else { fmt.Println("Successfully verified sso code: ", authInfo) } ``` ```java // The optional `w http.ResponseWriter` adds the session and refresh cookies to the response automatically. // Otherwise they're available via authInfo SAMLService ss = descopeClient.getAuthenticationServices().getSAMLService(); try { String url = ss.exchangeToken(code); } catch (DescopeException de) { // Handle the error } ``` ```ruby jwt_response = descope_client.saml_exchange_token(code) session_token = jwt_response[Descope::Mixins::Common::SESSION_TOKEN_NAME].fetch('jwt') refresh_token = jwt_response[Descope::Mixins::Common::REFRESH_SESSION_TOKEN_NAME].fetch('jwt') ``` ```php $response = $descopeSDK->auth->sso->exchangeToken("code"); print_r($response); ``` ```csharp // Args: // code (string): code extracted from the url after user is redirected to redirect_url. The code is in the url as a query parameter "code" of the page. var code = "authorization-code"; try { var authRes = await descopeClient.Auth.V1.Sso.Exchange.PostAsync( new ExchangeTokenRequest { Code = code }); } catch (DescopeException ex) { // Handle the error } ``` ## Step 3: Validate the Session Once you have the tokens, validate the user session on later requests. Descope covers session timeouts, logout, and related options. See [backend session validation](/sessions/validation/backend) for details and sample code. # Client SDK (/auth-methods/sso/with-sdks/client) Add single sign-on (SSO) to your application using Descope Client SDKs. Read the detailed implementation guide with sample code. # Single Sign-On (SSO) with Client SDKs Use the client SDK when the SSO handshake has to run in the browser. This usually means a single-page app with no backend to exchange the code. If you do have a server, [SSO with Backend SDKs](/auth-methods/sso/with-sdks/backend) is usually the better fit, because it keeps the code exchange off the client. If you want Descope to own login and sessions instead, start with the [Flows quickstart](/getting-started). SSO is configured per tenant, so each customer's SAML or OIDC connection can point at a different identity provider. See [tenant management](/management/tenant-management) for how to set that up. Before the code below will work, [enable SSO at the project level](/auth-methods/sso/settings) and configure a SAML or OIDC connection for at least one [tenant](https://app.descope.com/tenants). For the full walkthrough, see [Getting Started with SSO](/auth-methods/sso/getting-started), or [test without your own IdP](/management/tenant-management/sso/mock-saml-testing). ## How the Flow Works With the client SDKs, the SDK running in the user's browser drives the login. There are three steps. 1. **Start SSO.** You call `sso.start`, and the SDK redirects the browser to the tenant's identity provider. After the user authenticates, the browser comes back to your `redirectURL` with a one-time `code`. 2. **Exchange the code.** You call `sso.exchange` with that `code`. The SDK gets the user's session tokens back and stores them for you. 3. **Validate the session.** The SDK keeps the session fresh with auto-refresh and validates it on later requests. The diagram below shows who talks to whom at each step. Descope acts as the SAML/OIDC service provider toward the identity provider, so you never build the federation yourself. ## Client SDK For information on how to install and initialize the Descope Client SDK, please refer to the [Client SDK Installation Guide](/client-sdk/initialize-sdk). ## Step 1: Start SSO When the user clicks sign in with SSO, call `sso.start`. The SDK redirects the browser straight to the tenant's identity provider login screen. ```javascript // Args: // tenant_name_id_or_email: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. const tenant_name_id_or_email = "xxxx" // redirectURL: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.sso.start(tenant_name_id_or_email, redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start sso auth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const url = resp.data.url console.log("Successfully started sso auth. URL: " + url) } ``` ```javascript // Args: // tenant_name_id_or_email: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. const tenant_name_id_or_email = "xxxx" // redirectURL: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const resp = await descopeSdk.sso.start(tenant_name_id_or_email, redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start sso auth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const url = resp.data.url console.log("Successfully started sso auth. URL: " + url) } ``` ```html ``` ## Step 2: Exchange the Code After the user authenticates, the IdP sends them back to the `redirectURL` you passed to `sso.start`, with the code in the query string. Pull the `code` off the URL and exchange it as shown below. ```javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.sso.exchange(code); if (!resp.ok) { console.log("Failed to verify sso code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified sso code.") } ``` ```javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const resp = await descopeSdk.sso.exchange(code); if (!resp.ok) { console.log("Failed to verify sso code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified sso code.") } ``` ```html ``` ## Verify the IdP Email Matches the Initiated Email When you start SSO with the user's own email, you can require Descope to confirm the identity provider authenticated that same person before completing sign-in. The IdP session in the browser might belong to a different account than the one entered in your app. ```javascript const email = 'alex@acme.com'; const resp = await descopeSdk.sso.start( email, redirectURL, undefined, // loginOptions undefined, // token undefined, // ssoId false, // forceAuthn email, // loginHint true, // enforceInitiatedEmail ); ``` If the email (or NameID) the IdP returns doesn't match, case-insensitively, `sso.exchange` fails with [`E062020`](/other-troubleshooting/sso-troubleshooting#sso-login-and-federation) instead of returning a session. This has no effect on tenant ID/name starts or [IdP-initiated logins](/sso/idp-initiated), since there's no initiated email to compare against. If you're building with Descope Flows instead, use the SSO action's **Verify initiated email matches IdP response** toggle. See [SSO with Flows](/auth-methods/sso/with-flows#verify-initiated-email-matches-idp-response). # Mobile SDKs (/auth-methods/sso/with-sdks/mobile) Add single sign-on (SSO) to your application using Descope Mobile SDKs. Read the detailed implementation guide with sample code. # Single Sign-On (SSO) with Mobile SDKs Use the mobile SDK to run SSO natively in an iOS, Android, Flutter, or React Native app. SSO is configured per tenant, so each customer's SAML or OIDC connection can point at a different identity provider. See [tenant management](/management/tenant-management) for how to set that up. Before the code below will work, [enable SSO at the project level](/auth-methods/sso/settings) and configure a SAML or OIDC connection for at least one [tenant](https://app.descope.com/tenants). For the full walkthrough, see [Getting Started with SSO](/auth-methods/sso/getting-started), or [test without your own IdP](/management/tenant-management/sso/mock-saml-testing). ## Client SDK ### Install SDK ```swift // 1. Within XCode, go to File > Add Packages // 2. Search for the URL of the git repo: https://github.com/descope/descope-swift // 3. Configure your desired dependency rule // 4. Click Add Package ``` ```kotlin // 1. Within Android Studio, go to File > Project Structure > Dependencies > Add Dependency > 1 Library Dependency // 2. Search for the dependency: "com.descope" // 3. Configure your desired dependency rules // 4. Click "Ok" ``` ```dart // 1. From your Flutter project directory root, install the Descope SDK by running: flutter pub add descope // 2. Or, add Descope to your pubspec.yml by including this line: descope: ^0.9.0 // View the package on pub.dev: https://pub.dev/packages/descope ``` ```javascript // 1. From your React Native project directory root, install the Descope SDK by running: npm i @descope/react-native-sdk // View the package: https://github.com/descope/descope-react-native ``` ### Import and Initialize SDK ```swift import DescopeKit import AuthenticationServices do { Descope.setup(projectId: "__ProjectID__") { config in // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseURL = "https://auth.app.example.com" } print("Successfully initialized Descope") } catch { print("Failed to initialize Descope") print(error) } ``` ```kotlin import android.app.Application import com.descope.Descope class MyApplication : Application() { override fun onCreate() { super.onCreate() try { Descope.setup(this, projectId = "__ProjectID__") { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies baseUrl = "https://auth.app.example.com" // Enable the logger logger = DescopeLogger.debugLogger } } catch (e: Exception) { Log.e("ERROR", e.stackTraceToString()) } } } ``` ```dart import 'package:descope/descope.dart'; // Where your application state is being created Descope.setup('__ProjectID__', (config) { // Optional: Only set baseURL if using a custom domain with Descope and managing token response with cookies config.baseUrl = 'https://auth.app.example.com'; }); await Descope.sessionManager.loadSession(); ``` ```javascript import { AuthProvider } from '@descope/react-native-sdk' const AppRoot = () => { return ( ) } ``` ## Start SSO When the user taps sign in with SSO, call the start function. It opens the tenant's identity provider login screen in a browser webview. ```swift // Args: // emailOrTenantName: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. let emailOrTenantName = "email@company.com" // redirect_url: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. let redirectURL = "exampleauthschema://auth.company.com/handle-sso" guard let session = Descope.sessionManager.session else { return } var signInOptions: [SignInOptions] = [ .customClaims(["name": "{{user.name}}"]), .mfa(refreshJwt: session.refreshJwt), .stepup(refreshJwt: session.refreshJwt) ] do { let authURL = try await Descope.sso.start(emailOrTenantName: emailOrTenantName, redirectURL: redirectURL, options: signInOptions) guard let authURL = URL(string: authURL) else { return } print("Successfully initiated SSO Authentication") } catch { print("Failed to initiate SSO Authentication") print(error) } ``` ```kotlin // Args: // emailOrTenantName: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. val emailOrTenantName = "email@company.com" // redirectURL: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. val redirectURL = "exampleauthschema://auth.company.com/handle-sso" // options: optional options to get attributes like custom claims, stepup, mfa, and revoke sessions in response val options = listOf( SignInOptions.CustomClaims(mapOf("cc1" to "yes", "cc2" to true)), SignInOptions.StepUp(session.refreshJwt), SignInOptions.Mfa(session.refreshJwt), SignInOptions.RevokeOtherSessions ) try { val authURL = Descope.sso.start(emailOrTenantName, redirectURL, options) val uri = Uri.parse(authURL) if (uri != null) { println("Successfully initiated SSO Authentication") } else { println("Failed to initiate SSO Authentication") } } catch (exception: Exception) { println("Failed to initiate SSO Authentication") println(exception) } ``` ```dart // Args: // emailOrTenantName: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. let emailOrTenantName = "email@company.com" // redirect_url: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. let redirectURL = "exampleauthschema://auth.company.com/handle-sso" // options: Optional options to get custom claims in response const options = SignInOptions(customClaims: {'name': '{{user.name}}'}); // Choose which tenant to log into // If configured globally, the return URL is optional. If provided however, it will be used // instead of any global configuration. final authUrl = await Descope.sso.start( emailOrTenantId: 'my-tenant-ID', redirectUrl: 'exampleauthschema://my-app.com/handle-sso', options: options); ``` ```javascript // Args: // tenant_name_id_or_email: ID of the tenant that the user is authenticating to. The tenant ID is assigned to tenant at the time of creation. const tenant_name_id_or_email = "xxxx" // redirectURL: URL to return to after successful authentication with the SSO identity provider. You need to implement this page to access the token and finish oauth process (token exchange). The token arrives as a query parameter named 'code'. const redirectURL = "https://auth.company.com/token_exchange" // loginOptions (LoginOptions): this allows you to configure behavior during the authentication process. const loginOptions = { "stepup": false, "mfa": false, "customClaims": {"claim": "Value1"}, "templateOptions": {"option": "Value1"} } // refreshToken (optional): the user's current refresh token in the event of stepup/mfa const descopeSdk = useDescope(); const resp = await descopeSdk.sso.start(tenant_name_id_or_email, redirectURL, loginOptions); if (!resp.ok) { console.log("Failed to start sso auth") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { const url = resp.data.url console.log("Successfully started sso auth. URL: " + url) } ``` ## SSO Exchange Code After the user authenticates, the IdP sends them back to the `redirect_url` you passed to the start function, with the code in the query string. Pull the `code` off the URL and exchange it as shown below. ```swift // Args: // authURL: the authURL generated from the Start SSO let authURL = "xxxxx" do { let session = ASWebAuthenticationSession( url: authURL, callbackURLScheme: "exampleauthschema") { callbackURL, error in guard let url = callbackURL else {return} let component = URLComponents(url: url, resolvingAgainstBaseURL: false) guard let code = component?.queryItems?.first(where: {$0.name == "code"})?.value else { return } print(code) // Exchange code for session Task { do { let descopeSession = try await Descope.sso.exchange(code: code) print("Successfully Completed SSO Authentication") print(descopeSession as Any) } catch { print("Failed to Complete SSO Authentication") print(error) } } } session.presentationContextProvider = self session.prefersEphemeralWebBrowserSession = true session.start() } catch { print("Failed to Complete SSO Authentication") print(error) } ``` ```kotlin // Args: // authURL: the authURL generated from the Start SSO val code = "xxxxxx" // Code from authURL try { if (code != null) { val descopeSession = Descope.oauth.exchange(code) println("Successfully completed OAuth Authentication") println(descopeSession) } } catch (exception: Exception) { println("Failed to complete SSO Authentication") println(exception) } ``` ```dart // Args: // authURL: the authURL generated from the Start SSO const authURL = "xxxxx"; // Redirect the user to the returned URL to start the OAuth redirect chain final result = await FlutterWebAuth.authenticate( url: authUrl, callbackUrlScheme: 'exampleauthschema'); // Extract the returned code final code = Uri.parse(result).queryParameters['code']; // Exchange code for an authentication response final authResponse = await Descope.sso.exchange(code: code!); ``` ```javascript // Args: // code: code extracted from the url after user is redirected to redirectURL. The code is in the url as a query parameter "code" of the page. const code = "xxxxx" const descopeSdk = useDescope(); const resp = await descopeSdk.sso.exchange(code); if (!resp.ok) { console.log("Failed to verify sso code") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully verified sso code.") } ``` ## Session Validation The last step is validating the user's session. Descope handles session management for you, including configurable timeouts and logout. For the details and sample code, see [mobile session validation](/sessions/management/mobile). # Amplitude (/connectors/connector-configuration-guides/analytics/amplitude) Descope's Amplitude connector allows you to collect events from web and mobile apps to your Amplitude account. # Amplitude Connector Descope's Amplitude connector will allow you to use Amplitude for analytics. Amplitude is an analytics product which collects events from your app either web/mobile and presents them in a unified manner and to utilize them to better understand your customer needs. ## Configure Amplitude connector Navigate to Descope's connector menu and search for Amplitude Connector. Fill in the following parameters required for the connection: - **Name**: Connector's custom name. This will be helpful when creating multiple connectors from the same template. - **Description**: (Optional) Describes the purpose of this connector. - **API Key**: The Amplitude API Key generated for the Descope service. Read more on how to find it in [Amplitude](https://amplitude.com/docs/apis/authentication) - **Server URL**: (Optional) The server URL of Amplitude API, when using different API or a custom domain in Amplitude. - **Server Zone**: (Optional) Sets amplitude's server to EU or US zone. Default being US zone. ![Amplitude connector setup](/assets/amplitude-connector-setup.webp) ### Testing Before creating your connector, verify its working by clicking on "Test". The Test Results tab gives a successful result of a test event being created on Amplitude. ## Add Amplitude connector to a flow Navigate to Flows, choose your flow. Then, tap the create button (designated via the + icon), select Connector, and choose the Amplitude tracking option. ![Amplitude track in flow](/assets/amplitude-track-in-flow.webp) Click to set down the connector in your flow editor and fill out the fields: - **Event**: Name of the event you are tracking - **User ID**: The unique ID of the user - **Device ID**: User's device ID - **Event Property**: Any additional information that you can tie with the event which will help for better analytics. - **User Property**: Additional information related to the user like their IP address etc. - **Opt out**: Here, you set permission to allow Amplitude to post events. If `true`, prevents Amplitude from tracking and uploading events. Default value is `False`. Now, link your connector in your authentication flow, making sure it's placed after any required attributes you just chose are defined. ![Amplitude flow setup](/assets/amplitude-flow.webp) Simply by adjusting the attributes and location of the connector, you can track anything from a user sign up/in to the method used for authentication. ![Amplitude events](/assets/amplitude-events.webp) ## Viewing Logs The events are then reflected on Amplitude's dashboard. Further analysis can be achieved by creating customized dashboards for better understanding. ![Amplitude activity](/assets/amplitude-activity.webp) ![Amplitude dashboard](/assets/amplitude-dashboard.webp) # Google Analytics (/connectors/connector-configuration-guides/analytics/google-analytics) Descope's Google Analytics connector lets you send authentication events to Google Analytics 4 using the Measurement Protocol. # Google Analytics Connector Descope's Google Analytics connector allows you to send events from your authentication flows directly to a Google Analytics 4 (GA4) property, using GA4's Measurement Protocol. This lets you track sign-ups, sign-ins, and other authentication events alongside the rest of your product analytics, without adding any client-side tracking code to your login pages. ## Configure Google Analytics connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Google Analytics to create a new connector. Fill in the following parameters: - **Connector Name**: Custom name for your connector. This is useful when creating multiple connectors from the same template. - **Connector Description**: (Optional) Describe what this connector is used for. - **Measurement ID**: Your GA4 data stream's Measurement ID (for example `G-XXXXXXXXXX`). Found in Google Analytics under **Admin → Data Streams → your stream**. - **API Secret**: A Measurement Protocol API secret created for that data stream. Found under **Admin → Data Streams → your stream → Measurement Protocol API secrets**. ![Google Analytics Configuration](/assets/google-analytics-config.webp) ### Testing Before creating your connector, verify it's working by clicking **Test**. The Test Results tab shows whether Descope was able to reach Google's endpoint with a test event. ## Add Google Analytics connector to a flow Navigate to [Flows](https://app.descope.com/flows), choose your flow. Then, click the create button (designated via the `+` icon), select Connector, and choose the **Google Analytics / Track** option. Click to set down the connector in your flow editor and fill out the fields: - **Event Name**: The name of the event to record. Google Analytics only accepts letters, digits, and underscores, so the name is lowercased, any other character becomes an underscore, and the result is truncated to 40 characters. - **Client ID**: The Google Analytics client ID to attribute the event to. Leave empty on web, where Descope resolves it from the browser. Set it when the flow runs outside a browser, such as a native or TV app, using the client ID that app persists. - **User ID**: Your own ID for the user. Requires the User-ID feature to be enabled on the Google Analytics property to appear in reports. - **Session ID**: The Google Analytics session ID to attach the event to. Without it the event is still recorded, but it may not appear in session-scoped reports such as Realtime. - **Event Properties**: A dictionary of parameters to attach to the event. Avoid high-cardinality values such as IDs or timestamps, which make Google Analytics reports group rows under `(other)`. - **User Properties**: A dictionary of traits to record on the user, such as their plan or role. ![Google Analytics Action](/assets/ga4-action.webp) Now, link your connector in your authentication flow, making sure it's placed after any attributes you reference (like `form.email` or `user.userId`) are already defined. By adjusting the event name and where in the flow the connector sits, you can track anything from a completed sign-up to a specific authentication method being selected. ![Google Analytics Flow](/assets/ga4-flow.webp) ## Viewing Events Events sent through the connector appear in your GA4 property the same way client-side events do. Use [Realtime reports](https://support.google.com/analytics/answer/9271392) to confirm events are arriving as you test, and standard GA4 reports or Explorations for ongoing analysis. The events and logs can still be viewed in Descope on the [Audit and Troubleshoot page](https://app.descope.com/audits) of the Descope Console. # Google Cloud Logging (/connectors/connector-configuration-guides/analytics/google-cloud-logging) Descope's Google Cloud Logging connector allows you to send logs and stream audit events to your Google Cloud Logging account. # Google Cloud Logging Connector This guide covers implementing Descope's Google Cloud Logging connector. Google Cloud Logging is a troubleshooting product that allows you to collect log analytics. Descope enables you to automatically collect logs and audit events and stream them to a Google Cloud Logging account. ## Configure Google Cloud Logging connector ![Google Cloud Logging connector setup](/assets/google-cloud-logging-connector-setup.webp) Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Google Cloud Logging to create a new Google Cloud Logging connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Service Account Key**: A Service Account Key JSON file created from a service account on your Google Cloud project. This file is used to authenticate and authorize the connector to access Google Cloud Logging. The service account this key belongs to must have the appropriate permissions to write logs.(e.g. Logs Writer). - **Stream Audit Events**: Select which events are sent to Google Cloud Logging. Descopers can allow all audit events or filter them based on certain actions that occur or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Google Cloud Logging. ### Getting the Service Account Key Follow the steps in the [Google Cloud docs](https://cloud.google.com/iam/docs/keys-create-delete#creating) to create a Service Account Key. ## Viewing Audit Logs Once you've completed all of the steps above, your Descope Audit Logs should now available in Google Cloud Logging. You can test the connector during configuration to verify that logs are being sent and collected correctly. The logs can still be viewed in Descope on the [Audit and Troubleshoot page](https://app.descope.com/audits) of the Descope Console. # App Analytics Connectors (/connectors/connector-configuration-guides/analytics) App Analytics Connectors Overview # App Analytics Connectors App Analytics Connectors in Descope enable you to track analytics and user behavior during the authentication process. These connectors are added as steps within your [Flows](/flows) to track specific actions and events, such as user identification, sign-up events, authentication method selection, and other user interactions. ## How App Analytics Connectors Work App Analytics Connectors are added as steps within your Descope Flows to track analytics during the authentication journey. When a connector step is executed, it: 1. Captures user information and event data at that point in the flow 2. Sends tracking events to your connected analytics platform (e.g., Segment identify, Amplitude track) 3. Records user traits, properties, and event metadata for analysis You can place these connectors at strategic points in your flow to track: - User identification and profile updates - Sign-up and sign-in events - Authentication method selection - Specific actions within the authentication flow - User properties and custom event data This allows you to understand user behavior, conversion rates, and authentication patterns through your analytics platform. ## Available App Analytics Connectors Descope supports various analytics platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Mixpanel (/connectors/connector-configuration-guides/analytics/mixpanel) Descope's Mixpanel connector allows you to send logs and stream audit events to your Mixpanel account. # Mixpanel Connector This guide covers implementing Descope's Mixpanel connector. The Mixpanel Connector lets you automatically stream Descope [audit events](/audit-trails-and-integrations/audit-events) and troubleshooting logs into your Mixpanel project. Mixpanel is an advanced analytics platform that helps businesses understand how users interact with their digital products, like websites and mobile apps. ## Configure Mixpanel Connector Navigate to the Connectors page in the [Descope Console](https://app.descope.com/connectors/template/mixpanel) and configure the Mixpanel connector. ![Mixpanel connector setup](/assets/mixpanel-connector-setup.webp) The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: Briefly explain the purpose of this connector. - **Project Token**: The unique project token for your Mixpanel project. - **API Secret**: (Optional) The API secret for your Mixpanel project. - **Project ID**: (Optional) The project ID for your Mixpanel project. - **Service Account Username**: (Optional) The username of the service account to use for the connector. - **Service Account Secret**: (Optional) The secret of the service account to use for the connector. - **EU Residency**: (Optional) Indicating Mixpanel project data is stored in the EU region. - **Stream Audit Events**: Select which events are sent to Mixpanel. Descopers can allow all audit events or filter them based on certain actions that occur or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Mixpanel. - **Customize Log Field Prefix**: (Optional) Customize the prefix for log fields. Default is `descope.`. ### Getting the Project Token You can find the **Project Token** and **API secret** under **Access Keys** in the [Mixpanel project settings](https://mixpanel.com/project/settings). ![Mixpanel tokens](/assets/mixpanel-token.webp) ## Viewing Audit Events and Troubleshooting Logs Once you've completed all of the steps above, your Descope Audit Events and Troubleshooting Logs should now be available in Mixpanel. You can test the connector during configuration to verify that events are being sent and collected correctly. The events and logs can still be viewed in Descope on the [Audit and Troubleshoot page](https://app.descope.com/audits) of the Descope Console. # mParticle (/connectors/connector-configuration-guides/analytics/mparticle) Using Descope's connectors allows you to use mParticle to update events and user details in mParticle platform from web and mobile apps. # mParticle Connector This guide covers implementing Descope's mParticle connector. mParticle is a customer data platform (CDP) that helps companies collect, manage and activate customer data in real time. ## Configure mParticle connector Start by navigating to the Descope mParticle connector: [Connectors](https://app.descope.com/connectors) then select mParticle Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **API Key**: The mParticle Server to Server Key generated for the Descope service. - **API Secret**: The mParticle Server to Server Secret generated for the Descope service. - **Custom base URL**: The base URL of the mParticle API, when using a custom domain in mParticle. To grab the API key and Secret, create a Custom Feed under Setup -> Inputs -> Feeds. Read more on how to find it in [mParticle's documentation](https://web.archive.org/web/20260314232301/https://docs.mparticle.com/integrations/custom-feed/feed/). You can also configure the optional fields: - **Connector description**: Describe what your connector is used for. - **Environment**: The default environment of which connector send data to, either “production” or “development“. Default value: "production". This field can be overridden per event (see in flows). - **Use Static IPs**: If enabled, the connector uses a predetermined pool of IPs from which all requests will be made. These IPs are displayed in the UI for you to copy and allow-list on your API gateway, firewall, or server. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Descope example mParticle connector configuration](/assets/mparticle-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your mParticle connector to a flow Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose the mParticle option. Click to set down the connector in your flow editor and fill out the fields: - **Step name**: The unique name for the step. - **Connector**: Name of the connector which was used to setup the connector. - **Event Type**: mParticle event type. Can be `custom_event`, `session_start`,`session_end` etc. - **Environment**: Default value is `production`. Must be either `production` or `development`. - **mParticle ID**: The mParticle ID of the user that generated the event. If not provided, the event will not be associated to user. - **mParticle Device ID**: The mParticle Device ID of the device that geenerated the event. For any extra information that needs to be tracked and analyzed later, event properties can be added. Example: - **Key**: `event_name` - **Type**: `Dynamic` - **Value**: `project.name` ![Descope example mParticle connector events](/assets/mparticle-connector-events.webp) Now, link your connector in your authentication flow, making sure it's placed after any required attributes you just chose are defined. Simply by adjusting the attributes and location of the connector, you can track anything from a user sign up/in to the method used for authentication. ![Descope example mParticle connector within flow](/assets/mparticle-connector-placement.webp) That's it! Whenever a user goes through your authentication flow, the mParticle connector will be automatically triggered and the event will appear in your mParticle workspace. ![Descope example mParticle connector dashboard](/assets/mparticle-connector-dashboard.webp) The image above shows the Test Log along with event project name `Demo` as value which is in this scenario. # Open Telemetry (/connectors/connector-configuration-guides/analytics/open-telemetry) Descope's Open Telemetry connector allows you to send logs and stream audit events to your Open Telemetry Collector instance. # Open Telemetry Connector This guide explains how to implement Descope's Open Telemetry connector. [Open Telemetry](https://opentelemetry.io) is an observability framework for instrumenting, generating, collecting, and exporting telemetry data. Descope enables you to automatically collect authentication logs and audit events and forward them to your Open Telemetry Collector instance for centralized analysis and monitoring. ## Configure Open Telemetry Connector ![Open Telemetry connector setup](/assets/open-telemetry-connector-create-page.webp) Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select OpenTelemetry to create a new Open Telemetry connector. The following parameters are required: - **Connector Name**: Provide a unique name for your connector. This helps distinguish it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector (optional). - **Collector Endpoint**: The endpoint URL of your Open Telemetry Collector instance. - **Protocol**: The protocol to use when sending data to your Open Telemetry Collector (e.g., HTTP, gRPC). - **Authentication Type**: Choose the authentication method required by your Open Telemetry Collector (if any). - **Request Headers**: Specify any additional headers required for authentication or configuration when sending data to your Open Telemetry Collector. - **Trust Any Certificate**: Enable this option if your Open Telemetry Collector uses a self-signed certificate. - **Stream Audit Events**: Select which events are sent to Open Telemetry. You can allow all audit events or filter them based on specific actions or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Open Telemetry. ### Configuring Your Open Telemetry Collector In your OpenTelemetry setup, ensure that the [Collector](https://opentelemetry.io/docs/collector) is properly configured to receive logs and events from Descope. Here is an example of an OpenTelemetry Collector client using the OTLP exporter in a Node.js environment: ```typescript import express, { Express, Request, Response } from 'express'; const PORT: number = parseInt(process.env.PORT || '8080'); const app: Express = express(); // Add middleware for both JSON and protobuf app.use(express.json()); app.use(express.raw({ type: 'application/x-protobuf', limit: '10mb' })); function getRandomNumber(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min); } // OpenTelemetry Collector compatible endpoints app.post('/v1/traces', (req: Request, res: Response) => { try { console.log('Received traces data'); console.log('Content-Type:', req.headers['content-type']); console.log('Data length:', req.body.length); // For protobuf data, you'd need to decode it // This is a simplified response for now res.status(200).send(); } catch (error) { console.error('Error processing traces:', error); res.status(500).send(); } }); app.post('/v1/metrics', (req: Request, res: Response) => { try { console.log('Received metrics data'); console.log('Content-Type:', req.headers['content-type']); console.log('Data length:', req.body.length); res.status(200).send(); } catch (error) { console.error('Error processing metrics:', error); res.status(500).send(); } }); app.post('/v1/logs', (req: Request, res: Response) => { try { console.log('Received logs data'); console.log('Content-Type:', req.headers['content-type']); console.log('Data length:', req.body.length); res.status(200).send(); } catch (error) { console.error('Error processing logs:', error); res.status(500).send(); } }); // Keep your original webhook endpoint for JSON data app.post('/webhook/otel', (req: Request, res: Response) => { try { const webhookData = req.body; // Log the incoming webhook data console.log('Received webhook event:', JSON.stringify(webhookData, null, 2)); // Process OpenTelemetry compatible events if (webhookData.resourceSpans) { // Handle trace data console.log('Processing trace spans...'); webhookData.resourceSpans.forEach((resourceSpan: any) => { resourceSpan.scopeSpans?.forEach((scopeSpan: any) => { scopeSpan.spans?.forEach((span: any) => { console.log(`Span: ${span.name}, TraceID: ${span.traceId}, SpanID: ${span.spanId}`); }); }); }); } if (webhookData.resourceMetrics) { // Handle metrics data console.log('Processing metrics...'); webhookData.resourceMetrics.forEach((resourceMetric: any) => { resourceMetric.scopeMetrics?.forEach((scopeMetric: any) => { scopeMetric.metrics?.forEach((metric: any) => { console.log(`Metric: ${metric.name}, Type: ${metric.unit}`); }); }); }); } if (webhookData.resourceLogs) { // Handle log data console.log('Processing logs...'); webhookData.resourceLogs.forEach((resourceLog: any) => { resourceLog.scopeLogs?.forEach((scopeLog: any) => { scopeLog.logRecords?.forEach((logRecord: any) => { console.log(`Log: ${logRecord.body?.stringValue}, Severity: ${logRecord.severityText}`); }); }); }); } // Send success response res.status(200).json({ status: 'success', message: 'Webhook event processed', timestamp: new Date().toISOString() }); } catch (error) { console.error('Error processing webhook:', error); res.status(500).json({ status: 'error', message: 'Failed to process webhook event' }); } }); app.listen(PORT, () => { console.log(`Listening for requests on http://localhost:${PORT}`); }); ``` ```typescript 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(), metricReader: new PeriodicExportingMetricReader({ exporter: new ConsoleMetricExporter(), }), instrumentations: [getNodeAutoInstrumentations()], }); sdk.start(); ``` Optionally, specify the appropriate endpoint and authentication details in your Open Telemetry configuration file or interface. ### Viewing Audit Logs Audit logs are now viewable in Open Telemetry. You can test the connector during configuration to ensure logs are being sent and collected properly. This span represents the handling of an HTTP request to `/v1/logs` inside an Express.js application. It was captured by OpenTelemetry’s Express instrumentation, tied to a larger trace, executed on a Node.js process, and finished successfully in ~3 milliseconds: ``` { "resource": { "attributes": { "host.name": "example-host", "host.arch": "arm64", "host.id": "host-id-1234", "process.pid": 12345, "process.executable.name": "/usr/local/bin/node", "process.executable.path": "/usr/local/bin/node", "process.command_args": [ "/usr/local/bin/node", "--require", "/app/node_modules/tsx/dist/preflight.cjs", "--import", "file:///app/node_modules/tsx/dist/loader.mjs", "--import", "./instrumentation.ts", "/app/app.ts" ], "process.executable.path": "/usr/local/bin/node", "process.command_args": [ "/usr/local/bin/node", "--require", "/app/node_modules/tsx/dist/preflight.cjs", "--import", "file:///app/node_modules/tsx/dist/loader.mjs", "--import", "./instrumentation.ts", "/app/app.ts" ], "process.runtime.version": "24.x.x", "process.runtime.name": "nodejs", "process.runtime.description": "Node.js", "process.command": "/app/app.ts", "process.owner": "example-user", "service.name": "example-service", "telemetry.sdk.language": "nodejs", "telemetry.sdk.name": "opentelemetry", "telemetry.sdk.version": "2.x.x""instrumentationScope": { "name": "@opentelemetry/instrumentation-express", "version": "0.52.0", "schemaUrl": null }, } }, "instrumentationScope": { "name": "@opentelemetry/instrumentation-express", "version": "0.52.0", "schemaUrl": null }, "traceId": "11111111111111111111111111111111", "parentSpanContext": { "traceId": "11111111111111111111111111111111", "spanId": "2222222222222222", "traceFlags": 1, "traceState": null }, "traceState": null, "name": "request handler - /v1/logs", "id": "3333333333333333", "kind": 0, "timestamp": 1757334070604000, "duration": 3106.667, "attributes": { "http.route": "/v1/logs", "express.name": "/v1/logs", "express.type": "request_handler" }, "status": { "code": 0 }, "events": [], "links": [] } ``` The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. # Segment (/connectors/connector-configuration-guides/analytics/segment) Using Descope's connectors allows you to use Segment to collect events from web and mobile apps and better understand your customer's needs. # Segment Connector This guide covers implementing Descope's segment connector. Segment is an analytics product that allows you to collect events from web and mobile apps. Descope enables you to capture data from Segment to better understand your customer's needs. ## Configure Segment connector Descope uses your Segment Write Key to programmatically send event updates during your authentication flow. Start by navigating to the Descope Segment connector: [Connectors](https://app.descope.com/connectors) then select Segment Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Write Key**: The Segment Write Key generated for the Descope service. Read more on how to find it in [Segment's documentation](https://segment.com/docs/connections/find-writekey/). You can also configure the optional fields: - **Connector description**: Describe what your connector is used for. - **Custom base URL**: The base URL of the Segment API, when using a custom domain in Segment. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Descope example segment connector configuration](/assets/segment-connector-creation.webp) Save your configuration by hitting `Create.` ### Auto Audit Flow Steps Enable **Auto Audit Flow Steps** to have Descope send Segment events on its own, without placing the connector in a flow. When enabled, this connector is automatically triggered after every flow step of the types you select below — no need to add it to flows manually. - **Tracked step types**: Choose which kinds of flow steps should trigger an event — **Screens**, **Connectors**, **Errors**, **Actions**, and/or **Conditions**. - **Anonymous ID**: The ID associated with your user when you don't know who they are. - **Include Step Data**: When enabled, data about the flow step that triggered the event (such as its step ID or type) is automatically included as additional properties, on top of anything you configure below. - **Properties**: Key/value pairs sent as event properties, for example `email` → `{{form.email}}`, or `flowId` → `{{flowId}}`. Use `+`/`-` to add or remove rows. - **Traits**: A dictionary of traits you want to record on your user, like email or name. - **Context**: Key/value pairs sent as Segment context data (for example, information about the device, page, or app the event originated from). - **Integrations**: A dictionary of destination-specific options keyed by destination name (e.g. `Amplitude (Actions)`). Use it to pass destination-specific data such as the `Amplitude session ID` and `device ID`. #### Additional Step-Type Groups Each step type can only belong to one group at a time. Once a step type is assigned to a group, it no longer appears as an option when configuring another group. Since different step types often need different attributes, you can configure more than one Auto Audit Flow Steps group instead of a single set of settings that applies to every tracked step type. 1. Under **Additional step-type groups**, click **Add group config**. 2. In the new group's **Tracked step types** field, select one or more step types. 3. Configure the same fields for this group as described above (Anonymous ID, Include Step Data, Properties, Traits, Context, Integrations). 4. Click **Add group config** again to add further groups for the remaining step types. For example, you could keep **Screens** in the default group with one set of properties, and add a second group covering **Connectors**, **Errors**, and **Actions** together with a different set of properties. ## Add your Segment connector to a flow This is a manual, per-flow alternative to [Auto Audit Flow Steps](#auto-audit-flow-steps). Use this if you only want to send an event from a specific point in a specific flow, rather than automatically after every step of a given type. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose the Segment tracking option you'd like. Click to set down the connector in your flow editor and fill out the fields. ### Identify and Track - **User ID**: The unique ID of the user - **Anonymous ID**: The ID associated with your user when you don't know who they are - **Traits**: A dictionary of traits you want to record on your user, like email or name. - **Integrations**: A dictionary of destination-specific options keyed by destination name (e.g. `Amplitude (Actions)`). Use it to pass destination-specific data such as the `Amplitude session ID` and `device ID`. ### Page Use the **Page** action to record page views, so you can see what pages your users are visiting. - **User ID**: The unique ID of the user - **Anonymous ID**: The ID associated with your user when you don't know who they are - **Category**: The category of the page, for example `Retail` or `Blog`. - **Name**: The name of the page, for example `Home` or `Checkout`. - **Properties**: A dictionary of properties of the page view, such as `url`, `path`, `title`, or `referrer`. - **Context**: A dictionary of extra information about the tracked datapoint, such as the user's IP address or locale. Provide a User ID or an Anonymous ID so Segment can associate the page view with a user. Start typing in the empty field to view attribute options. For the User ID, you could use the `{{user.userId}}` field. Now, link your connector in your authentication flow, making sure it's placed after any required attributes you just chose are defined. Simply by adjusting the attributes and location of the connector, you can track anything from a user sign up/in to the method used for authentication. ![Descope example segment connector within flow](/assets/segment-connector-placement2.webp) That's it! Whenever a user goes through your authentication flow, the Segment connector will be automatically triggered and the event will appear in your Segment workspace. ## Handle errors The Segment connector allows for custom handling of errors. After adding the connector (identify, track, or page) to a flow, you can scroll down to the 'Error Handling' section. There, you can change the error handling from 'Automatic' to 'Custom', then link to a custom error handling flow. ![Descope example segment connector error handling configuration within flow](/assets/segment-connector-error-handling.webp) ![Descope example segment connector error handling connection within flow](/assets/segment-connector-error-handling-connection.webp) # AWS EventBridge (/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-eventbridge) Descope's AWS EventBridge connector allows you to send logs and stream audit events to your AWS EventBridge event bus. # AWS EventBridge Connector Amazon EventBridge is a serverless event bus with routing rules to targets like AWS Lambda, Amazon SQS, or Amazon Kinesis Data Firehose. Descope's AWS EventBridge connector allows you to publish audit events and troubleshooting logs directly to an event bus in your AWS account, so you can route them into your existing event-driven pipelines. ## Setting Up the AWS EventBridge Connector To integrate the connector, follow the steps below: ### 1. Navigate to Connector - Visit the [Connectors page](https://app.descope.com/connectors) in the Descope Console. - Select **AWS EventBridge** from the list of connectors. ### 2. Connector Setup Enter the following information to configure the connector: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description (Optional)**: Describe what your connector is used for. - **Authentication**: Choose between the following authentication options: #### Use AWS Credentials Use your AWS credentials to authenticate. You will need to provide: - **Access Key ID**: The AWS access key ID. - **Secret Access Key**: The AWS secret access key. The IAM user associated with these credentials must have the `events:PutEvents` permission on your event bus. You can attach a policy like this: ```json title="Policy editor" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["events:PutEvents"], "Resource": "arn:aws:events:us-east-1:123456789012:event-bus/your-event-bus-name" } ] } ``` #### Use Role Based Permissions Use role-based permissions to authenticate instead of long-lived credentials. You will need to provide: - **Role ARN**: The Amazon Resource Name (ARN) of the role that has permissions to call `events:PutEvents` on your event bus. - **External ID**: The external ID used to assume the role. When creating the IAM role for this connector, you must include a trust policy that allows Descope to assume the role: **Trust Policy Requirements:** - **Principal**: `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` - **External ID**: The external ID provided by Descope during connector configuration - **Action**: `sts:AssumeRole` Your trust policy should look like this: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::312892722078:role/prod-external-role-us-east-1" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "your-external-id-from-descope" } } } ] } ``` Without this trust policy configuration, you'll receive an `is not authorized to perform: sts:AssumeRole` error when testing the connector. - **Region**: The AWS region your event bus is located in, e.g. `us-east-1`. - **Event Bus**: The name or ARN of the event bus that logs and audit events will be sent to. This can be your account's default event bus or a custom event bus. - **Stream Audit Events**: Select this if you want to stream audit events to your AWS EventBridge event bus. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to AWS EventBridge. - **Mask PII data**: Decide whether to mask PII (personally identifiable information) in the logs. ![AWS EventBridge connector setup](/assets/aws-eventbridge-config.webp) ### 3. Test & Save - Use the **Test** option to verify that events can be published to your event bus. - Review the results in the `Test Results` panel. - Once successful, click `Create` to save the connector. ## Viewing Audit Logs Once configured, audit and troubleshooting logs are published to your AWS EventBridge event bus as events. Each event includes a `Source` and `Detail-Type` identifying it as originating from Descope, along with a `Detail` payload containing the event data as JSON. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. For more information on audit trail and log streaming see [Audit Trail Streaming](/audit-trails-and-integrations/audit-trail-streaming). From your event bus, you can attach [rules and targets](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rules.html) to route Descope events to destinations such as AWS Lambda, Amazon SQS, or Amazon Kinesis Data Firehose for further processing and storage. # AWS S3 (/connectors/connector-configuration-guides/audit-and-troubleshooting/aws-s3) Descope's AWS S3 connector allows you to send logs and stream audit events to your AWS S3 account. # AWS S3 Connector This guide covers implementing Descope's AWS S3 connector. Descope enables you to automatically collect troubleshooting logs and audit events in your AWS S3 Bucket. ## Configure AWS S3 Connector in Descope ### Configuring the Connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select AWS S3 to create a new AWS S3 connector. ![AWS S3 connector setup](/assets/s3-config.webp) The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Authentication**: Choose whether to use AWS credentials or role-based permissions to authenticate. Follow the guidance below to configure each method. - **Region**: The AWS S3 region, e.g. `us-east-1` - **Bucket**: The name of the AWS S3 bucket that the logs and audit events will be sent to. - **Stream Audit Events**: Select this if you want to stream audit events to your AWS S3 bucket. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to AWS S3. - **Mask PII data**: Decide whether to mask PII (personally identifiable information) in the logs. ## Authentication ### Use AWS Credentials #### Prerequisites 1. Have an AWS S3 bucket set up. 2. Have an IAM user with the necessary AWS S3 bucket permissions. #### Getting the Access Key 1. In AWS, navigate to Services in the top left and select IAM. On the IAM page, navigate to Users. If you don't have an IAM user, create one now. If you already have one, click on "Add Permissions". You can assign the required permissions either by adding the user to a group or by directly attaching policies to the user. For more information see [Amazon Documentation](https://docs.aws.amazon.com/console/iam/access-type). ![Adding Permissions to AWS IAM User](/assets/aws-s3-permissions.webp) 2. Go to the User's page and click on "Create access key" and then on "Third-party service" if you are adding the policy directly. ![Create Access Key for AWS IAM User](/assets/aws-s3-user-page.webp) 3. Make sure to save your Secret access key as you won't be able to view it again. ![Access Key Fields in AWS S3](/assets/aws-s3-access-key.webp) 4. Insert the `Access Key` ID and secret to the Descope console. ### Use Role-Based Permissions #### Prerequisites 1. Have an AWS S3 bucket set up. ### Creating the Role 1. Insert the `region` and the `bucket` name. 2. Doing so will create a `Cloud Formation Stack` link: ![AWS S3 create cloud formation link](/assets/s3-connector-cloudformation-link.webp) 3. Following the link will prompt creating a stack - completing the creation will configure the role for you. ![AWS S3 create cloud formation stack](/assets/cf-create-stack-s3.webp) When creating a custom IAM role for the AWS S3 connector, you must include the following trust policy to allow Descope to assume the role: **Trust Policy Requirements:** - **Principal**: `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` - **External ID**: The external ID provided by Descope during connector configuration - **Action**: `sts:AssumeRole` Your trust policy should look like this: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::312892722078:role/prod-external-role-us-east-1" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "your-external-id-from-descope" } } } ] } ``` Without this trust policy configuration, you'll receive an `is not authorized to perform: sts:AssumeRole` error when testing the connector. 4. Insert the role ARN created, e.g. `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` into Descope's console. ## Viewing Audit Logs Now audit logs will be sent to the AWS S3 bucket as JSON objects. The connector can be tested while configuring it so you can ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. For more information on audit trail and log streaming see [Audit Trail Streaming](/audit-trails-and-integrations/audit-trail-streaming). ![An example of directory structure when streaming Descope audit logs to Amazon S3](/assets/example-amazon-s3-directory-structure.webp) ![An example of the date formatted directory structure when streaming Descope audit logs to Amazon S3](/assets/example-amazon-s3-directory-structure-2.webp) # Coralogix (/connectors/connector-configuration-guides/audit-and-troubleshooting/coralogix) Descope's Coralogix connector allows you to send logs and stream audit events to your Coralogix account. # Coralogix Connector This guide covers implementing Descope's Coralogix connector. Coralogix is a cloud-native observability platform that provides real-time insights into logs, metrics, and traces. Descope enables you to automatically collect authentication logs and audit events and stream them to your Coralogix account for centralized monitoring and analysis. ## Configure Coralogix connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Coralogix to create a new Coralogix connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: (Optional) Briefly explain the purpose of this connector. - **Ingress Endpoint**: The ingress endpoint URL. - **Send-Your-Data API Key**: In the Coralogix console, navigate to **Settings -> API Keys -> Send-Your-Data API Keys** in the left sidebar and create a new API key. - **Stream Audit Events**: Select which events are sent to Coralogix. Descopers can allow all audit events or filter them based on certain actions that occur or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Coralogix. - **Mask PII**: Decide whether to mask PII (personally identifiable information) in the logs. ![Coralogix connector setup](/assets/coralogix-connector-setup.webp) ## Viewing Audit Logs Once configured, audit logs are automatically streamed to Coralogix. The connector can be tested while configuring it to ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. In Coralogix, navigate to **Explore** > **Logs** to view your Descope audit events. You can filter logs by the Application Name and Subsystem Name you configured. Each log entry will contain detailed information about events, including user actions, timestamps, IP addresses, and other relevant data. You can click on any log entry to view its full details and use Coralogix's powerful search and analysis tools to gain insights into your events. ![Viewing logs in Coralogix](/assets/coralogix-logs.webp) # Cribl (/connectors/connector-configuration-guides/audit-and-troubleshooting/cribl) Descope's Cribl connector allows you to send logs and stream audit events to your Cribl Stream instance. # Cribl Connector Descope enables you to automatically collect authentication logs and audit events and forward them to your Cribl Stream instance for centralized analysis, routing, and processing. ## Configure the Cribl Connector in Descope ### Configuring the Connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Cribl to create a new Cribl connector. ![Cribl connector setup](/assets/cribl-config.webp) The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector (optional). - **Endpoint URL**: The base URL of your Cribl Stream instance. - **Authentication Token**: A shared secret token for authenticating with your Cribl HTTP Source. - **Source (Optional)**: A source identifier attached to all the events in Cribl (default is `descope`). - **Stream Audit Events**: Select this if you want to stream audit events to your Cribl Stream instance. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Cribl. - **Mask PII data**: Select whether to mask PII data in the logs. ### Testing the Connector Before creating your connector, its important to verify if the connector configurations works. For this simply click on **Test** and view the Test Results panel. Confirm it works and click **Create**. ## Viewing Audit Logs Once configured, audit logs from Descope will flow into your Cribl Stream instance. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. This should match the events in Cribl Stream under the **Search** section. ![Viewing logs in Cribl Stream](/assets/cribl-audit-logs.webp) Clicking on one of the events, you see details of all fields being prefixed with `descope.`. You can remove or change the prefix via the connector configuration. ![Viewing logs in Cribl Stream](/assets/cribl-audit.webp) # Datadog (/connectors/connector-configuration-guides/audit-and-troubleshooting/datadog) Descope's Datadog connector allows you to send logs and stream audit events to your Datadog account. # Datadog Connector This guide covers implementing Descope's Datadog connector. Datadog is a troubleshooting product that allows you to collect log analytics. Descope enables you to automatically collect logs and audit events and stream them to a Datadog site. ## Configure Datadog connector ![Datadog connector setup](/assets/datadog-config.webp) Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Datadog to create a new Datadog connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **API Key**: The unique Datadog organization. Found in your [Datadog](https://app.datadoghq.com/dashboard/lists) account. - **Site**: The Datadog site that the connector sends logs to. The default is `datadoghq.com`. Needs to be set for European, free tier, and some other customers. - **Source** (optional): Overrides the default log source (`descope`) for entries sent to Datadog. Use it to identify which environment a log came from, such as `production` or `staging`. - **Tags** (optional): Comma-separated custom tags appended to every log entry, such as `env:production,team:auth`, on top of Descope's default `projectid` and `connectorid` tags. - **Stream Audit Events**: Select this if you want to stream audit events to your Datadog account. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Datadog. - **Mask PII**: Decide whether to mask PII (personally identifiable information) in the logs. ### Getting the API Key In Datadog, navigate to Settings and select Organization Settings in the top left. From this page, navigate to API Keys using the sidebar. Create a new API Key or use an existing one. Make sure to copy the API key after it is created to include in the connector configuration. ![Creating Datadog API key](/assets/datadog-connector-apikey.webp) ## Viewing Audit Logs Now the audit logs are viewable in Datadog. The connector can be tested while configuring it so you can ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. ![Viewing logs in Datadog](/assets/datadog-connector-log.webp) # Groundcover (/connectors/connector-configuration-guides/audit-and-troubleshooting/groundcover) Descope's groundcover connector allows you to send logs and stream audit events to your groundcover account. # Groundcover Connector This guide covers implementing Descope's Groundcover connector. Groundcover is an observability product that allows you to collect log analytics. Descope enables you to automatically collect logs and audit events and stream them to Groundcover. ## Configure Groundcover connector ![Groundcover connector setup](/assets/groundcover-config.webp) Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Groundcover to create a new Groundcover connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Ingress Endpoint**: The gRPC OTLP backend endpoint URL for your groundcover deployment. Found in the groundcover console under **Settings → Access → Ingestion Keys → Backend Endpoints**. - **Ingestion Key**: A third party ingestion key that authenticates external data sources sending telemetry to groundcover. Create one in the groundcover console under **Settings → Access → Ingestion Keys**. Sent as the `apikey` header on each request. - **Environment Name** (optional): A name to appear under `x-groundcover-env-name` header. - **Service Name** (optional): A name to appear as send service. Default is `descope`. - **Stream Audit Events**: Select this if you want to stream audit events to your Groundcover account. You can choose to stream all audit events or apply a specific filter. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Groundcover. - **Mask PII**: Decide whether to mask PII (personally identifiable information) in the logs. ### Getting the Ingress Endpoint and Ingestion Key In Groundcover, navigate to **Settings** and select **Access** in the sidebar. From this page, navigate to **Ingestion Keys**. You can find the Ingress Endpoint listed as **Public Endpoint** at the top of the page. For the **Ingestion Key**, create a new Ingestion Key or use an existing one. The key must be of type `3rd Party`, so if you create a new key, select `3rd Party` as the key type. ![Creating Groundcover ingestion key](/assets/groundcover-cnnctr-ingestionkey.webp) ## Viewing Audit Logs Now the audit logs are viewable in Groundcover in the **Logs** section of your console. The connector can be tested while configuring it so you can ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. ![Viewing logs in Groundcover](/assets/groundcover-connector-log.webp) # Audit & Troubleshooting (/connectors/connector-configuration-guides/audit-and-troubleshooting) Audit & Troubleshooting Connectors Overview # Audit & Troubleshooting Connectors Audit & Troubleshooting Connectors in Descope automatically stream authentication logs, audit events, and troubleshooting data to third-party monitoring and log management services. Unlike regular connectors that are used as steps within flows, Audit & Troubleshooting Connectors work in the background and don't need to be added to your flow configurations. ## How Audit & Troubleshooting Connectors Work Once configured, Audit & Troubleshooting Connectors automatically stream data from Descope to your connected third-party service. For more information on audit trail streaming, see [Audit Trail Streaming](/audit-trails-and-integrations/audit-trail-streaming). ## Available Audit & Troubleshooting Connectors Descope supports various audit and troubleshooting platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Logz.io (/connectors/connector-configuration-guides/audit-and-troubleshooting/logzio) Descope's Logz.io connector allows you to send logs and stream audit events to your Logz.io account. # Logz.io Connector This guide covers implementing the Logz.io connector. Logz.io is a cloud-based observability platform built on open-source tools like Elasticsearch, Kibana, and Grafana that provides log management, metrics, and distributed tracing. Descope can automatically collect authentication logs and audit events and stream them to your Logz.io account for centralized monitoring and analysis. ## Configure Logz.io connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Logz.io to create a new Logz.io connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: (Optional) Briefly explain the purpose of this connector. - **Shipping Token**: In Logz.io, navigate to **Settings** and select **Manage Tokens** from the sidebar, then select the **Data Shipping Tokens** tab. Create a new token or use an existing one, then copy it to include in the connector configuration. - **Listener URL**: The region-specific listener endpoint for your Logz.io account (for example, `listener.logz.io`, `listener-uk.logz.io`, `listener-au.logz.io`, `listener-ca.logz.io`, or `listener-nl.logz.io`). This must match the region your Logz.io account was created in. - **Shipping Method**: Select the shipping method for your logs. - **HTTP Bulk**: Use this method to send logs to Logz.io using the HTTP protocol. - **OpenTelemetry (OTLP)**: Use this method to send logs to Logz.io using the OpenTelemetry protocol. - **Log Type**: The Logz.io type used for parsing. Applies to HTTP bulk shipping only and cannot contain spaces. - **Stream Audit Events**: Select which events are sent to Logz.io. Descopers can allow all audit events or filter them based on certain actions that occur or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Logz.io. ![Logz.io connector configuration](/assets/logzio-settings.webp) ### Testing Before creating your connector, its important to verify if the connector configurations works. For this simply click on "Test" and view the Test Results panel. Confirm it works and click Create. ## Viewing Audit Logs Once configured, audit logs are automatically streamed to Logz.io. The connector can be tested while configuring it to ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. In Logz.io, navigate to **Logs** to view your Descope audit events. Each log entry will contain detailed information about events, including user actions, timestamps, IP addresses, and other relevant data. You can click on any log entry to view its full details and use Logz.io's search and analysis tools to gain insights into your events. ![Logz.io audit logs](/assets/logzio-audit-logs.webp) # New Relic (/connectors/connector-configuration-guides/audit-and-troubleshooting/newrelic) Learn how to send audit events with New Relic Connector within your applications. # New Relic Connector Descope enables you to stream audit logs to New Relic using their log's API. You can use the New Relic connector to send in your audit events and troubleshooting logs from Descope. ## Configure New Relic Connector in Descope ### Prerequisites 1. Have a New Relic Account created. 2. Make a note of your API key required to set up Connector on Descope side. (This key is available on your new relic platform under your profile labeled as "API Keys". Copy and paste "INGEST - LICENSE" type key value.) ### Configuring the connector Navigate to Descope's connector menu and search for New Relic Connector. Fill in the following parameters required for the connection: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of using this connector. - **API Key**: Ingest license Key / API Key of the New Relic account you want to report data to. - **Data Center**: (Optional) : The New Relic data center the account belongs to. Possible values are: US, EU, FedRAMP. Default is US. - **Stream Audit Events**: Select this if you want to stream audit events to your New Relic account. - **Stream Troubleshooting Logs**: Decide if you want to send troubleshooting events. ![Create New Relic Connector ](/assets/newrelic-config.webp) ### Testing Before creating your connector, its important to verify if the connector configurations works. For this simply click on "Test" and view the Test Results panel. Confirm it works and click Create. ## View the Audit Logs Once you have configured the connector successfully, your events will be sent to the API you specified. You can view the logs on the Descope side in the Audit tab under Audit and Troubleshooting. This should match the events in New Relic Connector under Logs -> All logs. ![New Relic Logs ](/assets/newrelic-logs.webp) Clicking on one of the events, you see details of all fields being prefixed with `descope.`. You can remove or change the prefix via the connector configuration. ![New Relic Logs Events ](/assets/newrelic-logs-events.webp) If you need to view the logs based on certain attributes like `project-id`, `connectorId` etc, click on the "Attributes" option under "All logs" and choose your desired filter to view respective logs. ![New Relic Attributes ](/assets/newrelic-attributes.webp) And that's it! You have now successfully configured New Relic Connector within Descope. # Pendo (/connectors/connector-configuration-guides/audit-and-troubleshooting/pendo) Descope's Pendo connector allows you to send logs and stream audit events to your Pendo account. # Pendo Connector This guide covers implementing the Pendo connector. Pendo is a product experience platform that helps teams understand user behavior through in-app analytics, guides, and feedback. Descope can automatically collect authentication logs and audit events and stream them to your Pendo account as track events, so you can analyze admin and user activity alongside your other product analytics. ## Configure Pendo connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Pendo to create a new Pendo connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: (Optional) Briefly explain the purpose of this connector. - **Base URL**: The Pendo regional domain to send data to. Default is the US region, `https://data.pendo.io`. Customers in other regions must set this accordingly. - **Integration Key**: Your Pendo Integration Key, used to authenticate requests to the Pendo API. You can find it in the Pendo Console under **Settings** > **Integrations** > **Integration Keys**. - **Stream Audit Events**: Select which events are sent to Pendo. Descopers can allow all audit events or filter them based on certain actions that occur or tenants in the project. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Pendo. - **Mask PII data**: Decide whether to mask PII (personally identifiable information) in the logs. ![Pendo connector configuration](/assets/pendo-config.webp) ### Testing Before creating your connector, it's important to verify that the connector configuration works. For this, simply click on "Test" and view the Test Results panel. Confirm it works and click Create. ## Viewing Audit Logs Once configured, audit logs are automatically streamed to Pendo as track events. The connector can be tested while configuring it to ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. In Pendo, navigate to your app's event or track event data to view your Descope audit events. Each event contains detailed information, including user actions, timestamps, and other relevant data, which you can use with Pendo's reporting and segmentation tools to gain insights into your events. ## Tracking Custom Events in Flows In addition to streaming audit events in the background, the Pendo connector can be added as a step within your flows to track a custom event for a given visitor. Navigate to [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose the Pendo option, then select the **Pendo / Track** action. Configure the following fields: - **Step name**: The unique name for the step. - **Connector**: Name of the connector which was used to set up the connector. - **Event**: The name of the custom event to track in Pendo. - **Visitor ID**: The ID of the Pendo visitor to associate the event with. - **Account ID** (Optional): The ID of the Pendo account associated with the visitor. - **Properties** (Optional): A dictionary of custom properties to attach to the event. Use **+ Add row** to add each property. - **Context** (Optional): A dictionary of contextual information, such as the user's IP address or URL. Use **+ Add row** to add each entry. ![Pendo Track action](/assets/pendo-track-action.webp) You can also enable **Run asynchronously** so the flow doesn't wait for a response from Pendo before moving on to the next step. Once added to your flow, the Track action fires whenever that step is reached, sending the custom event to Pendo for the specified visitor. # Snowflake (/connectors/connector-configuration-guides/audit-and-troubleshooting/snowflake) Descope's Snowflake connector allows you to send logs and stream audit events to your Snowflake data warehouse. # Snowflake Connector This guide covers implementing Descope's Snowflake connector. Snowflake is a cloud-based data warehouse platform for storage, processing, and analytics. Descope enables you to automatically collect authentication logs and audit events and stream them to your Snowflake data warehouse for centralized analysis and long-term storage. ## Configure Snowflake connector ### Prerequisites Before configuring the Descope Snowflake connector, ensure you have: 1. A Snowflake account with appropriate permissions 2. A warehouse, database, and schema created in Snowflake 3. A user account with write permissions for the target database and table ### Configuring the Connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Snowflake to create a new Snowflake connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description (optional)**: Briefly explain the purpose of this connector. - **Programmatic Access Token**: A Snowflake PAT used to authenticate API requests. - **Account URL**: The URL of your Snowflake account. e.g. `https://-.snowflakecomputing.com`. Found in Snowsight under **Admin > Accounts**. - **Warehouse**: The Snowflake warehouse to use for query execution. - **Database**: The Snowflake database where audit events will be stored. - **Schema**: The schema within the database for organizing audit event tables. - **Audit Table**: The table to write audit events to. - **Stream Audit Events**: Select this if you want to stream audit events to your Snowflake account. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Snowflake. - **Mask PII Data**: Decide whether to mask PII (personally identifiable information) in the logs. ![Snowflake connector setup](/assets/snowflake-config.webp) ### Testing Before creating your connector, verify that the connector's configuration works. Once configured, simply click on "Test" and check the Test Results panel. Confirm, then click "Create." ## Configuring the Database, Schema, and Table Audit events are stored in a single table with **two columns**: | Column | Type | Purpose | | ------------ | --------------- | ----------------------------------------------------------------------- | | **DATA** | `VARIANT` | Full event payload as JSON. | | **CREATED_AT** | `TIMESTAMP_LTZ` | Insertion time (set automatically when rows are written). | Default fully qualified name: **`DESCOPE_EXPORT_DB.PUBLIC.DESCOPE_AUDIT_LOGS`**. If you create the objects yourself in Snowflake, example DDL: ```sql CREATE DATABASE IF NOT EXISTS DESCOPE_EXPORT_DB; CREATE SCHEMA IF NOT EXISTS DESCOPE_EXPORT_DB.PUBLIC; CREATE TABLE IF NOT EXISTS DESCOPE_EXPORT_DB.PUBLIC.DESCOPE_AUDIT_LOGS ( DATA VARIANT, CREATED_AT TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP() ); ``` Match the **database**, **schema**, and **table** names to what you enter in the connector. If you use non-default names, update the connector fields accordingly. The table must include **`DATA`** (`VARIANT`) and **`CREATED_AT`** (`TIMESTAMP_LTZ`) so Descope can write events as expected. Grant the PAT user (via its role) permission to insert into the table and use the warehouse, for example: ```sql -- Replace MY_ROLE and COMPUTE_WH with your role and warehouse names GRANT USAGE ON DATABASE DESCOPE_EXPORT_DB TO ROLE MY_ROLE; GRANT USAGE ON SCHEMA DESCOPE_EXPORT_DB.PUBLIC TO ROLE MY_ROLE; GRANT INSERT ON TABLE DESCOPE_EXPORT_DB.PUBLIC.DESCOPE_AUDIT_LOGS TO ROLE MY_ROLE; GRANT USAGE ON WAREHOUSE COMPUTE_WH TO ROLE MY_ROLE; ``` Adjust grants if your Snowflake admin uses different roles or if the connector uses a warehouse other than `COMPUTE_WH`. ## Viewing Audit Logs Now the audit logs are viewable in Snowflake. Query recent rows (default table name `DESCOPE_AUDIT_LOGS`): ```sql SELECT DATA, CREATED_AT FROM DESCOPE_EXPORT_DB.PUBLIC.DESCOPE_AUDIT_LOGS ORDER BY CREATED_AT DESC LIMIT 100; ``` Exact keys inside `DATA` depend on the event type; inspect sample rows or your audit event schema in Descope. ![Viewing audit logs in Snowflake](/assets/snowflake-connector-log.webp) The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. For more information on audit trail streaming, see [Audit Trail Streaming](/audit-trails-and-integrations/audit-trail-streaming). # Splunk (/connectors/connector-configuration-guides/audit-and-troubleshooting/splunk) Descope's Splunk connector allows you to send logs and stream audit events to your Splunk instance. # Splunk Connector This guide covers implementing Descope's Splunk connector. Splunk is a data platform for searching, monitoring, and analyzing machine-generated logs in real time. Descope enables you to automatically collect authentication logs and audit events and forward them to your Splunk instance for centralized analysis and security monitoring. ## Configure Splunk connector ![Splunk connector setup](/assets/splunk-config.webp) Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Splunk to create a new Splunk connector. The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector (optional). - **HTTP Event Collector Token**: An HTTP Event Collector token configured on your Splunk instance. - **HTTP Event Collector URL**: The URL to be used accessing your Splunk instance, including the appropriate port. - **Index**: An index to use for all events sent to Splunk (optional) - **Stream Audit Events**: Select this if you want to stream audit events to your Splunk instance. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also sent to Splunk. ### Creating Your HTTP Event Collector (HEC) in Splunk In Splunk, navigate to Settings and select Data Inputs under DATA. ![Splunk settings window](/assets/splunk-settings-data-input.webp) Click **+ Add new** next to “HTTP Event Collector”. Add a name for your HEC and then click the green **Next >** button at the top of the screen. ![Splunk create HEC and add name](/assets/splunk-create-hec.webp) Under **Source type**, navigate to **Select** and find **_json** in the dropdown menu. Under **Index**, select **main**, or another specific index that you desire to use. Then click the **Review >** button at the top of the screen. ![Splunk create HEC and set input settings](/assets/splunk-hec-input-settings.webp) After confirming all of the configurations are correct, click the green **Submit >** button at the top of the screen. ### Obtaining Your HEC Token After you have successfully created your HTTP Event Collector, you will be brought to a screen which will display the token associated with your HEC. Copy this token and paste it into the **HTTP Event Collector Token** field in your Descope Splunk Connector. You can also get the token value of any of your existing HEC's at any time by navigating to Settings > Data inputs > HTTP Event Collector. ![Splunk display HEC Token](/assets/splunk-hec-get-token.webp) ### Configuring Your HEC URL 1. For Splunk Enterprise accounts, use the following URL scheme: ``` ://: ``` - **Protocol**: `https` if SSL is enabled on your HEC Global Settings, `http` otherwise. - **Host**: the Splunk instance that runs the HEC. - **Port**: 8088 by default, unless you change it in the HEC Global Settings. 2. For Splunk Cloud Platform accounts, use the following URL schemes: For Splunk Cloud Platform free trials: ``` ://http-inputs-.splunkcloud.com: ``` For Splunk Cloud Platform on AWS: ``` ://http-inputs-.splunkcloud.com: ``` For Splunk Cloud Platform on Google Cloud or Azure: ``` ://http-inputs..splunkcloud.com: ``` For Splunk Cloud Fedramp Moderate on AWS Govcloud: ``` ://http-inputs..splunkcloudgc.com: ``` - **Protocol**: either `https` or `http`. - **Host**: the Splunk instance that runs the HEC. - **Port**: 8088 on Splunk Cloud Platform free trials, 443 by default on Splunk Cloud Platform instances ## Viewing Audit Logs Now the audit logs are viewable in Splunk. The connector can be tested while configuring it so you can ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. ![Viewing logs in Splunk](/assets/splunk-audit-result.webp) # Sumo Logic (/connectors/connector-configuration-guides/audit-and-troubleshooting/sumologic) The Sumo Logic connector allows you to send logs and stream audit events from Descope to your Sumo Logic account. # Sumo Logic Connector This guide covers implementing Descope's Sumo Logic connector. Sumo Logic is a troubleshooting product that allows you to collect log analytics. Descope enables you to automatically collect logs and audit events and stream them to a Sumo Logic collector. ## Configure Sumo Logic connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select Sumo Logic to create a new Sumo Logic connector. ![Sumo Logic Connector setup](/assets/sumologic-config.webp) The following parameters are required to use it: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **HTTP Source URL**: The source URL for a Sumo Logic hosted collector. Needs to be configured in [Sumo Logic](https://help.sumologic.com/docs/send-data/hosted-collectors/http-source/logs-metrics/). - **Stream Audit Events**: Select this if you want to stream audit events to your Sumo Logic collector. - **Stream Troubleshooting Events**: Decide whether troubleshooting events are also collected by Sumo Logic. ### Getting the HTTP Source URL In Sumo Logic, navigate to Manage Data then Collection. From the Collection page, either create a new collection or use an existing one. The collection must have a source created. After the source is created, a URL will be displayed that can be copied into the connector configuration. ![Getting HTTP Source URL](/assets/sumologic-connector-url.webp) ## Viewing Audit Logs Now the audit logs are viewable in Sumo Logic. The connector can be tested while configuring it so you can ensure the logs are being sent and collected properly. The logs can still be viewed in Descope under the [Audit and Troubleshoot](https://app.descope.com/audits) section of the Descope Console. ![Viewing logs in sumo logic](/assets/sumologic-connector-log.webp) # AWS Rekognition (/connectors/connector-configuration-guides/kyc/aws-rekognition) Use Descope's AWS Rekognition Connector to achieve facial recognition in your authentication flow # AWS Rekognition Connector AWS Rekognition is a cloud-based AI service that offers computer vision capabilities for analyzing and processing images, and can be used to detect faces and ID cards, store them in a collection, and compare them to other faces. This guide shows how to use Descope's AWS Rekognition Connector to achieve facial recognition in your authentication flow. ## Items to Note - This connector is not suited to work when authenticating with the following authentication methods: 1. [**SAML**](/auth-methods/sso/with-sdks/backend) 2. [**Microsoft Azure OAuth**](https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/auth-oauth2) - AWS Rekogition does not check ID card authenticity ## How to Configure You can begin the configuration with two simple steps listed below. 1. Generate Access Key ID and Secret Access Key from [AWS IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html). The user associated with the keys must have the the [**Amazon Rekognition Full Access**](https://docs.aws.amazon.com/rekognition/latest/dg/security-iam-awsmanpol.html) policy attached. 2. Configure the connector with the required parameters, and save your configuration by clicking `Create`: ![Setup connector](/assets/aws-rekognition-connector-setup.webp) ## How to Register a User You can register a user to a collection using their ID (identity) document. This is equivalent to signing the user up to the service. Registration requires an identification document image and an external ID (automatically populated using the user's identifier). To register a user, follow these steps: 1. In your **Sign Up** [flow](https://app.descope.com/flows), add an **_Upload Document_** component to a screen: ![Upload document](/assets/upload-document.webp) 2. Then, add the **_AWS Rekognition / Register_** action block to the flow, and link it to the screen created in the previous step. It's a good idea to use a meaningful context key value (such as `register`, in the example below). ![Register step](/assets/register-step.webp) 3. Finally, add a new conditional block to the flow, and link it to the **_AWS Rekognition / Register_** action block. In the conditional block, you can use the information received from the previous step, which includes: - `documentConfidence`: **Scale of 0-100** - the confidence level when checking if it is a valid ID (the higher the score - the higher the confidence). This is used to verify that the user is legitimate and can proceed to complete the sign up process. - `existingSearchConfidence`: **Scale of 0-100** - the confidence level when checking if its ID already exists in the collection. This is used to identify if a user that already exists in your database. If so, then it should skip sign up and proceed directly to the sign in process. Configure the condition based on the parameters mentioned above, to determine whether to accept the registration or not, and how to handle the different use cases: ![Post register condition](/assets/post-register-condition.webp) An example of what the flow looks like: ![Sign up flow](/assets/sign-up-flow.webp) ## How to Verify a User This section covers how you can verify if a user is part of the previously defined collection using their photo. This is equivalent to adding an extra layer of verification, such as multi-factor authentication (MFA) or step up. Verification requires a selfie photo and an external ID (automatically populated using the user's identifier). To verify a user as part of an MFA process, follow these steps: 1. Design the flow to include a sign in process using your desired authentication methods. 2. Add the **_Take a Photo_** component to a new screen in your flow, after its sign in section: ![Take a photo](/assets/take-photo.webp) 3. Then, add the **_AWS Rekognition / Register_** action block to the flow, and link it to the screen created in the previous step. It's a good idea to use a meaningful context key value (such as `verify`, in the example below). ![Verify step](/assets/verify-step.webp) 4. Add a new condition block to the flow, and connect it to the **_AWS Rekognition / Register_** action block. In the condition, you can use the information received from the previous step, which includes: - `externalIdMatched`: **True or False** - returns `true` if the submitted external ID exists in the collection. This means that the user already exists in the collection and can continue with the verification process. - `confidence`: **Scale of 0-100** - the confidence level of the submitted photo already existing in the collection (the higher the score - the higher the confidence). This score allows your to accept the user's MFA - and approve its sign in attempt. Configure the condition based on the parameters mentioned above, to determine whether to accept the verification or not, and how to handle the different use cases: ![Post verify condition](/assets/post-verify-condition.webp) An example of what the flow looks like: ![Sign In flow](/assets/sign-in-flow.webp) And that's it! You should now be able to use AWS Rekogition in your authentication flow, using Descope. # Incode (/connectors/connector-configuration-guides/kyc/incode) Use Descope's Incode Connector to achieve facial recognition in your authentication flow # Incode Connector [Incode](https://www.incode.com/) is a cloud-based identity verification platform that offers computer vision capabilities for analyzing and processing images, and can be used to verify user identities. This guide shows how to use Descope's Incode Connector to verify user identities in your authentication flow. ## Setting Up the Incode Connector To integrate the Incode connector, follow the steps below: ### 1. Navigate to Connector - Visit the [Connectors page](https://app.descope.com/connectors) in the Descope Console. - Select **Incode** from the list of connectors. ### 2. Connector Setup Enter the following information to configure the connector: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **(Optional) Connector description**: A brief description of your connector's purpose. - **API Key**: Your production API Key as appears in your Incode account. - **API URL**: The base URL of the Incode API. - **Flow ID**: The flow ID of the Incode flow you want to use. ![Incode connector setup](/assets/incode-config.webp) ### 3. Test & Save - Validate your configuration by clicking the `Test` button and reviewing the `Test Results` section. - Complete the setup by selecting `Create`. ## Implementing the Incode Connector in Your Flow ### 1. Select or Create a Flow - Navigate to the [Flows Page](https://app.descope.com/flows) of the Descope console. - Select an existing flow or create a new one. ### 2. Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below: ![Incode connector flow](/assets/incode-connector-action.webp) When the `Incode / Verify` step runs, the user is redirected to Incode to complete the verification flow configured in your connector (`Flow ID`). After that flow finishes, Incode redirects the user back to your Descope flow with the connector output populated. Add a **condition** step immediately after the connector to evaluate whether verification succeeded, using keys such as `connectors.incode_verify.status`, `connectors.incode_verify.data.status`, and `connectors.incode_verify.data.score.overall.status`. ![Incode connector flow](/assets/incode-condition.webp) This is an example of a flow that uses the Incode connector to verify a user's identity: ![Incode connector flow](/assets/incode-flow.webp) # KYC (/connectors/connector-configuration-guides/kyc) Overview of Descope KYC connectors for verifying user identity and documents within flows. # KYC Connectors KYC (Know Your Customer) connectors let you integrate third-party identity verification providers directly into Descope flows. They enable you to verify government-issued IDs, perform face matching, and validate user data before granting access. ## How KYC Connectors Work When you add a KYC connector step to a flow, the flow: 1. Collects the required user data (such as document images, photos, or personal information) 2. Sends the data to the configured verification provider 3. Receives verification results, status codes, scores, or user attributes 4. Makes those results available for conditional logic inside the flow Common patterns include: - Blocking or flagging users whose verification fails - Requiring additional authentication when verification is pending or inconclusive - Storing document metadata on the user for audit purposes ## Available KYC Connectors Descope supports various KYC platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # AbuseIPDB (/connectors/connector-configuration-guides/fraud/abuseipdb) Leverage Descope's AbuseIPDB connector to establish a reputation-based score on a user's originating IP address # AbuseIPDB Connector Descope's Abuse IPDB connector helps you establish a reputation-based score on a user's originating IP address, thus aiding in the detection of fraudulent or hacker-associated connections to your system. The connector offers you a result varying from 0 to 100, where 0 means safe , and 100 very likely to take part in such activity. The rate limit for the AbuseIPDB connector relies on the AbuseIPDB platform and its licensing, and it is up to the customer to consider the pricing. ## Setting Up The AbuseIPDB Connector To integrate the AbuseIPDB connector, follow the steps below: ### 1. Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **AbuseIPDB** from the list of connectors. ### 2. Connector Setup Ready your [AbuseIPDB](https://www.abuseipdb.com/) private API access token for integration. ![abuseipdb key generation](/assets/abuseipdb-key.webp) ![abuseipdb connector setup](/assets/abuseipdb-connector-setup.webp) Proceed with the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **(Optional) Connector description**: A brief description of your connector's purpose. - **API Key**: The AbuseIPDB API access token generated for the Descope service. ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the AbuseIPDB Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Opt for an existing flow or generate a new one. ### 2. Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below: ![abuseipdb connector flow component](/assets/abuseipdb-flow-component.webp) Integrating that connector action inside a flow can be used in various scenarios. Here is an example of a flow that utilizes the result as follows: ![abuseipdb connector flow component](/assets/abuseipdb-flow-condition.webp) ![abuseipdb connector flow condition](/assets/abuseipdb-flow-condition-2.webp) The flow checks whether the IP Reputation came back above 50 (This integer can be altered at your wish) and, if so, asks the user for a second-factor authentication. The condition key to get the IP reputation value will be `connectors.abuseipdb_checkIP.abuseConfidenceScore`. # Alloy (/connectors/connector-configuration-guides/fraud/alloy) Use Descope's Alloy connector for identity verification and fraud monitoring through Alloy's Journey and Events APIs. # Alloy Connector This connector integrates with [Alloy](https://www.alloy.com/) to provide identity verification and fraud monitoring through Alloy's [Journey](https://developer.alloy.com/public/reference/post_journeys-journey-token-applications) and [Events](https://developer.alloy.com/public/reference/post_events) APIs. Descope's Alloy connector lets you submit entities to Alloy Journeys and send lifecycle events for ongoing monitoring, then branch your flows using the responses. ## Configuring the Alloy Connector To integrate the Alloy connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Alloy** from the list of connectors. ### Connector Setup Configure the following fields: * **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. * **Connector description**: (Optional) A short description of how you use this connector. * **API token**: The Alloy API token. * **API secret**: The Alloy API secret. * **Base URL**: The base URL for the Alloy API, for example: `https://api.alloy.co/v1` (production) or `https://sandbox.alloy.co/v1` (sandbox). ![Alloy connector setup](/assets/alloy-configuration.webp) ### Test & Save * Use the **Test** button to verify your configuration. The test performs an authenticated request to the Alloy API to confirm that the credentials and base URL are correct. * When the test succeeds, click **Create** to save the connector. ## Implementing the Alloy Connector Place Alloy connector steps **after** screens or actions that collect the identity and context fields you map into the connector (for example, name, email, address). ### 1. Select or Create a Flow Navigate to [Flows](https://app.descope.com/flows) in the Descope Console and open an existing flow or create a new one. ### 2. Integration Click the blue **+** icon in the flow builder and select **Connector**, then select the **Alloy** action steps. ![Alloy connector flow](/assets/alloy-connector-flow.webp) #### Alloy / Journey Application Submits a person or business entity to an Alloy Journey for identity verification (KYC/KYB). You can map common identity fields from your flow context—such as first name, last name, email, phone, date of birth, address, and SSN—and supply any journey-specific fields that are not covered by explicit parameters through the **Additional entity data** object. After a Journey Application step runs, you can access the results in the flow context under the key `connectors.alloy_journeyApplication`. ![Alloy journey application response](/assets/alloy-journey-action.webp) For the complete response shape, see the [Alloy Journey Applications API reference](https://developer.alloy.com/public/reference/post_journeys-journey-token-applications). #### Alloy / Send Event Sends lifecycle events to Alloy for ongoing monitoring. Supported event types include `login`, `person_created`, `person_updated`, `transaction`, `credentials_updated`, and others defined by Alloy. You must supply at least one entity identifier: either an **external entity ID** (your own id, such as the Descope login id used when the journey was created) or an **entity token** from Alloy’s journey response. Each event type can require specific fields in the **Event data** parameter. For example, a `login` event expects `login_method` with one of: `biometric`, `password`, `passwordless`, `sso`. See the [Alloy Events API reference](https://developer.alloy.com/public/reference/post_events) for type-specific requirements. After Send Event, you can access the results in the flow context under the key `connectors.alloy_sendEvent`. ![Alloy send event response](/assets/alloy-send-event.webp) Refer to the [Alloy Events API reference](https://developer.alloy.com/public/reference/post_events) for full field documentation. # Arkose Labs (/connectors/connector-configuration-guides/fraud/arkose) Leverage Descope's Arkose Labs connector to protect your authentication flow from bot attacks # Arkose Labs Connector Arkose Labs is a bot and fraud prevention platform that uses enforcement challenges. These challenges are not static like traditional CAPTCHAs—they are context-based and dynamic, making them resistant to automation and AI. Use the Arkose Labs connector to integrate the bot and fraud prevention capabilities of Arkose Labs into your flow. This connector lets you trigger Arkose Labs Enforcement Challenges whenever a user interacts with specific elements in a flow screen. ## Setting Up the Arkose Labs Connector To integrate the Arkose Labs connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors/template/arkose). * Choose **Arkose Labs** from the list of connectors. ### Connector Setup Set up the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **Connector description**: (Optional) A brief description of your connector's purpose. - **Public Key**: The Arkose Labs public key, that can be found on the Keys screen in the Arkose Labs portal. - **Private Key**: The Arkose Labs private key, that can be found on the Keys screen in the Arkose Labs portal. - **Client API Base URL**: (Optional) A custom base URL to use when loading the Arkose Labs API script. If not provided, the default value of `https://client-api.arkoselabs.com/v2` will be used. - **Verify API Base URL**: (Optional) A custom base URL to use when verifying the session token using the Arkose Labs Verify API. If not provided, the default value of `https://verify-api.arkoselabs.com/api/v4` will be used. ### Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the Arkose Labs Connector in Your Flow ### Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below: ![arkose connector component](/assets/arkose-connector.webp) This flow shows an example where the user needs to complete the Arkose Labs challenge on the Welcome screen to proceed: ![arkose connector flow](/assets/arkose-connector-flow.webp) In the flow, go to the screen you want to protect and in the Actions section, select the **Arkose Labs** connector. This will trigger the Arkose Labs challenge whenever the user interacts and tries to proceed to the next step. ![screen action arkose](/assets/screen-action-arkose.webp) ### How Enforcement Works When the Arkose Labs connector is active on a screen, any button that attempts to move the user forward will be subject to Arkose Labs validation. If Arkose Labs detects suspicious or high-risk signals, it automatically presents the user with a challenge (puzzle) before allowing them to proceed. ![enforcement challenge](/assets/enforcement-challenge.webp) If the challenge fails, Arkose Labs will return with **validation failed**, and the end user will be blocked from moving to the next step in the flow. If the challenge succeeds, the user can continue normally onto the next screen without interruption. ## Testing the Arkose Labs Connector If you would like to simulate a high risk scenario and trigger the Arkose Labs challenge prompt, you can do this by following the steps below: To test the challenge behavior: 1. Open Chrome DevTools. 2. Go to the Network Conditions tab (or via Command Menu → “Network conditions”). 3. Change your User Agent to: `challengeme` 4. Retry the flow action. This forces Arkose Labs to present a challenge so you can confirm that the connector is working end-to-end. ![network conditions](/assets/network-conditions.webp) # AWS SES Email Validation (/connectors/connector-configuration-guides/fraud/aws-ses-email-validation) Use Descope's AWS SES Email Validation connector to check email syntax, DNS records, mailbox existence, and deliverability using Amazon SES. # AWS SES Email Validation Connector AWS SES Email Validation uses the Amazon SES `GetEmailAddressInsights` API to validate email addresses, checking syntax, DNS records, mailbox existence, and whether the address is disposable, role-based, or randomly generated. Descope's AWS SES Email Validation connector allows you to run this check directly in your authentication flows to catch invalid or low-quality email addresses before they reach your user base. Your AWS account must have [Virtual Deliverability Manager](https://docs.aws.amazon.com/ses/latest/dg/vdm-getting-started.html) enabled in SES for the target region. ## Setting Up the AWS SES Email Validation Connector To integrate the connector, follow the steps below: ### 1. Navigate to Connector - Visit the [Connectors page](https://app.descope.com/connectors) in the Descope Console. - Select **AWS SES Email Validation** from the list of connectors. ### 2. Connector Setup Enter the following information to configure the connector: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description (Optional)**: Describe what your connector is used for. - **Authentication**: Choose between the following authentication options: #### Use AWS Credentials Use your AWS credentials to authenticate. You will need to provide: - **Access Key ID**: The AWS access key ID. - **Secret Access Key**: The AWS secret access key. - **(Optional) Session Token**: A session token for temporary credentials. The IAM user associated with these credentials must have the `ses:GetEmailAddressInsights` permission. You can attach a policy like this: ```json title="Policy editor" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": ["ses:GetEmailAddressInsights"], "Resource": "*" } ] } ``` #### Use Role Based Permissions Use role-based permissions to authenticate instead of long-lived credentials. You will need to provide: - **Role ARN**: The Amazon Resource Name (ARN) of the role that has permissions to call `ses:GetEmailAddressInsights`. - **External ID**: The external ID used to assume the role. When creating the IAM role for this connector, you must include a trust policy that allows Descope to assume the role: **Trust Policy Requirements:** - **Principal**: `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` - **External ID**: The external ID provided by Descope during connector configuration - **Action**: `sts:AssumeRole` Your trust policy should look like this: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::312892722078:role/prod-external-role-us-east-1" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "your-external-id-from-descope" } } } ] } ``` Without this trust policy configuration, you'll receive an `is not authorized to perform: sts:AssumeRole` error when testing the connector. - **Region**: The AWS region to use, e.g. `us-east-1`. ![AWS SES Email Validation connector setup](/assets/ses-validation-config.webp) ### 3. Test & Save - Enter an **Email** in the Test Configuration section and click `Test` to verify your configuration works. - Review the results in the `Test Results` panel. - Once successful, click `Create` to save the connector. ## Implementing the AWS SES Email Validation Connector in Your Flow ### 1. Select or Create a Flow - Navigate to the [Flows page](https://app.descope.com/flows) of the Descope console. - Select an existing flow or create a new one. ### 2. Add the Connector to the Flow Click the blue plus icon in the flow builder, select **Connector**, and choose the **AWS SES Email Validation** action. You can enter the email address to validate manually or set it dynamically based on previous attributes (e.g., `{{form.email}}` or `{{user.email}}`). ![AWS SES Email Validation connector action](/assets/ses-validation-action.webp) ### 3. Handle the Response The connector's response is mapped into the `connectors.aws-ses-email-validation_validateEmail` context key. Add a **condition** step immediately after the connector to refer to this context key and query its data to evaluate the validation result. # Bitsight (/connectors/connector-configuration-guides/fraud/bitsight) Use Descope's Bitsight Threat Intelligence connector to detect leaked credentials & enrich suspicious IoCs during auth flows. # Bitsight Connector Descope's Bitsight connector enables you to integrate security rating assessments into your authentication flows, allowing you to make risk-based decisions about user access based on their organization's security posture. ## Setting up the Bitsight Connector To integrate the Bitsight connector, follow the steps below: ### 1. Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Bitsight** from the list of connectors. ### 2. Connector Setup Set up the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **Connector description**: (Optional) A brief description of your connector's purpose. - **Client ID**: API Client ID issued when you create the credentials in Bitsight Threat Intelligence. - **Client Secret**: Client secret issued when you create the credentials in Bitsight Threat Intelligence. ![Bitsight Connector Configuration](/assets/bitsight-connector-configuration.webp) ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the Bitsight Connector ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com/flows) -> Flows. - Opt for an existing flow or generate a new one. ### 2. Integration Click the `+` plus icon in the flow builder and select **Connector**. You should be able to see the **Bitsight Threat Intelligence / Enrich IOC** and **Bitsight Threat Intelligence / Search Leaked Credentials** connector actions. ![Bitsight connector actions](/assets/bitsight-connector-actions.webp) #### Bitsight Threat Intelligence / Search Leaked Credentials Check if an email address or domain has been exposed in breach dumps. This is ideal for evaluating login or signup risk by detecting whether a user's credentials have appeared in breach dumps. The result is saved in the connector context, e.g., `connectors.bitsight_leaks_search`. You can use this result in a condition to block the user if the email address or domain has been exposed in breach dumps. ![Bitsight connector flow component](/assets/bitsight-leaks-search-flow-component.webp) #### Bitsight Threat Intelligence / Enrich IOC Get STIX-formatted threat intelligence data about a suspicious IP address, domain, or hash. This is ideal for detecting whether an entity is associated with malware, APT groups, or criminal campaigns. You need to provide the indicator type and the indicator value. The indicator type can be `ip`, `domain`, or `hash`. The result is saved in the connector context, e.g., `connectors.bitsight_ioc_enrich`. You can use this result in a condition to block the user if the IP address, domain, or hash is associated with malware, APT groups, or criminal campaigns. ![Bitsight connector flow component](/assets/bitsight-enrich-ioc-flow-component.webp) This is an example of a flow that uses the **Bitsight Threat Intelligence / Enrich IOC** connector action to enrich the IP address and then use the result in a condition to block the user if the IP address is associated with malware, APT groups, or criminal campaigns. ![Bitsight connector flow example](/assets/bitsight-connector-flow-example.webp) # Darwinium (/connectors/connector-configuration-guides/fraud/darwinium) Use Descope's Darwinium connector for AI-powered fraud detection and device intelligence across the entire customer journey # Darwinium Connector Descope's Darwinium connector integrates directly with Darwinium's Event API to perform device profiling and risk assessment during your authentication flows. The connector automatically collects profiling data and sends it to Darwinium for analysis, and returns a decision result that you can use to control flow logic—allowing you to pass, challenge, reject, or review users based on their risk profile. ## Setting Up The Darwinium Connector To integrate the Darwinium connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Darwinium** from the list of connectors. ### Connector Setup Set up the required credentials and configuration provided by Darwinium: - **Connector Name**: Assign a custom name for your connector. - **Connector Description**: (Optional) A brief description of your connector's purpose. - **Darwinium Node Name**: The name of your Darwinium node (provided by Darwinium during onboarding). - **Darwinium Journey Name**: The name of the Darwinium journey to use for profiling. A journey represents a sequence of user interactions that Darwinium tracks for risk assessment. - **Darwinium Web API Name**: The name of the Darwinium Web API to use for browser-based profiling. - **Darwinium Native Mobile API Name**: (Optional) The name of the Darwinium Native Mobile API to use. - **Native Blob Key Name**: (Optional) The key name for the native profiling blob sent via the client parameter. If not provided, the default key of `nativeProfilingBlob` will be used. - **PEM Certificate**: The PEM certificate for client authentication, provided by Darwinium. - **Passphrase**: (Optional) The passphrase for the PEM certificate, if applicable. - **Private Key**: The private key for client authentication, provided by Darwinium. - **Profiling Tags Script URL**: (Optional) The custom URL where the Darwinium Tags script is hosted. - **Default Result**: The default result to return if no result is available from Darwinium. Options: **Pass**, **Challenge**, **Reject**, **Review** (default: Pass). ![darwinium connector setup](/assets/darwinium-connector-setup.webp) ### Test & Save * Validate your configuration using the `Test` button and observing the `Test Results` section. * Conclude the setup process by selecting `Create`. The Darwinium connector performs client-side profiling and must be placed after at least one **screen** component in your flow to ensure the profiling script has loaded and can collect behavioral data. ## Implementing the Darwinium Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com/flows) -> Flows. - Opt for an existing flow or generate a new one. ### 2. Integration Click the `+` plus icon in the flow builder and select **Connector**. You should be able to see the **Darwinium / Profile User** connector action. This action automatically collects profiling data from the user's device (web browser or mobile app) and sends it to Darwinium's Event API. Darwinium analyzes the profiling data along with any associated user information and returns a decision result based on your configured decision strategies. You need to select the Darwinium connector on the first screen in the flow builder to ensure the profiling data is collected. This is required for the connector to work correctly. The connector should be placed after the screen component in the flow builder. ![Darwinium Screen](/assets/darwinium-screen.webp) The following flow shows an example where the user enters their email and details on the first screen and then we send the profiling data to **Darwinium / Profile User** connector that is then passed to a condition that checks the Darwinium decision result. The Darwinium response is mapped into the `darwinium_profileUser` context key in the flow. You can add a **Condition** step that reads from this context key and uses the `result` field to decide how to route the user. You can use the `result` value in **Conditions** to determine if the user should proceed, undergo additional verification (step-up authentication), be blocked, or be flagged for manual review. In the example below: - If `result` is `pass`, the user continues in the flow. - If `result` is `challenge`, the user is prompted to complete the Challenge and then continue in the flow. - If `result` is `review`, the user is flagged for manual review. - If `result` is `reject`, the user is blocked from continuing in the flow. You can customize the flow conditions and actions based on your specific risk assessment needs. ![darwinium flow example](/assets/darwinium-flow-example.webp) ![darwinium flow condition](/assets/darwinium-condition.webp) # Elephant (/connectors/connector-configuration-guides/fraud/elephant) Leverage Descope's Elephant connector to establish an identity trust score for new users # Elephant Connector Use the [Elephant](https://www.elephant.online/) Connector to obtain an identity trust score for users. The risk score is calculated based on adaptive algorithms and extensive identity data. You can also use more detailed information that Elephant provides about users (e.g. email address or phone number age). ## Setting Up The Elephant Connector To integrate the Elephant connector, follow these steps: ### 1. Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Select **Elephant** from the list of connectors. ### 2. Connector Setup Prepare your Elephant Access Key: If you're new to Elephant, use the [request portal](https://go.elephant.online/descope-request). If you have an existing Elephant account, visit the [site](https://trust.pipl.com/organization/keys). Enter the following information: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **(Optional) Connector description**: A brief description of your connector's purpose. - **Access Key**: The Elephant API access key generated for the Descope service. ### 3. Test & Save - Validate your configuration by clicking the `Test` button and reviewing the `Test Results` section. - Complete the setup by selecting `Create`. ## Implementing the Elephant Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Choose an existing flow or create a new one. ### 2. Integration Click on the blue plus sign inside the flow builder and select "Connector". You should see the new connector action as shown below: ![elephant connector flow component](/assets/elephant-connector-action.webp) For more information on available fields, refer to the Elephant [trust API response schema documentation](https://docs-trust.pipl.com/docs/api/trust-api/response/schema). The connector action can be integrated into your flow in various ways. Here's an example of how to utilize the results: ![elephant connector flow component](/assets/elephant-flow-layout.webp) ![elephant connector flow condition](/assets/elephant-flow-condition.webp) The flow determines whether Elephant allows the end user to proceed with sign-up based on various indicators. The key used in the flow is `connectors.elephant_riskCheck.decision`, which can return `approve`, `review`, or `decline`. For more comprehensive checks, the Elephant connector allows you to base your flow conditions on various keys, as detailed in the API documentation above. Some key metrics include [score](https://docs-trust.pipl.com/docs/api/trust-api/response/scores), [connectivity](https://docs-trust.pipl.com/docs/api/trust-api/response/connectivity), and [trust signals](https://docs-trust.pipl.com/docs/api/trust-api/response/signals). The Descope flow library offers examples for different use cases with Elephant: [Sign up with Elephant identity trust decision](https://app.descope.com/flows?template=sign-up-with-elephant-identity-trust-decision) [Sign up with Elephant email domain type check](https://app.descope.com/flows?template=sign-up-with-elephant-email-domain-type-check) # Fingerprint (/connectors/connector-configuration-guides/fraud/fingerprint) Use Descope's connector to Fingerprint to perform device detection for malicious or bot activity. # Fingerprint Connector Fingerprint (previously FingerprintJS) is a device detection platform that provides valuable information about the device. Fingerprint's key feature is its ability to generate a unique ID, often called a "device fingerprint", and permanently associate it with each device. Once the service identifies the device, it can provide crucial geoinformation, bot detection, VPN detection, and more. This connector provides the ability to automate threat detection in an authentication process with Descope and scare off unwelcome visitors to a platform. ## Setting Up The Fingerprint Connector To integrate the Fingerprint connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Fingerprint** from the list of connectors. ### Connector Setup Set up the necessary inputs: - Connector name: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - (Optional) Connector description: A brief description of your connector's purpose. Proceed by providing the connector `Public API Key` and `Secret API Key`, which are copied from Fingerprint's configuration page, the details for these keys can be found here: * Login to `https://fingerprint.com/`. * Go to "App Settings". * Go to the API Keys tab. ### CloudFlare Integration When a Descope flow uses the Fingerprint connector, it calls Fingerprint to establish or get a unique device ID. Toggle CloudFlare integration to configure Fingerprint's API calls to match your domain. Integration with CloudFlare needs to be ready on Fingerprint [read more](https://dev.fingerprint.com/docs/cloudflare-integration). * Cloudflare integration Script URL: The Cloudflare integration Script URL (format example - `https://api.company.com/fXj8gt3x8VulJBnb/w78aRZnnDZ3Aqw0v`). * Cloudflare integration Endpoint URL: The Cloudflare integration Endpoint URL (format example - `https://api.company.com/fXj8gt3x8VclJBna/x96Emn69oZwcd7I4`). Make sure that the URLs are accessible. ### Test & Save The "Test" button allows you to test the configuration, ensuring it was successfully configured before using it in a flow. Note that it may take up to a minute for a new private key created in Fingerprint to work. Until then, you may face a failure response. - Conclude the setup process by selecting `Create`. ## Implementing the Fingerprint Connector ### Select or Create a Flow Access your [Dashboard](https://app.descope.com/flows) and go to flows. You can choose an existing flow or generate a new one. ### Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector actions as shown below: ![fingerprint flow actions](/assets/fingerprint-flow-actions.webp) Since fingerprinting is completed within the browser, the connector steps need to be added after at least one "screen" component has been displayed to the user. ## Examples Integrating one of the connector actions inside a flow can be used in various scenarios. Here is an example of a flow that utilizes the result as follows: ![fingerprint flow example](/assets/fingerprint-flow-example.webp) ![fingerprint flow condition](/assets/fingerprint-flow-condition.webp) The flow checks whether the user returned from a non-VPN origin and continues logging him in. This example uses the context key - `connectors.fingerprint_getEvent.data.vpn.data.result` To read about the possible results from Fingerprint [click here](https://dev.fingerprint.com/reference/getevent). # Forter (/connectors/connector-configuration-guides/fraud/forter) Use Descope's connector to Forter to astablish a ML based risk score on user's behavior, thus helping you detect fraud or hacker associated connections. # Forter Connector Forter is a fraud prevention service that provides a real-time evaluation at different touchpoints during the user's journey. Forter uses machine learning to analyze account activities such as sign-ups and logins, providing risk assessments to ensure the security of your users. Descope's Forter connector helps you astablish a machine-learning based risk score on user's behavior, thus helping you detect fraud or hacker associated connections to your system. The connector offers you a result with multiple call-to-actions to use inside your flow, such as a recommendation to log in the user. ## Setting Up The Forter Connector To integrate the Forter connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Forter** from the list of connectors. ### Connector Setup Set up the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **Connector description**: (Optional) A brief description of your connector's purpose. - **Site ID**: The Forter site ID. - **Secret Key**: The Forter secret key. - **API Version**: (Optional) Configure which version of the Forter API is being used, most recent by default The Forter `Site ID` and `Secret Key` can be found in the [Forter portal](https://portal.forter.com/app/developer/settings/general). ### Test & Save The Forter connector offers a way to test the integration with a specified email and IP address. * Override IP Address: Override the user IP address. * Override User Email: Override the user email. Overriding the user IP address or email is intended for testing purpose and should not be utilized in production environments. - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the Forter Connector Since the Forter connector runs in the browser, the connector steps need to be added after at least one **screen** or **action** component in the flow. ## Select or Create a Flow Access your [Dashboard](https://app.descope.com/flows) and go to flows. Opt for an existing flow or generate a new one. ### Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector actions as shown below: ![forter flow actions](/assets/forter-flow-actions.webp) ### Log In `Forter / Login Check` is designated to be used after log-in to asses the user's behavior given the parameters. - **Customer's account UID**: In the merchant's site. Should not be the user email. Leave empty if no account ID is available. - **Login Method Type**: The authentication method used to log in. Possible values are: * "PASSWORD" * "SMS" * "SOCIAL" * "EMAIL_LINK_OTP" * "AUTH_TOKEN_REFRESH" * "APPLE" * "MFA_CODE_REFRESH" * "EMAIL_MAGIC_LINK" * "OTHER" - **Login Status**: The status of the login attempt. Possible values are: * "SUCCESS" * "FAILED" * "BLOCKED_BY_MERCHANT" * "ACCOUNT_DOES_NOT_EXIST" - **User Input Type**: The type of identifier used by the user to log in. Possible values are: * "USERNAME" * "EMAIL" * "PHONE" * "SOCIAL" - **User Email**: The user's email. Leave empty if no email is available. - **User Phone**: The user's phone. Leave empty if no phone is available. - **Additional Information**: Generic object to include any generic data about the user. - **Account Data**: Account data object as described in Forter docs. - **Additional Authentication Methods**: Additional authentication method object as described in Forter docs. - **Customer Signed In Using Social Network Account**: Boolean indicating if customer signed in using a social network account. ### Sign Up `Forter / Sign Up Check` is designated to be used after sign-up to asses the user's behavior given the parameters listed below: - **Customer's account UID**: In the merchant's site. Should not be the user email. Leave empty if no account ID is available. - **Additional Information**: Generic object to include any generic data about the user. - **Account Data**: Account data object as described in Forter docs. - **Additional Account Event Identifiers**: Additional account event identifiers object as described in Forter docs. - **Customer Signed In Using Social Network Account**: Boolean indicating if customer signed in using a social network account. - **Promotions**: Promotions object as described in Forter docs. ### Profile Access `Forter / Profile Access Check` is designated to be used after an MFA attempt to assess the user's behavior given the parameters listed below: - **Customer's account UID**: In the merchant's site. Should not be the user email. Leave empty if no account ID is available. - **Additional Information**: Generic object to include any generic data about the user. - **Account Owner**: Account owner object as described in Forter docs. - **Additional Account Event Identifiers**: Additional account event identifiers object as described in Forter docs. ### Response The response object populates the following context keys, depending on the action performed: * `connectors.forter_loginCheck` * `connectors.forter_signUpCheck` * `connectors.forter_profileAccessCheck` The fields that can be used inside the flow are: * __forterDecision__ - The latest Forter decision regarding the attempted action. * __recommendation__ - A specific recommendation for an action that might help the customer to complete their transaction/action (e.g. verify phone via SMS, verify via push notification, verify email, perform a 3DS check, etc.) * __verificationMethod__ - The specific verification method to be used when verification is required according to "recommendation" field. * __decisionReason__ - The main reason behind the Forter decision. * __merchantPolicyId__ - UID of the custom policy created in Forter's Policies tool that resulted in this decision. * __accountId__ - When applicable, the customer's account UID in merchant's site. * __correlationId__ - A forter unique identifier that should be sent to Forter as part of the AdvancedAuthenticationMethod object to correlate the MFA recommendation given in this response with the relevant additional authentication attempt result. To learn more about Forter's APIs and what you can retrieve with this connector, visit the Forter [API Reference](https://portal.forter.com/app/developer/api/api/services-and-apis/account-login-api#response) guide. ## Examples Integrating one of the connector actions inside a flow can be used in various scenarios. Here is an example of a flow that utilizes the result as follows: ![forter flow example](/assets/forter-flow-example.webp) ![forter flow condition](/assets/forter-flow-condition.webp) The flow checks whether the user came back with an approved recommendation, if so it asks the user for a second-factor authentication. # hCaptcha (/connectors/connector-configuration-guides/fraud/hcaptcha) # hCaptcha Connector Use Descope's hCaptcha connector to secure your authentication flow. [hCaptcha](https://www.hcaptcha.com/) is a service that protects your site from spam and abuse that uses advanced risk analysis techniques to tell humans and bots apart. This article will explain how to set up the connector and include it in your Descope authentication flow. ## Configure hCaptcha connector Start with the [hCaptcha template](https://app.descope.com/connectors/template/hcaptcha) on the Connectors page of the Descope Console. Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Site Key**: The unique site key generated in hCaptcha. For more information, see [here](https://docs.hcaptcha.com/switch/#get-your-hcaptcha-sitekey-and-secret-key). - **Secret Key**: The account secret key you get from hCaptcha. For more information, see [here](https://docs.hcaptcha.com/switch/#get-your-hcaptcha-sitekey-and-secret-key). - **Bot Threshold (0-1)**: The bot threshold is used to determine whether the request is a bot or a human. The score ranges between 0 and 1, where 1 is a human interaction and 0 is a bot. If the score is below this threshold, the request is considered a bot. And if you'd like, the optional fields: - **Connector description**: Describe what your connector is used for. - **Override Assessment (For Testing)**: Allows for automated testing by overriding the assessment model with a custom score. Not intended for production environments, only for testing. * **Assessment Score (0-1)**: When configured, the hCaptcha action will return the score without assessing the request. The score ranges between 0 and 1, where 1 indicates a human interaction and 0 indicates a bot. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![hCaptcha connector initialization](/assets/hcaptcha-connector-creation.webp) Save your configuration by hitting `Create`. ## Add your hCaptcha connector to a screen Go to the [Dashboard](https://app.descope.com/flows) and open your flow. In the editor, select a screen (typically the first one), then find the **hCaptcha** widget under the **Risk** section. Drag it onto the screen and click **Done**. ![hCaptcha connector placement in screen](/assets/hcaptcha-widget-placement-in-screen.webp) ## Add your hCaptcha connector to the flow Since the hCaptcha connector runs in the browser, the connector actions need to be added after at least one **screen** or **action** component in the flow. Back in the flow editor, click the **create**(`+`) button, select **Connector**, and choose **hCaptcha**. Click to set down the connector in your flow editor, and connect it at a point that comes after the screen with the hCaptcha widget that you added in the previous step. Now, hit **Save**. ![hCaptcha connector placement](/assets/hcaptcha-connector-placement.webp) Whenever a user goes through your authentication flow, the hCaptcha connector will automatically run and the risk analysis will now be included in the flow `riskInfo.riskScore` key. # Fraud & Risk Connectors (/connectors/connector-configuration-guides/fraud) Fraud Connectors Overview # Fraud & Risk Connectors Fraud Connectors in Descope enable you to integrate with third-party fraud detection and risk assessment services to protect your authentication flows from bots, malicious activity, and fraudulent access attempts. Unlike analytics connectors that stream data, Fraud Connectors are used as steps within your [Flows](/flows) to evaluate user risk and make real-time security decisions during authentication. ## How Fraud & Risk Connectors Work For more information on using built-in risk signals and how to combine them with fraud & risk connectors, see our [Fingerprinting Doc](/fingerprinting). Fraud & Risk Connectors are added as steps within your Descope Flows to evaluate user risk at various points in the authentication journey. When a connector step is executed, it: 1. Collects relevant data about the user, device, or session 2. Sends the data to the third-party fraud detection service 3. Receives risk assessments, scores, or detection signals 4. Makes the results available in your flow for conditional logic You can use the connector's risk signals to implement conditional branching in your flows, such as: - Requiring additional authentication steps for high-risk users - Blocking suspicious signups automatically - Allowing low-risk users to proceed with reduced friction - Triggering step-up authentication based on risk scores Fraud & Risk Connectors complement Descope's built-in risk signals which include bot detection, impossible travel detection, and risk scoring. ## Available Fraud Connectors Descope supports various fraud detection and risk assessment platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Pwned (/connectors/connector-configuration-guides/fraud/pwned) Leverage Descope's Have I Been Pwned connector to verify password security by checking against known data breaches # Have I Been Pwned Connector The Have I Been Pwned connector offered by Descope is an invaluable tool to enhance password security. By cross-referencing passwords with an extensive database of data breaches, this connector ensures that users avoid passwords previously compromised. Dive into this guide to set up and incorporate the Have I Been Pwned connector within your flows. ## Setting Up The Have I Been Pwned Connector For a seamless integration of the connector, follow these steps: ### 1. Navigate to Connector - Head over to your [Descope Console](https://app.descope.com). - Path: Dashboard -> Connectors. - Opt for “Have I Been Pwned”. ### 2. Connector Configuration ![Have I Been Pwned connector configuration](/assets/have-i-been-pwned-connector-configuration.webp) Complete the necessary fields: - Connector name: Assign a custom name for the connector. This distinction is beneficial when you've multiple connectors based on the same template. - (Optional) Connector description: Provide a brief outline of your connector's functionality. ### 3. Usage To use the connector, include the 'Breached Password Check' action subsequent to any user interface where a password input is required. Then you can conditionally render different actions based on success, error with HIBP API Check, No Password provided, or password used has appeared in a breach. ![Have I Been Pwned connector configuration](/assets/have-i-been-pwned-connector-usage.webp) ## Enhancing Password Security in Your Flow Incorporating the Have I Been Pwned connector is a proactive step towards better password security. As users navigate through the flow, they receive immediate feedback about the strength and security of their chosen passwords, promoting better password practices. # Reassigned (/connectors/connector-configuration-guides/fraud/reassigned) Learn how to integrate Reassigned's RND database to detect phone number risk and protect against number recycling fraud. # Reassigned Connector Descope's Reassigned connector integrates Reassigned's RND (Reassigned Number Database) to detect whether a phone number's owner has changed since a specific date. This connector helps protect against phone number recycling fraud, where threat actors exploit recently reassigned phone numbers to gain unauthorized access to accounts or intercept sensitive communications. Phone number recycling occurs when carriers reassign inactive numbers to new subscribers. Without proper checks, legitimate authentication messages or account recovery codes may be delivered to unintended recipients, creating security vulnerabilities. The Reassigned connector relies on the external Reassigned Numbers Database (RND). Response times, uptime, and the freshness of results are determined by the RND service, which is not real-time and may only be updated monthly. ## Setting Up the Reassigned Connector To integrate the Reassigned connector, follow the steps below: ### 1. Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Reassigned** from the list of connectors. ### 2. Connector Setup Set up the necessary inputs: ![Reassigned connector setup](/assets/reassigned-connector-setup.webp) - **Connector Name**: Assign a unique name to your connector for easy identification, particularly useful when using multiple connectors of the same type. - **(Optional) Connector Description**: Summarize the purpose of your connector. - **Company ID**: Your RND company ID (e.g., C038612852). Retrieve this from your RND account under Account -> Company -> Company ID. - **Refresh Token**: Your RND refresh token for authentication. Retrieve this from your RND account under Account -> API Credentials. You must be registered as a Caller or Caller Agent in the RND and have an active subscription to use this connector. Registration and subscription information can be found [here](https://www.reassigned.us/querying-data). ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the Reassigned Connector in a Flow This flow demonstrates how the Reassigned connector is utilized to check if a phone number has been reassigned since a specific date, helping you identify potential security risks from phone number recycling. ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below: ![reassigned connector flow component](/assets/reassigned-flow-component.webp) To check if a phone number has been reassigned, use the **RND Reassigned Numbers / Check Number Reassignment** connector action in your flow configuration. This action queries the RND database and returns whether the phone number has been disconnected and reassigned. You need to provide the **Last Known Contact Date** in YYYY-MM-DD format (e.g., 2024-01-15). This is the date when you last had contact with the consumer or when they provided consent. Typically, use the current date when checking during sign-up or phone number updates. ![reassigned connector flow action](/assets/reassigned-flow-action.webp) The connector will return a boolean value in the `connectors.rnd-reassigned_checkReassignment` context key, indicating whether the phone number has been disconnected and reassigned. In conditions, use the `connectors.rnd-reassigned_checkReassignment.disconnected` context key to evaluate the result and block the user if the phone number has been disconnected and reassigned. This is an example of a flow that uses the Reassigned connector to check if a phone number has been disconnected and reassigned: ![reassigned connector flow](/assets/reassigned-flow.webp) # reCAPTCHA Enterprise (/connectors/connector-configuration-guides/fraud/recaptcha-enterprise) Utilize Descope's reCAPTCHA Enterprise connector for enhanced security in your applications # reCAPTCHA Enterprise Connector Use Descope's reCAPTCHA Enterprise connector to enhance the security of your authentication flows. reCAPTCHA Enterprise, a service from Google Cloud, offers comprehensive bot and online fraud protection while allowing legitimate user interactions to proceed smoothly. This guide will walk you through the configuration of the connector and its integration into your application. ## Configuration Begin by navigating to your Descope dashboard, and access the reCAPTCHA Enterprise connector configuration through: Dashboard -> Connectors -> reCAPTCHA Enterprise. You will need to fill out the following details: - **Connector Name**: Assign a meaningful name to your connector for easy identification among multiple instances. - **Connector Description**: Briefly describe the purpose of this connector. - **Project ID**: Your Google Cloud project ID where reCAPTCHA Enterprise is enabled. - **Site Key**: The site key provided by Google for the reCAPTCHA Enterprise service. - **API Key**: The [API key](https://cloud.google.com/docs/authentication/api-keys) associated with your Google Cloud project. - **Base URL**: Apply a custom URL to the reCAPTCHA Enterprise scripts. This is useful when attempting to use reCAPTCHA globally. Defaults to https://www.google.com - **Override Assessment**: This is related to overriding assessment values when performing end-to-end tests. You should not use this configuration in a production environment. For details about using this configuration, see the section below for [override assessment for testing](#override-assessment-for-testing). - **Bot Threshold (0-1)**: The bot threshold is used to determine whether the request is a bot or a human. The score ranges between 0 and 1, where 1 is a human interaction and 0 is a bot. If the score is below this threshold, the request is considered a bot. Ensure the reCAPTCHA Enterprise API is enabled in your Google Cloud project and you have generated the necessary API key. Refer to Google's documentation for detailed instructions on enabling the API and generating keys. ![reCAPTCHA Enterprise connector creation](/assets/recaptcha-enterprise-connector-creation.webp) ## Integrating reCAPTCHA Enterprise ### Screen Component Incorporate the reCAPTCHA Enterprise Privacy & Terms component in a flow screen. This initiates the CAPTCHA verification setup process for users interacting with your application. ![reCAPTCHA connector initialization](/assets/recaptcha-enterprise-connector-initialization.webp) ### Assessment and Annotation - **Assessment**: Add an "Assess" block in your flow to make a request to Google reCAPTCHA. This returns data including the user's risk scores. The main risk score (`riskAnalysis.score`) determines the likelihood of bot activity, while the SMS risk score (`riskInfo.smsRiskScore`) specifically evaluates SMS-related fraud risk. ```json { "event":{ "expectedAction":"EXPECTED_ACTION", "hashedAccountId":"ACCOUNT_ID", "siteKey":"KEY_ID", "token":"TOKEN", "userAgent":"(USER-PROVIDED STRING)", "userIpAddress":"USER_PROVIDED_IP_ADDRESS" }, "name":"ASSESSMENT_ID", "riskAnalysis":{ "reasons":[], "score":"SCORE" }, "riskInfo": { "riskScore": "SCORE", "botDetected": false, "smsRiskScore": "SCORE" }, "tokenProperties":{ "action":"USER_INTERACTION", "createTime":"TIMESTAMP", "hostname":"HOSTNAME", "invalidReason":"(ENUM)", "valid":(BOOLEAN) } } ``` ![reCAPTCHA connector assess](/assets/recaptcha-enterprise-connector-assess.webp) Since the reCAPTCHA connector runs in the browser, the connector steps need to be added after at least one **screen** or **action** component in the flow. - **Annotation**: Utilize the "Annotate" block to feed back information about the session (successful or not) to Google. This helps in refining the risk analysis for future sessions. Read more from Google [here](https://cloud.google.com/recaptcha-enterprise/docs/annotate-assessment). ```json { 'tokenProperties': { 'valid': True, 'hostname': 'www.google.com', 'action': 'homepage', 'createTime': u'2019-03-28T12:24:17.894Z' }, 'riskAnalysis': { 'score': 0.1, 'reasons': ['AUTOMATION'] }, 'event': { 'token': 'RESPONSE_TOKEN', 'siteKey': 'KEY_ID' }, 'name': 'ASSESSMENT_ID' } ``` ![reCAPTCHA connector annotate](/assets/recaptcha-enterprise-connector-annotate.webp) ### Using Assessment Data The assessment returns two different risk scores that work inversely to each other: 1. **Global Risk Score** (`riskAnalysis.score` and `riskInfo.riskScore`): Ranges from 0.0 to 1.0, where: - 1.0 indicates high confidence of legitimate human interaction - 0.0 indicates high confidence of automated/bot interaction 2. **SMS Defense Risk Score** (`riskInfo.smsRiskScore`): Ranges from 0.0 to 1.0, but works inversely: - 0.0 indicates low confidence of SMS toll fraud occurring - 1.0 indicates high confidence of SMS toll fraud occurring You can use these scores to introduce conditional logic in your flow, such as triggering additional authentication steps for users with low global risk scores or high SMS fraud risk scores. To learn more about interpreting assess values, check out Google's docs [here](https://cloud.google.com/recaptcha-enterprise/docs/interpret-assessment-website). ![reCAPTCHA connector condition](/assets/recaptcha-enterprise-connector-condition.webp) ### Example Flow View an example flow configuration that leverages reCAPTCHA Enterprise for sophisticated risk analysis and fraud prevention in your application. Check out an example flow on Descope Explorer [here](https://explorer.descope.com/?flow=recaptcha-enterprise). ![reCAPTCHA connector flow](/assets/recaptcha-enterprise-connector-flow.webp) ## Override Assessment for Testing Descope allows you to override the assessment value within the Recaptcha Enterprise connector. Overriding the assessment s helpful in a few scenarios like automated end-to-end testing where you do not want to fail on Recaptcha assessment or testing scenarios where you do want to fail Recaptcha assessment. To configure the overriding of the Recaptcha assessment within the Recaptcha Enterprise connector configuration, check the box `Override Assessment (For Testing)`. You can then set the `Assessment score`; when configured, the Recaptcha action will return the score without assessing the request. The score ranges between 0 and 1, where 1 is a human interaction, and 0 is a bot. ## Additional Resources For a deeper dive into configuring and utilizing reCAPTCHA Enterprise within Descope, refer to the additional resources and documentation provided by Google Cloud and Descope. These resources offer comprehensive guidance on setting up reCAPTCHA Enterprise for optimal security and fraud prevention in your digital applications. - [reCAPTCHA Enterprise and Descope combine no-code authentication and fraud prevention | Google Cloud Blog](https://cloud.google.com/blog/topics/partners/recaptcha-enterprise-and-descope-combine-no-code-authentication-and-fraud-prevention) - [Connector Spotlight: Fraud Prevention With Descope and Google reCAPTCHA Enterprise](https://www.descope.com/blog/post/google-recaptcha-enterprise-connector) - [reCAPTCHA Enterprise overview | Google Cloud](https://cloud.google.com/recaptcha-enterprise/docs/overview) - [Comparison of features between reCAPTCHA versions | reCAPTCHA Enterprise | Google Cloud](https://cloud.google.com/recaptcha/docs/compare-tiers) # reCAPTCHA v3 (/connectors/connector-configuration-guides/fraud/recaptcha-v3) # reCAPTCHA v3 Connector Use Descope's reCAPTCHA v3 connector to secure your authentication flow. [reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3) is Google's free service for bot protection and risk scoring. It helps distinguish legitimate users from automated traffic during authentication. This guide shows how to configure the connector, add the widget to a screen, and run the reCAPTCHA assessment in your flow. Descope also supports reCAPTCHA Enterprise. To learn more, see the [reCAPTCHA Enterprise guide](/connectors/connector-configuration-guides/fraud/recaptcha-enterprise). ## Configure reCAPTCHA v3 connector Navigate to the Connectors page in the Descope Console and select reCAPTCHA v3 to create a new reCAPTCHA v3 connector. ### Configuring the connector ![reCAPTCHA connector initialization](/assets/recaptcha-v3-connector-creation.webp) - **Connector name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector description (optional)**: Briefly explain the purpose of this connector. - **Site Key**: The site key from Google. For details, see [reCAPTCHA overview](https://developers.google.com/recaptcha/intro#recaptcha-overview). - **Secret Key**: The secret key from Google. For details, see [reCAPTCHA overview](https://developers.google.com/recaptcha/intro#recaptcha-overview). - **Override Assessment (For Testing) (optional)**: Allows for automated testing by overriding the assessment model with a custom score. Not intended for production environments, only for testing. - **Assessment Score (0-1) (optional)**: When configured, the reCAPTCHA action returns this score without assessing the request. The score ranges from 0 to 1, where 1 indicates human interaction and 0 indicates bot activity. Learn more [here](https://cloud.google.com/recaptcha/docs/interpret-assessment-website#interpret_scores). reCAPTCHA v3 is Google's free tier offering that provides basic bot detection and risk scoring. For more advanced features including SMS fraud detection, enhanced analytics, and better accuracy, consider upgrading to [reCAPTCHA Enterprise](/connectors/connector-configuration-guides/fraud/recaptcha-enterprise). View a detailed comparison [here](https://cloud.google.com/recaptcha/docs/compare-tiers). ### Testing the connector Before creating your connector, verify that the connector's configuration works. Once configured, simply click on "Test" and check the Test Results panel. Confirm, then click "Create." ## Implementing the reCAPTCHA connector in your flow ### 1. Select or Create a flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Add the reCAPTCHA connector to the flow screen In the editor, select a screen (typically the first screen), then add the reCAPTCHA widget from the `risk` section. Drag it onto the screen and click **Done**. ![reCAPTCHA connector placement in screen](/assets/recaptcha-v3-widget-placement-in-screen.webp) ### 3. Add the reCAPTCHA connector to the flow Since the reCAPTCHA connector runs in the browser, the connector steps need to be added after at least one **screen** or **action** component in the flow. In the flow editor, click the create (`+`) button, select **Connector**, and choose **reCAPTCHA v3 / Verify**. Place the connector after the screen where you added the reCAPTCHA widget, then click **Save**. ![reCAPTCHA connector placement](/assets/recaptcha-v3-connector-placement.webp) When a user runs through your authentication flow, the reCAPTCHA connector runs automatically and adds risk analysis to the `riskInfo.riskScore` context key. # Sardine (/connectors/connector-configuration-guides/fraud/sardine) Use Descope's connector to Sardine AI to evaluate login and onboarding risk using machine learning-based fraud detection. # Sardine Connector Sardine AI is a risk intelligence platform that provides real-time fraud detection and behavioral risk scoring using machine learning. It offers tools for login protection, identity verification, and user onboarding assessment. Descope's Sardine connector integrates directly with Sardine's API to evaluate user risk during login or signup. The connector enables you to receive fraud risk assessments and drive security decisions dynamically within your Descope flows. ## Setting Up the Sardine Connector To integrate the Sardine connector, follow the steps below: ### Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Sardine** from the list of connectors. ### Connector Setup Set up the required credentials provided by Sardine: * **Connector Name**: Assign a name to distinguish this connector in your flows. * **(Optional) Connector Description**: A short description of this connector's purpose. * **Client ID**: Your Sardine-issued Client ID. * **Client Secret**: Your Sardine-issued Client Secret. * **Base URL**: The Sardine API endpoint to use, such as: * `https://api.sandbox.sardine.ai` (sandbox) * `https://api.sardine.ai` (production, US) * `https://api.eu.sardine.ai` (production, EU) Ensure the credentials you provide have access to Sardine's API, and that the base URL matches your deployment region and environment. ### Test & Save * Use the `Test` button to validate your connection with test inputs. * Once successful, click `Create` to save the connector. ## Implementing the Sardine Connector Because the Sardine connector evaluates risk at runtime, it must be placed after a user input screen or action in the flow. ## Select or Create a Flow Navigate to [Flows](https://app.descope.com/flows) in the dashboard and open an existing flow or create a new one. ### Integration Click the blue plus icon in the flow builder and select **Connector**. You'll now see the available Sardine actions: ![sardine flow actions](/assets/sardine-flow-actions.webp) ## Available Actions The Sardine connector includes the following actions: ### Sardine / Login Account Takeover Check Evaluate a login attempt for potential account takeover risk. **Parameters**: * **User ID** (required): A unique identifier for the user being evaluated. * **Customer Properties**: Optional key-value pairs matching Sardine's [customer details object](https://docs.sardine.ai/guides/api-reference/customer/evaluate-customer-sessiontransaction-risk#body-customer), such as: * `email` * `phone` * `ip_address` * `device_fingerprint` * `user_agent` These properties can be passed dynamically using context keys, such as `{{user.email}}` or `{{ipAddress}}`. ### Sardine / Onboarding User Identity Check Evaluate a new sign-up attempt for fraud or risk. **Parameters**: * **User ID** (required): A unique identifier for the new user. * **Customer Properties**: Optional key-value pairs similar to those used in the login check. This action is typically used right after a registration or sign-up screen to determine whether further verification is necessary. ## Response Handling The response is saved in the connector context, e.g., `connectors.sardine_loginCheck` or `connectors.sardine_onboardingCheck`. Fields available for use in the flow include: * `decision`: The Sardine risk decision (e.g., `approve`, `review`, `decline`). * `score`: A numeric risk score assigned by Sardine. * `reason`: Explanation or factors contributing to the decision. * `recommendation`: Any follow-up action suggested by Sardine, such as requiring additional verification. You can use these values in conditional branches to determine if the user should proceed, step-up authentication, or be blocked. ## Example Flow Below is an example flow that checks a user's login risk and routes them through passkey-based MFA authentication if the decision is not `approve`. ![sardine flow example](/assets/sardine-flow-example.webp) Refer to the [Sardine API Reference](https://docs.sardine.ai/guides/api-reference/) for detailed information on supported fields and integration best practices. # Telesign (/connectors/connector-configuration-guides/fraud/telesign) Learn how to integrate Telesign Phone Number Intelligence API for risk assessment in your applications. # Telesign Connector Integrate the Telesign Phone Number Intelligence API to assess the risk associated with phone numbers. This connector leverages Telesign's technology to provide a risk score, helping you identify and mitigate potential threats. For more details on the Telesign API, refer to their [official documentation](https://developer.telesign.com/docs/phone-number-intelligence). Ensure you have your Telesign API Key and Customer ID ready by visiting [Telesign support](https://support.telesign.com/s/article/Find-Customer-ID-and-API-Key). ## Configuration To set up the Telesign Connector in your project, navigate to the [Connectors page](https://app.descope.com/connectors), select the Telesign connector, and configure the following parameters: - **Connector Name**: Assign a unique name to your connector for easy identification, particularly useful when using multiple connectors of the same type. - **Connector Description**: Summarize the purpose of your connector. - **API Key**: Your Telesign API Key. - **Customer ID**: Your Telesign Customer-ID. ### Testing It's crucial to test your connector to ensure its configuration is correct: - Utilize the **Test** button to verify the setup of your connector. This testing step confirms the connector is properly configured to analyze and score phone numbers effectively. For additional help or troubleshooting, refer to Telesign's documentation or contact Descope support. ## Using the Telesign Connector in a Flow Since the Telesign connector runs in the browser, the connector steps need to be added after at least one **screen** or **action** component in the flow. This flow shows how the Telesign connector is utilized to check the riskiness of a phone number. ![Overview of Flow Using Telesign Connector](/assets/telesign-flow-overview.webp) ### Connector Widgets in Flow Configuration #### Check Phone Number To request risk insights, set the `Request Risk Insights` parameter to `true` within the connector widget in your flow configuration. This setting enables the assessment of the phone number's risk level. ![Connector Widget in Flow Configuration](/assets/telesign-connector-widget.webp) #### Get Phone Number Information This connector can also be used to retrieve detailed information such as carrier, phone type, and risk level about a phone number with the "Get Phone Number Info" widget. Simply include the phone number, and optionally Originating IP, Account Lifecycle Event, and External ID, and the aforementioned information will be mapped to the `connectors.telesign_getPhoneInfo` key (or a key you customize). You can use a Condition Step to later refer to this key and query its data. ![Get Phone Number Information Connector Widget in Flow Configuration](/assets/telesign-widget-get-phone-number-info.png) ### Condition to Check Risk Score After obtaining the risk score, you should add a condition to your flow that checks if the risk score is greater than 600. Scores above 600 indicate a high likelihood of the phone number being involved in malicious activities. ![Condition to Check Risk Score](/assets/telesign-condition-risk-score.webp) ## Step-by-Step Guide ### 1. Add a Phone Input Screen First, add a screen with a phone input. ![Descope phone input screen](/assets/phone-input-screen.webp) ### 2. Add the "Check Phone" Connector Action to Your Flow First, integrate the "Check Phone" action from Telesign into your flow. This action will analyze the phone number and return a risk score. ### 3. Add a Condition to Evaluate the Risk Score Following the action, incorporate a condition to evaluate the risk score obtained from the Telesign connector. Use the result in the following format: `connectors.telesign_checkPhone.risk.score`. This score ranges from 0 to 1000, where 0 indicates safety and 1000 indicates a high likelihood of malicious activity. For a risk score greater than 600, Telesign recommends taking protective measures. # Traceable (/connectors/connector-configuration-guides/fraud/traceable) Leverage Descope's Traceable connector to easily add deep, contextual user behavioral data to your authentication and user journey flows. # Traceable Connector Descope's Traceable connector enables developers to add deep, contextual user behavioral data to their authentication and user journey flows. This connector combines the advanced, AI-driven behavioral intelligence of Traceable with the drag-and-drop authentication and user journeys of Descope to help developers easily add identity fraud prevention controls to their login flow. ## Setting Up The Traceable Connector To integrate the Traceable connector, follow the steps below: ### 1. Navigate to Connector * Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). * Choose **Traceable** from the list of connectors. ### 2. Connector Setup Ready your [Traceable](https://www.traceable.ai/) private API access token for integration. ![traceable connector setup](/assets/traceable-connector-setup.webp) Proceed with the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **(Optional) Connector description**: A brief description of your connector's purpose. - **Secret Key**: The Traceable secret API access token generated for the Descope service which you can gather from your Traceable's setting page. - **EU Region**: Use EU(Europe) Region deployment of Traceable platform. Default is US. ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the Traceable Connector in Your Flow Since the Traceable connector runs in the browser, the connector steps need to be added after at least one **screen** or **action** component in the flow. ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Opt for an existing flow or generate a new one. ### 2. Integration Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below: ![traceable connector flow component](/assets/traceable-flow-component.webp) Integrating that connector action inside a flow can be used in various scenarios. Here is an example of a flow that utilizes the result as follows: ![traceable connector flow component](/assets/traceable-flow-condition.webp) ![traceable connector flow condition](/assets/traceable-flow-condition-2.webp) The flow checks whether the email reputation of the user is bad or the `riskScore` is above 0.85 (this integer can be altered at your wish) and, if so, you show the user a blocked screen and block the user. If not, verify the user with an OTP and continue ahead. # Turnstile (/connectors/connector-configuration-guides/fraud/turnstile) Use Descope's Cloudflare Turnstile connector to easily add CAPTCHA protection to your authentication and user journey flows. # Turnstile Connector Turnstile is Cloudflare's smart CAPTCHA alternative that protects your site from spam and abuse. Unlike traditional CAPTCHAs, Turnstile can be embedded into any website without sending traffic through Cloudflare and works without showing visitors a CAPTCHA in most cases. ## How Turnstile Works Turnstile uses advanced risk analysis techniques to distinguish humans from bots. The service adapts the challenge outcome to each individual visitor or browser by running a series of small, non-interactive JavaScript challenges. These challenges include: - **Proof-of-work and proof-of-space**: Computational challenges that verify legitimate browser behavior - **Web API probing**: Detection of browser environment and capabilities - **Browser-quirk detection**: Analysis of human-like browser interactions and behavior patterns Based on these signals, Turnstile fine-tunes the difficulty of the challenge to the specific request, allowing most legitimate users to pass without any interaction. Only suspected bots are presented with an interactive challenge. Turnstile is WCAG 2.1 AA compliant and accessible to all users. For detailed information on Turnstile's data privacy practices, refer to the [Turnstile Privacy Addendum](https://www.cloudflare.com/turnstile-privacy-policy/). ## Setting Up the Turnstile Connector If your site enforces a Content Security Policy, you must add `challenges.cloudflare.com` to your `script-src` and `frame-src` directives for Turnstile to load. Without this, the Turnstile widget will be silently blocked by the browser. To integrate the Turnstile connector, follow the steps below: ### 1. Navigate to Connector - Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). - Choose **Turnstile** from the list of connectors. ### 2. Connector Setup Set up the necessary inputs: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: Describe what your connector is used for. - **Site Key**: The site key you get from Cloudflare. For more information on obtaining your keys, see the [Cloudflare Turnstile documentation](https://developers.cloudflare.com/turnstile/). - **Secret Key**: The secret key you get from Cloudflare. Keep this key secure and never expose it in client-side code. ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Once successful, click `Create` to save the connector. ## Implementing the Turnstile Connector in Your Flow The Turnstile connector requires at least the following SDK versions: | SDK | Minimum version | | ------------------------ | --------------- | | `@descope/web-component` | 3.69.1 | | `@descope/react-sdk` | 2.29.2 | | `@descope/angular-sdk` | 0.26.2 | | `@descope/vue-sdk` | 2.17.2 | | `@descope/nextjs-sdk` | 0.15.45 | | `@descope/web-js-sdk` | 1.50.4 | ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Integration Add the Turnstile component to the relevant screen in your flow. ![Turnstile connector component](/assets/turnstile-component.webp) Click on the blue plus sign inside the flow builder and choose "Connector". You should be able to see the new connector action as shown below. ![Turnstile connector action](/assets/turnstile-action.webp) Place the **Turnstile / Verify** command in the flow, typically immediately after the screen step. The Verify command should be used whenever you need to calculate risk with Turnstile or any CAPTCHA solution. Once you have a Turnstile connector configured after a screen step (as required), its result will be incorporated into the flow's `riskInfo.riskScore` context key. ![Turnstile connector flow](/assets/turnstile-flow.webp) The Verify command validates the Turnstile challenge token that was generated on the client side, ensuring that the user has successfully completed the Turnstile challenge. # Unibeam (/connectors/connector-configuration-guides/fraud/unibeam) Use Descope's Unibeam connector for SIM-based passwordless authentication and transaction approval using Unibeam's OnSim technology. # Unibeam Connector [Unibeam](https://unibeam.com/) is a SIM-based authentication platform that enables passwordless authentication and transaction approval using their OnSim technology. Descope's Unibeam connector allows you to authenticate users and request approvals directly through their SIM card, without requiring any app installation. ## Setting Up the Unibeam Connector To integrate the Unibeam connector, follow the steps below: ### 1. Navigate to Connector - Visit the [Connectors page](https://app.descope.com/connectors) in the Descope Console. - Select **Unibeam** from the list of connectors. ### 2. Connector Setup Enter the following information to configure the connector: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description (Optional)**: Describe what your connector is used for. - **Base URL**: Unibeam API base URL. Defaults to `https://apigw.us.unibeam.com`. - **Customer ID**: Your Unibeam customer ID. - **Client ID**: OAuth2 client ID for authentication. - **Client Secret**: OAuth2 client secret for authentication. - **HMAC Secret**: HMAC secret supplied by Unibeam for securing communications. - **(Optional) Default Message**: Default message to display when no message is provided in commands. ### 3. Test & Save - Enter a **Receiver** in the Test Configuration section and click `Test` to verify your configuration works. - Review the results in the `Test Results` panel. - Once successful, click `Create` to save the connector. ![Unibeam connector configuration](/assets/unibeam-config.webp) ## Implementing the Unibeam Connector in Your Flow ### 1. Select or Create a Flow - Navigate to the [Flows page](https://app.descope.com/flows) of the Descope console. - Select an existing flow or create a new one. ### 2. Add the Unibeam Connector to the Flow Because the Unibeam connector waits on user approval at runtime, it should be placed after the screen or action that identifies the user (e.g., after collecting their phone number). - Click the blue plus icon in the flow builder, select **Connector**, and choose either **Unibeam / Business Transaction Touch** or **Unibeam / Business Transaction Multi-Secure**. - You can configure the connector action with the **message** that will be displayed to the user for approval. ![Unibeam connector action](/assets/unibeam-action.webp) ### 3. Handle the Response The connector's response is mapped into a context key based on which command was used. For **Business Transaction Touch**, the response is mapped into `connectors.unibeam_businessTouch.status`, while for **Business Transaction Multi-Secure**, it is mapped into `connectors.unibeam_businessMultiSecure.status`. You can use a **Condition** step immediately after the connector to refer to the relevant key above and query its data to determine whether the user approved or rejected the request. For example, if the status indicates the request was approved, you can continue the flow to complete login or authorize the transaction. If it indicates the request was rejected, you can block the action or return the user to a previous screen. If the request times out or errors, you can offer a fallback authentication method or restart the flow instead. ![Unibeam connector flow example](/assets/unibeam-flow.webp) # ZeroBounce (/connectors/connector-configuration-guides/fraud/zerobounce) Use Descope's ZeroBounce connector to validate email addresses in your flows before sending OTPs or magic links. # ZeroBounce Connector [ZeroBounce](https://www.zerobounce.net/) is an email validation service that checks whether an email address is valid, deliverable, and not a spam trap. Descope's ZeroBounce connector runs that check as a step inside your authentication flows. Sending a one-time code or magic link to a fake or mistyped address produces a bounce, and repeated bounces lower the sending reputation of your email domain. Validating the address first keeps those bounces off your domain. Descope also has a built-in [Validate Email Address](/flows/actions/validate-email-action) action, which checks the address format and looks up the domain's MX records without a third-party account. It confirms that a domain can receive email, but not that the mailbox exists. Separate built-in actions check whether an address [belongs to a free email provider](/flows/conditions/free-email) or is a [disposable address](/flows/conditions/disposable-email). Reach for ZeroBounce when you need signals none of those cover, such as spam trap detection or a per-mailbox status. ZeroBounce is a paid third-party service. Each validation request consumes credits from your ZeroBounce account, and your ZeroBounce plan governs pricing and rate limits. ## Setting Up the ZeroBounce Connector To integrate the connector, follow the steps below: ### 1. Navigate to Connector - Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). - Choose **ZeroBounce** from the list of connectors. ### 2. Connector Setup Have your ZeroBounce API key ready. Enter the following information to configure the connector: - **Name**: Custom name for your connector. - **(Optional) Description**: Describe what your connector is used for. - **API Key**: The API key from your ZeroBounce account. For more information, see the [ZeroBounce API documentation](https://www.zerobounce.net/api/). - **Region**: The region your ZeroBounce account belongs to. Select **Default** for the global endpoint or **US** for the US endpoint. ![ZeroBounce connector setup](/assets/zerobounce-connector-setup.webp) ### 3. Test & Save - Validate your configuration by clicking the `Test` button and reviewing the **Test Results** tab. - Conclude the setup by selecting `Create`. ## Implementing the ZeroBounce Connector in Your Flow ### 1. Select or Create a Flow - Navigate to the [Flows page](https://app.descope.com/flows) of the Descope Console. - Select an existing flow or create a new one. ### 2. Add the Connector to the Flow Click the blue plus icon in the flow builder, select **Connector**, and choose the **ZeroBounce / Validate Email** action. Fill in the action parameters: - **Email**: The address you want to validate. Enter it manually or set it dynamically from an earlier step, for example `{{form.email}}` or `{{user.email}}`. - **(Optional) Add Activity Data**: Append ZeroBounce activity information about the address to the validation result. ![ZeroBounce flow action](/assets/zerobounce-flow-action.webp) Place the step before whichever step sends the email, so an address failing validation never triggers a send. ### 3. Handle the Response Descope maps the connector's response into the **Context Key**, which is prefilled as `connectors.zerobounce_validateEmail`. You can rename the key if you prefer. Add a **condition** step immediately after the connector to refer to this context key and query its data. The response carries the fields returned by ZeroBounce, among them: - `status` and `sub_status`: the verdict for the address, and the reason behind it when there is one. - `free_email`: whether the address belongs to a free email provider. - `domain`: the domain of the address. For example, branch on `connectors.zerobounce_validateEmail.status`. For the full list of fields and the values each one can hold, see the [ZeroBounce email validation documentation](https://www.zerobounce.net/docs/email-validation-api-quickstart/). When ZeroBounce cannot be reached or rejects the request, the step returns an error status rather than a verdict. Handle that branch in your flow so a validation outage does not lock users out of signing up. # AWS Translate (/connectors/connector-configuration-guides/localization/aws-translate) Use Descope's AWS Translate connector to support automatic localization in your authentication flow # AWS Translate Connector Descope's AWS Translate connector powers automatic localization within your authentication flow, allowing for the adaptation of language in your product to users' specific country or region. When localization is performed, Descope will take the primary attribute and drop the region-specific part. For example, a user in the `es-MX` locale will only load `es`, and Descope will translate for the user based on this locale. ## Setup ### Navigate to Connector 1. Go to your [Descope Console](https://app.descope.com) 2. Navigate: Dashboard → Connectors 3. Choose "AWS Translate" ### Configure the Connector Get your AWS Access Key ID, Secret Access Key, and Region by setting up AWS Translate [here](https://aws.amazon.com/translate/). ![localization connector initialization](/assets/localization-connector-initialization-2.webp) Complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **(Optional) Connector description**: Describe what your connector is used for. - **Access Key ID**: Your AWS access key ID. ### Test & Save - You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. - Finalize and save by clicking `Create`. ## Activating the Connector ### Navigate to Localization 1. Navigate to the [Localization](https://app.descope.com/localization) section within the Descope console. 2. Select the applicable flow you want to localize from the dropdown, then click `Configure Localization`. ![Start localization of a flow within Descope](/assets/localization-start.webp) ### Configuration Once you have clicked `Configure Localization`, you will be prompted to select the connector, source language, and target languages. When you select the dropdown for `Connector`, choose the AWS Translate connector that you previously set up. Additionally, you have the option to specify which `Target languages` you want to support for your user base, determining the languages your content will be translated into. ![Selecting a configured connector for localization of a flow in Descope](/assets/configure-connector-flow-descope.webp) Once you have clicked `Done`, the localization will automatically be generated for the flow based on the returned translation. ![An example configuration of localization with a connector within Descope (Knowledge base guide)](/assets/example-configuration-connector.webp) ![An example output of connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-output-connector.webp) ### Manually Overriding Translations When you configure localization with a connector, you can manually override the translation. When you override the translation, you will see the override highlighted like the below example. ![An example of manually overriding connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-manually-override-connector.webp) ## Testing & Customization If you'd like to test your translation, you can either simply change the language in your browser settings or configure the `locale` within the Descope component. This is an example of an updated Descope flow component to include `locale`: ```javascript ``` You can learn more in the [Web Component SDK README](https://github.com/descope/descope-js/tree/main/packages/sdks/web-component). All supported locales are accessible in your dashboard, specifically within each flow's settings where localization was enabled earlier. ![localization connector locales](/assets/localization-connector-locales.webp) # Google Cloud Translation (/connectors/connector-configuration-guides/localization/google-cloud-translation) Use Descope's Google Cloud Translation connector to support automatic localization in your authentication flow # Google Cloud Translation Connector Descope's Google Cloud Translation connector powers automatic localization within your authentication flow, allowing for the adaptation of language in your product to users' specific country or region. When localization is performed, Descope will take the primary attribute and drop the region-specific part. For example, a user in the `es-MX` locale will only load `es`, and Descope will translate for the user based on this locale. ## Setup ### Navigate to Connector 1. Go to your [Descope Console](https://app.descope.com) 2. Navigate: Dashboard → Connectors 3. Choose "Google Cloud Translate" ### Configure the Connector Get your Service Account JSON and associated Project ID from Google Cloud by following the instructions [here](https://cloud.google.com/iam/docs/service-accounts-create). ![localization connector initialization](/assets/localization-connector-initialization.webp) Complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **(Optional) Connector description**: Describe what your connector is used for. - **Project ID**: Your Google Cloud project ID. - **Service Account JSON**: Service Account JSON associated with the current project. Make sure the Cloud Translation API is enabled for your project. For more information, see [here](https://cloud.google.com/translate/docs/setup#api). ### Test & Save - You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. - Finalize and save by clicking `Create`. ## Activating the Connector ### Navigate to Localization 1. Navigate to the [Localization](https://app.descope.com/localization) section within the Descope console. 2. Select the applicable flow you want to localize from the dropdown, then click `Configure Localization`. ![Start localization of a flow within Descope](/assets/localization-start.webp) ### Configuration Once you have clicked `Configure Localization`, you will be prompted to select the connector, source language, and target languages. When you select the dropdown for `Connector`, choose the Google Cloud Translation connector that you previously set up. Additionally, you have the option to specify which `Target languages` you want to support for your user base, determining the languages your content will be translated into. ![Selecting a configured connector for localization of a flow in Descope](/assets/configure-connector-flow-descope.webp) Once you have clicked `Done`, the localization will automatically be generated for the flow based on the returned translation. ![An example configuration of localization with a connector within Descope (Knowledge base guide)](/assets/example-configuration-connector.webp) ![An example output of connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-output-connector.webp) ### Manually Overriding Translations When you configure localization with a connector, you can manually override the translation. When you override the translation, you will see the override highlighted like the below example. ![An example of manually overriding connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-manually-override-connector.webp) ## Testing & Customization If you'd like to test your translation, you can either simply change the language in your browser settings or configure the `locale` within the Descope component. This is an example of an updated Descope flow component to include `locale`: ```javascript ``` You can learn more in the [Web Component README](https://github.com/descope/descope-js/tree/main/packages/sdks/web-component). All supported locales are accessible in your dashboard, specifically within each flow's settings where localization was enabled earlier. ![localization connector locales](/assets/localization-connector-locales.webp) # Localization (/connectors/connector-configuration-guides/localization) Localization Connectors Overview # Localization Connectors Localization Connectors in Descope enable automatic translation and localization within your authentication flows. These connectors allow you to adapt the language in your product to users' specific country or region, providing a seamless multilingual experience. ## How Localization Connectors Work Localization Connectors generate translations at the end of authentication flows. When you configure localization: 1. The configured translation connector is invoked automatically 2. The connector translates your flow content into the target languages you've specified 3. Translations are stored and can be manually overridden if needed 4. Your application can display the appropriate language based on user locale You can configure localization connectors at the flow level, allowing different flows to use different translation services or settings as needed. For more information on configuring localization, see [Localization](/management/localization). When localization is performed, Descope will take the primary attribute and drop the region-specific part. For example, a user in the `es-MX` locale will only load `es`, and Descope will translate for the user based on this locale. ## Available Localization Connectors Descope supports various localization platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Lokalise (/connectors/connector-configuration-guides/localization/lokalise) Use Descope's Lokalise connector to support automatic localization in your authentication flow # Lokalise Connector Descope's Lokalise connector powers automatic localization within your authentication flow, allowing for the adaptation of language in your product to users' specific country or region. When localization is performed, Descope will take the primary attribute and drop the region-specific part. For example, a user in the `es-MX` locale will only load `es`, and Descope will translate for the user based on this locale. ## Setup ### Navigate to Connector 1. Go to your [Descope Console](https://app.descope.com) 2. Navigate: Dashboard → Connectors 3. Choose "Lokalise" ### Configure the Connector Get your Lokalise API Token and Project ID by setting up Lokalise [here](https://lokalise.com/). ![localization connector initialization](/assets/localization-connector-initialization-3.webp) Complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **(Optional) Connector description**: Describe what your connector is used for. - **API Token**: Lokalise API token. You can find it in your Lokalise Profile Settings under "API tokens" tab. - **Project ID**: Lokalise project ID. You can find it in the project settings tab. - **(Optional) Team ID**: Lokalise team ID. If not provided, the oldest available team will be used. Currently, the team ID is not displayed in the Lokalise UI, but you can find it in the list all teams API response. - **(Optional) Card ID**: The ID of the payment card to use for translation orders. If not provided, the team credit will be used. Currently, the card ID is not displayed in the Lokalise UI, but you can find it in the list all cards API response. - **(Optional) Translation Provider**: The translation provider to use ('gengo', 'google', 'lokalise', 'deepl'), default is 'deepl'. ### Test & Save - You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. - Finalize and save by clicking `Create`. ## Activating the Connector ### Navigate to Localization 1. Navigate to the [Localization](https://app.descope.com/localization) section within the Descope console. 2. Select the applicable flow you want to localize from the dropdown, then click `Configure Localization`. ![Start localization of a flow within Descope](/assets/localization-start.webp) ### Configuration Once you have clicked `Configure Localization`, you will be prompted to select the connector, source language, and target languages. When you select the dropdown for `Connector`, choose the Lokalise connector that you previously set up. Additionally, you have the option to specify which `Target languages` you want to support for your user base, determining the languages your content will be translated into. ![Selecting a configured connector for localization of a flow in Descope](/assets/configure-connector-flow-descope.webp) Once you have clicked `Done`, the localization will automatically be generated for the flow based on the returned translation. ![An example configuration of localization with a connector within Descope (Knowledge base guide)](/assets/example-configuration-connector.webp) ![An example output of connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-output-connector.webp) ### Manually Overriding Translations When you configure localization with a connector, you can manually override the translation. When you override the translation, you will see the override highlighted like the below example. ![An example of manually overriding connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-manually-override-connector.webp) ## Testing & Customization If you'd like to test your translation, you can either simply change the language in your browser settings or configure the `locale` within the Descope component. This is an example of an updated Descope flow component to include `locale`: ```javascript ``` You can learn more in the [Web Component SDK README](https://github.com/descope/descope-js/tree/main/packages/sdks/web-component). All supported locales are accessible in your dashboard, specifically within each flow's settings where localization was enabled earlier. ![localization connector locales](/assets/localization-connector-locales.webp) # Smartling (/connectors/connector-configuration-guides/localization/smartling) Use Descope's Smartling connector to support automatic localization in your authentication flow # Smartling Connector Descope's Smartling connector powers automatic localization within your authentication flow, allowing for the adaptation of language in your product to users' specific country or region. When localization is performed, Descope will take the primary attribute and drop the region-specific part. For example, a user in the `es-MX` locale will only load `es`, and Descope will translate for the user based on this locale. ## Setup ### Navigate to Connector 1. Go to your [Descope Console](https://app.descope.com) 2. Navigate: Dashboard → Connectors 3. Choose "Smartling" ### Configure the Connector Get your User Identifier, User Secret, and Account UID from your [Smartling account](https://www.smartling.com/). ![localization connector initialization](/assets/localization-connector-initialization-4.webp) Complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **(Optional) Connector description**: Describe what your connector is used for. - **User Identifier**: The user identifier for your Smartling account. - **User Secret**: The user secret for your Smartling account. - **Account UID**: The account UID for your Smartling account. ### Test & Save - You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. - Finalize and save by clicking `Create`. ## Activating the Connector ### Navigate to Localization 1. Navigate to the [Localization](https://app.descope.com/localization) section within the Descope console. 2. Select the applicable flow you want to localize from the dropdown, then click `Configure Localization`. ![Start localization of a flow within Descope](/assets/localization-start.webp) ### Configuration Once you have clicked `Configure Localization`, you will be prompted to select the connector, source language, and target languages. When you select the dropdown for `Connector`, choose the Smartling connector that you previously set up. Additionally, you have the option to specify which `Target languages` you want to support for your user base, determining the languages your content will be translated into. ![Selecting a configured connector for localization of a flow in Descope](/assets/configure-connector-flow-descope.webp) Once you have clicked `Done`, the localization will automatically be generated for the flow based on the returned translation. ![An example configuration of localization with a connector within Descope (Knowledge base guide)](/assets/example-configuration-connector.webp) ![An example output of connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-output-connector.webp) ### Manually Overriding Translations When you configure localization with a connector, you can manually override the translation. When you override the translation, you will see the override highlighted like the below example. ![An example of manually overriding connector based localization of a flow within Descope (Knowledge base guide)](/assets/example-manually-override-connector.webp) ## Testing & Customization If you'd like to test your translation, you can either simply change the language in your browser settings or configure the `locale` within the Descope component. This is an example of an updated Descope flow component to include `locale`: ```javascript ``` You can learn more in the [Web Component SDK README](https://github.com/descope/descope-js/tree/main/packages/sdks/web-component). All supported locales are accessible in your dashboard, specifically within each flow's settings where localization was enabled earlier. ![localization connector locales](/assets/localization-connector-locales.webp) # HubSpot (/connectors/connector-configuration-guides/marketing/hubspot) Harness the power of Descope's HubSpot connector to manage contacts seamlessly within your flow # HubSpot Connector Descope's HubSpot connector bridges the CRM capabilities of HubSpot within your authentication or user management flow. From creating a new contact to fetching existing contact information, integrating this connector streamlines your CRM operations. This guide offers insights into setting up the HubSpot connector and including it in your flows. The HubSpot connector relies on specific API access tokens, ensuring secure and reliable integration. ## Setting Up The HubSpot Connector To integrate the HubSpot connector, follow the steps below: ### 1. Navigate to Connector - Visit your [Descope Console](https://app.descope.com). - Follow: Dashboard -> Connectors. - Choose “HubSpot”. ### 2. Connector Setup Ready your HubSpot private API access token for integration. ![HubSpot connector setup](/assets/hubspot-connector-setup.webp) Proceed with the necessary inputs: - **Connector name**: Assign a custom name for your connector, especially useful when using multiple connectors originating from the same template. - **(Optional) Connector description**: A brief description of your connector's purpose. - **Access Token**: The HubSpot private API access token generated for the Descope service. - **Custom base URL**: The base path URL of the HubSpot API. The default is `https://api.hubapi.com`. - **Use Static IPs**: If enabled, the connector uses a predetermined pool of IPs from which all requests will be made. These IPs are displayed in the UI for you to copy and allow-list on your API gateway, firewall, or server. ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Conclude the setup process by selecting `Create`. ## Implementing the HubSpot Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Opt for an existing flow or generate a new one. ### 2. Integration Introduce the HubSpot connector blocks, either 'Get Contact', 'Create Contact', 'Create Deal', 'Create Company', or 'Create or Update Contact' into your flow based on the requirements. * **Get Contact** will require the `contact_id` to retrieve relevant information. * **Create Contact** will require you input the necessary contact details such as name, email, address, etc. * **Create Deal** will require a `Deal Name` with optional fields of amount, deal stage, pipeline, and Hubspot owner id. * **Create Company** will require a `Company Name` and `Company Domain Name` with optional fields of company attributes you can add. * **Create or Update Contact** will require you to provide the necessary contact details, such as email, first name, last name, along with any additional attributes you would like to include. Depending on the outcome of the 'Create Contact' action—success, duplication, or failure—you can then connect this block to various subsequent actions within your flow. With this integration, you can now automate Hubspot tasks within your flow, leveraging the capabilities of HubSpot seamlessly. ## (Optional) Additional Features and Considerations: For those keen on exploring further, consider: - Implementing conditions to manage flow based on 'Get Contact' results. - Utilizing other HubSpot functionalities or integrating more third-party connectors to enhance your flow. # CRM & Support (/connectors/connector-configuration-guides/marketing) Marketing Connectors Overview # CRM & Support Connectors CRM & Support Connectors in Descope enable you to integrate with CRM and customer management platforms to synchronize user data and automate marketing workflows during authentication. These connectors allow you to create contacts, update customer information, query CRM data, and manage deals directly within your authentication flows. ## How CRM & Support Connectors Work Marketing Connectors are added as steps within your Descope flows to interact with CRM and customer management platforms. When a connector step is executed, it: 1. Collects user information from the authentication flow context 2. Sends requests to the CRM platform (create, update, or query operations) 3. Receives customer data or confirmation responses 4. Makes the results available in your flow for conditional logic or further processing You can use marketing connectors to: - Create new contacts when users sign up - Update existing customer records during authentication - Query customer information to check account status or roles - Create deals or opportunities in your sales pipeline - Implement conditional logic based on customer data from your CRM ## Available CRM & Support Connectors Descope support various CRM & Support platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Intercom (/connectors/connector-configuration-guides/marketing/intercom) Integrate Intercom to manage contacts seamlessly within your Descope projects. # Intercom Connector Interact with Intercom directly from Descope using the Intercom Connector. This powerful tool allows you to add, update, and retrieve Intercom contacts through Descope flows, leveraging Intercom's comprehensive contact management capabilities. For a detailed understanding of the Intercom contact entity model, visit the [Intercom API documentation](https://developers.intercom.com/docs/references/rest-api/api.intercom.io/contacts/contact/). ## Configuration To get started with the Intercom Connector, navigate to the [Connectors page](https://app.descope.com/connectors), select the Intercom connector, and set up the following parameters: - **Connector Name**: Provide a custom name for your connector. This is useful for identifying the connector in projects where multiple connectors are used. - **Connector Description**: A brief explanation of what this connector will be used for in your project. - **Token**: The access token for interacting with the Intercom API. Follow the instructions [here](https://developers.intercom.com/docs/build-an-integration/learn-more/authentication/) to obtain your access token. - **Region**: The Intercom region your account is associated with. Options include US, EU, or AU. The default setting is US. ## Activating the Connector The Intercom connector can be used to get, create, and update contacts in Intercom within your flow. You'll simply navigate to the [flows](https://app.descope.com/flows) page in your Console and add the desired connector action ![intercom connector options](/assets/intercom-connector-options.webp) For instance, you can use the user email you received during a flow to check if a contact exists in Intercom. ![intercom get contacts config](/assets/intercom-get-contacts-config.webp) Then, you can use a condition based on the response from the connector to conduct certain actions, such as creating a contact in Intercom. ![intercom connnector in flow](/assets/intercom-connector-in-flow.webp) ![using intercom get connector id response in condition](/assets/using-intercom-get-connector-id-response-in-condition.webp) # Salesforce (/connectors/connector-configuration-guides/marketing/salesforce) Use Salesforce Connector to execute queries within your flow. This guide walks you through setting up the connector and incorporating it into your flow. # Salesforce Connector Salesforce is a cloud-based customer relationship management (CRM) service. Descope's Salesforce Connector allows you to execute SQL queries within your authentication flow, and retrieve information from a context key. This guide shows how to configure and use Descope's Salesforce Connector, as well as some example queries you may want to use in your flow. ## Items to Note - The connector requires authentication using OAuth 2.0 Client Credentials grant type. - Ensure you have the necessary permissions to access the Salesforce API. ## Example Queries for Authentication Flows Here are some example queries that might be useful in an authentication flow: ### 1. Check User's Role This query can be used to check if the user has a specific role within the organization. This can be helpful for role-based access control (RBAC). ```sql q=SELECT+Id,UserRole.Name+FROM+User+WHERE+Username+=+'user@example.com' ``` ### 2. Verify User's Account Status This query checks if the user's account is active. This can prevent inactive users from logging in. ```sql q=SELECT+Id,IsActive+FROM+User+WHERE+Username+=+'user@example.com' ``` ### 3. Retrieve User's Profile Information This query fetches detailed profile information about the user, which can be used for display purposes or further verification. ```sql q=SELECT+Id,FirstName,LastName,Email+FROM+User+WHERE+Username+=+'user@example.com' ``` ### 4. Check User's Last Login Time This query retrieves the last login time of the user. It can be used to enforce security policies or provide additional context to the user. ```sql q=SELECT+Id,LastLoginDate+FROM+User+WHERE+Username+=+'user@example.com' ``` ### 5. Fetch User's Custom Attributes If your Salesforce setup includes custom attributes, you can fetch them using a query like this. Custom attributes can be used for various conditional logic in your flow. ```sql q=SELECT+Id,CustomAttribute__c+FROM+User+WHERE+Username+=+'user@example.com' ``` Now that you're more familiar with the kinds of SQL queries you can use with this connector, let's get into the setup process. ## How to Configure
You can begin the configuration with two simple steps listed below. 1. Generate Client ID and Client Secret from your Salesforce connected app. Ensure the app has the necessary permissions to access the API. 2. Configure the connector with the required parameters, and save your configuration by clicking `Create`: ![Setup connector](/assets/salesforce-connector-setup.webp) ### Required Parameters: - **Connector name**: Custom name for your connector. - **Connector description**: Describe what your connector is used for. - **Base URL**: The Salesforce API base URL, such as `https://mydomainname.my.salesforce.com`. - **Client ID**: The consumer key of the connected app. - **Client Secret**: The consumer secret of the connected app. - **API Version**: The version of the Salesforce API to use, such as `v60.0`. ## How to Execute a Query Please note that Salesforce has a rate limit of [3600 requests per hour](https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_login.htm). If you exceed this limit, you will receive an error message. It's important to ensure you [properly handle errors](/handling-flow-errors/customizing-flow-errors) from connectors in your flow. You can use the Salesforce connector to execute a generic SQL query. This can be incorporated into your authentication flow for various use cases. To execute a query, follow these steps: 1. In your [flow](https://app.descope.com/flows), add the **_Salesforce / Query_** action block to the desired position in your flow: ![Query step](/assets/salesforce-connector-step.webp) 2. Configure the query action with the necessary SQL query. The query uses the `/services/data/v60.0/query/` endpoint. You can utilize dynamic values as well in the Connector action. Here is an example format: ```sql SELECT+Id,Account.Support_Tier__c+FROM+User+WHERE+Username+=+'{{user.email}}' ``` ![Query condition](/assets/salesforce-connector-condition.webp) And that's it! You should now be able to use Salesforce queries in your authentication flow, using Descope. # 8x8 (/connectors/connector-configuration-guides/messaging/8x8) Using Descope's connectors allows you to use 8x8 to send SMS messages with your own 8x8 account # 8x8 Connector Descope’s 8x8 connectors let you use your own 8x8 account to send messages via SMS, Viber, or WhatsApp directly within your authentication flows. This guide walks you through configuring and using each of the available 8x8 connectors. ## Configuration To configure any of the 8x8 connectors, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for 8x8, and select the connector tile that matches your use case. Below are lists of applicable settings for each of the 8x8 messaging connectors: ### 8x8 for SMS Connector - **Connector Name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: Describe what your connector is used for. - **Subaccount ID**: 8x8 Subaccount ID. To obtain this, go to the 8x8 Connect Portal / API Keys. - **API Key**: 8x8 API Key. To obtain this, go to the 8x8 Connect Portal / API Keys. - **Source**: (Optional) The Sender name (SenderId) from which the SMS is going to be sent. - **Country**: (Optional) The country code of Destination number. See 8x8 documentation for more details. - **Phone Number**: Use this number for testing. ### 8x8 for WhatsApp Connector - **Connector Name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: Describe what your connector is used for. - **Subaccount ID**: 8x8 Subaccount ID. To obtain this, go to the 8x8 Connect Portal / API Keys. - **API Key**: 8x8 API Key. To obtain this, go to the 8x8 Connect Portal / API Keys. - **Template ID**: 8x8 template ID for a WhatsApp message. These are managed in your 8x8 dashboard. Read more on templates [here](https://developer.8x8.com/connect/docs/whatsapp-templates-management). - **Country**: (Optional) The country code of Destination number. See 8x8 documentation for more details. - **Use Static IPs**: When enabled, the connector uses a predetermined pool of IPs from which all requests are made. - **Receiver**: Use this number for testing. ### 8x8 for Viber Connector - **Connector Name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: Describe what your connector is used for. - **Subaccount ID**: 8x8 Subaccount ID. To obtain this, go to the 8x8 Connect Portal / API Keys. - **API Key**: 8x8 API Key. To obtain this, go to the 8x8 Connect Portal / API Keys. - **Country**: (Optional) The country code of Destination number. See 8x8 documentation for more details. - **Use Static IPs**: When enabled, the connector uses a predetermined pool of IPs from which all requests are made. - **Receiver**: Use this number for testing. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![8x8 connector initialization](/assets/8x8-connector-creation.webp) Save your configuration by hitting `Create`. ## Using an 8x8 Connector in a Flow ### Configuring 8x8 as the Default OTP Connector To use 8x8 as the default connector for OTP authentication via SMS or Instant Messaging: 1. Go to Authentication Methods > [OTP](https://app.descope.com/settings/authentication/otp) in the Descope Console. 2. Choose the appropriate 8x8 connector: - Use the SMS connector for SMS-based OTP. - Use the Viber or WhatsApp connector for Instant Messaging OTP. You can also configure the OTP message template used with the 8x8 connector. For more information, see our [Using Email and SMS Templates](https://docs.descope.com/flows/actions/email-sms-templates-in-flows) guide. Once 8x8 is set as the default connector, any OTP verification triggered through Descope Flows or via the API/SDK will automatically use 8x8 for SMS or Instant Messaging delivery. ![Configuring 8x8 as the default OTP connector within Descope Authentication Methods](/assets/8x8-default-messaging-connector.webp) ### Using 8x8 Explicitly in Flows To override the default OTP connector within your flow to utilize 8x8, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use 8x8, select and edit the action that starts the OTP process, such as Sign In, Sign Up, or Sign Up or In with OTP via SMS. If you have not yet added the OTP action, you can do so by clicking the blue `+` icon at the top left, searching and selecting the OTP action, and then selecting and editing the initiating action. In this example, we will update the `Sign Up or In / OTP / SMS` action to utilize the 8x8 connector and override the configured default for the OTP auth method. For the 8x8 for Viber or WhatsApp connectors, the `Sign Up or In / OTP / Instant Messaging (IM)` should be used. Once in edit mode for the initiating OTP action, select the dropdown for the connector and choose your 8x8 connector. After selecting the 8x8 connector, click done, and save your flow. You can also select a customized template; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). This action within your flow will utilize the 8x8 connector and override the configured default OTP connector. ![Configuring 8x8 as the OTP connector to be used within Descope flow actions](/assets/8x8-flow-action-config.webp) ### Sending Custom Messages via 8x8 in Flows There may be a use case where you want to send a text message as part of your Descope flow that is not directly related to the OTP Auth flow; this section will describe how to use 8x8 to send custom messages. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose the 8x8 connector action that matches your use case. Click to set down the connector in your flow editor and double click the action to add required fields: - To: Who the message should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.phone}}`). As you type in the input field, options appear. - Connector: Specify which connector should be used if multiple of one type of 8x8 connector exists in the project. - Message content: Enter the content of the message. The 8x8 for WhatsApp connector does not support writing custom messages in this step. To send a custom message, a new template should be made in 8x8 and a new connector can be made with the `Template ID`. ![8x8 connector widget input](/assets/8x8-connector-placement.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after the phone number has been submitted by the user. ![8x8 connector placement](/assets/8x8-connector-placement-2.webp) Once you've completed these steps, whenever a user goes through your authentication flow, the 8x8 connector will be automatically triggered in your flow. # Adobe Campaign Classic (/connectors/connector-configuration-guides/messaging/adobe-campaign-manager) Use Descope's Adobe Campaign Classic connector to send authentication emails via Adobe Campaign Classic (ACC) as your mail server. # Adobe Campaign Classic Connector Using Descope's connectors allows you to use Adobe Campaign Classic (ACC) to send email messages with your own ACC account and trigger these actions within the authentication flow. This connector supports Adobe Campaign Classic v7 and later versions, allowing you to leverage ACC as your mail sending server. This article will guide you through setting up and using the Adobe Campaign Classic connector. Requests from this connector come from a fixed set of Descope IP addresses. If your Adobe Campaign Classic instance restricts inbound traffic by IP, allowlist these addresses so Descope's requests are not blocked. The connector's configuration screen in the Console shows the exact IPs your requests originate from, for example: `Requests from this connector will come from the following IPs: 52.210.65.163, 52.209.75.229` See [Public Static IPs](/how-to-deploy-to-production/public-static-ips) for the full, region-specific list. ### Configure Adobe Campaign Classic connector Descope uses your Adobe Campaign Classic API credentials and sender details for integration. To configure an Adobe Campaign Classic connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Adobe Campaign Classic, and select the connector tile. Configure the connector with the following parameters: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: (Optional) Describe what your connector is used for. - **Host URL**: The Adobe Campaign Classic host URL for your instance. - **Technical User**: Your Adobe Campaign Classic username for authentication. - **Password**: Your Adobe Campaign Classic password for authentication. - **Context Tag**: The context tag to be used when passing event type information to your Adobe Campaign Classic. ![Example of configuring Adobe Campaign Classic connector](/assets/acm-configure.webp) ### Testing You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. Save your configuration by hitting `Create`. ## Add your Adobe Campaign Classic connector to a flow ### Configure Adobe Campaign Classic as the Default Email Connector To use Adobe Campaign Classic for sending emails, go to [Authentication Methods](https://app.descope.com/settings/authentication/) and select the method you want to configure. Under the Email section, select the Adobe Campaign Classic connector. You can also set a default email template for your messages — see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows) for details. ![Example of configuring Adobe Campaign Classic as the default email connector](/assets/acm-auth-settings.webp) Once saved, all email messages triggered by Descope flows or API/SDK calls will be routed through Adobe Campaign Classic automatically. ### Using Adobe Campaign Classic Explicitly in Flows To use Adobe Campaign Classic for a specific flow, go to [Flows](https://app.descope.com/flows) and open the flow you want to update. Select and edit the action that initiates the email process — such as **Sign Up or In / OTP / Email** or **Sign Up or In / Magic Link / Email**. If you haven't added it yet, click the **+** icon in the top left, search for the email action, and add it before editing. Select the dropdown for the connector and choose your Adobe Campaign Classic connector. After selecting the Adobe Campaign Classic connector, click **Done**, and save your flow. You can also select a customized template; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). ![Example of configuring Adobe Campaign Classic as the email connector for a flow](/assets/acm-flow-settings.webp) This action within your flow will utilize the Adobe Campaign Classic connector and override the configured default email connector. ### Setting an External ID for Events Adobe Campaign Classic matches each event Descope sends to an existing contact record for deduplication, tracking, and audience segmentation. To enable this, add a template option with the key `rtEventExternalId` and a value that uniquely identifies the recipient, such as their login ID. See [Dynamic Content with Template Options](/management/messaging-templates#dynamic-content-with-template-options) for how to add template options through flows or SDKs. `rtEventExternalId` is a reserved key for this connector. Descope doesn't insert its value into your message content like other template options. Instead, the value goes to Adobe Campaign Classic as the event's external ID. Leave it unset and Descope sends no external ID. # Apple Push Notification (APN) (/connectors/connector-configuration-guides/messaging/apple-push-notification) Learn how to send push notifications using Apple's Push Notification service with the Descope APN connector. # Apple Push Notification (APN) Connector Send push notifications using Apple's Push Notification service. For more information about Apple Push Notifications, visit the [Apple documentation](https://developer.apple.com/documentation/usernotifications). ## Configuration To configure the Apple Push Notification connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Apple Push Notification (APN), and select the connector card. ![Apple Push Notification Connector](/assets/apple-push-notification-connector.webp) ### Settings Below is a list of applicable settings for the Apple Push Notification (APN) connector: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: Describe what your connector is used for. - **Team ID**: Your Apple Developer Team ID. You can find this in your Apple Developer account under Membership. - **Bundle ID**: The bundle identifier of your iOS app (for example, `com.example.myapp`). - **Key ID**: The key identifier for your APNs auth key. You can find this when you create an APNs auth key in your Apple Developer account. - **Private Key**: The private key content for your APNs auth key. This should be the content of your `.p8` key file. Descope uses token-based APNs authentication, so a single key works for both the development (sandbox) and production environments. ### Testing You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. For this connector, enter the device token to which the test notification will be sent. Save your configuration by hitting `Create.` ## Activating the Connector Use the Apple Push Notification (APN) connector to deliver push authentication requests on iOS. After you create the connector, select it in [Push Authentication settings](/auth-methods/push/settings). For full setup of enrollment, notification payloads, and SDK usage, see [Push Authentication](/auth-methods/push). # AWS SES (/connectors/connector-configuration-guides/messaging/aws-ses) Learn how to send emails using the AWS SES Connector within your applications. # AWS SES Connector Send emails with ease using the AWS SES Connector. This connector leverages Amazon Web Services' Simple Email Service (SES) to enable email sending capabilities. For detailed information on AWS SES, refer to the [official documentation](https://docs.aws.amazon.com/ses/latest/dg/welcome.html). Ensure you have the email address (or domain) you intend to use [verified](https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html) with AWS SES. Then, you can proceed to obtain your AWS Access Key ID and Secret Access Key from your AWS account. ## Configuration To integrate the AWS SES Connector into your project, navigate to the [Connectors page](https://app.descope.com/connectors), select the AWS SES connector, and configure the following parameters: - **Connector Name**: Assign a custom name to your connector. This is particularly useful for differentiating between multiple connectors derived from the same template. - **Connector Description**: Provide a description of what your connector is used for. - **Authentication**: Choose whether to use AWS credentials or role-based permissions to authenticate. Follow the guidance below to configure each method. - **Endpoint** (Optional): Specify an endpoint URL, which can be either the hostname alone or a fully qualified URI. - **Region**: The AWS region your requests should be directed to (e.g., `us-west-2`). - **Sender Address**: The sender email address that will appear on outgoing emails. Make sure these are registered properly in your server. - **Sender Name** (Optional): The sender name that will appear on outgoing emails. - **Tenant Name** (Optional): Sent to AWS SES as the tenant identifier for [SES Tenants](https://docs.aws.amazon.com/ses/latest/dg/tenants.html), via the `TenantName` parameter of the SES v2 API. If you set **Tenant Name**, that tenant must already exist in your AWS SES account, with your sending identity (and any configuration set or template you use) associated to it. AWS SES rejects the send if the referenced tenant doesn't exist or isn't associated with those resources. ### Authentication #### Use AWS Credentials #### Getting the Access Key 1. In AWS, navigate to Services in the top left and select IAM. On the IAM page, navigate to Users. If you don't have an IAM user, create one now. If you already have one, click on "Add Permissions". You can assign the required permissions either by adding the user to a group or by directly attaching policies to the user. For more information see [Amazon Documentation](https://docs.aws.amazon.com/console/iam/access-type). ![Adding Permissions to AWS IAM User](/assets/aws-s3-permissions.webp) Assign the following permissions policy to your SES service account. You can create a new policy with these permissions and attach it to the user: ```json title="Policy editor" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ses:SendRawEmail" ], "Resource": "*" } ] } ``` ![SES Sending Access Policy](/assets/aws-ses-sending-access-policy.webp) 2. Go to the User's page and click on "Create access key" and then on "Third-party service" if you are adding the policy directly. ![Create Access Key for AWS IAM User](/assets/aws-s3-user-page.webp) 3. Make sure to save your Secret access key as you won't be able to view it again. ![Access Key Fields in AWS S3](/assets/aws-s3-access-key.webp) 4. Insert the `Access Key` ID and secret to the Descope console. ### Use Role-Based Permissions #### Creating the Role 1. Insert the `region` and the `sender address` name. 2. Doing so will create a `Cloud Formation Stack` link: ![AWS S3 create cloud formation link](/assets/ses-connector-cloudformation-link.webp) 3. Following the link will prompt creating a stack - completing the creation will configure the role for you. ![AWS S3 create cloud formation stack](/assets/ses-cf-create-stack.webp) When creating a custom IAM role for the AWS SES connector, you must include the following trust policy to allow Descope to assume the role: **Trust Policy Requirements:** - **Principal**: `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` - **External ID**: The external ID provided by Descope during connector configuration - **Action**: `sts:AssumeRole` Your trust policy should look like this: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::312892722078:role/prod-external-role-us-east-1" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "your-external-id-from-descope" } } } ] } ``` Without this trust policy configuration, you'll receive an `is not authorized to perform: sts:AssumeRole` error when testing the connector. 4. Insert the role ARN created, e.g. `arn:aws:iam::312892722078:role/prod-external-role-us-east-1` into Descope's console. #### Troubleshooting Tips When using role-based permissions with the AWS SES connector, keep the following in mind: - **Configuration Sets**: If you're using a configuration set, make sure to grant access to it in the role-based access policy. The IAM role must have SES send permissions (for example, `ses:SendEmail` and/or `ses:SendRawEmail`) that allow use of the configuration set resource, such as `arn:aws:ses:us-east-1:123456789012:configuration-set/ExampleConfigSet`. - **Identity ARN Matching**: Ensure that the Identity ARN in your connector configuration (the SES identity for the sender email address or domain) matches an identity that the IAM role is allowed to use. For example, if you send from `no-reply@example.com`, the corresponding SES identity ARN would look like `arn:aws:ses:us-east-1:123456789012:identity/example.com`, and that ARN must be covered by the role's SES permissions. ### Dynamic Value Configuration You may want to dynamically configure sender details depending on the tenant sending the message. To do so, configure the value of **Sender Address**, **Sender Name**, or **Tenant Name** to be `{{options_}}`, replacing `` with the name of the [dynamic value](/flows/dynamic-keys) you would like to utilize. For example, you can set **Tenant Name** to `{{options_tenantName}}` to dynamically populate the tenant identifier based on the tenant sending the email. Refer to the [Template Options doc](/flows/actions/email-sms-templates-in-flows#using-template-options-dynamic-keys) to learn how to set these dynamic values when using messaging connectors with our Flows or SDKs. ![Amazon SES email config](/assets/aws-ses-connector.webp) ### Testing Before you fully integrate your connector, it's important to test its configuration: 1. Provide an email address to which a test email will be sent. 2. Use the **Test** button to send a test email. This process verifies that your connector is correctly configured and capable of sending emails through AWS SES. For any issues or further assistance, consult the AWS SES documentation or contact Descope support. ## Activating Connectors After successfully configuring the AWS SES Connector, you can utilize it in various ways depending on your application's requirements. Whether it's sending notification emails, marketing content, or transactional messages, the AWS SES Connector streamlines the email delivery process within your projects. 1. One-time Password (OTP) The first way to use the Amazon SES connector is to send email OTP messages. Simply head to Authentication Methods, click [One-time Password](https://app.descope.com/settings/authentication/otp), and choose a different Connector to send emails. ![Authentication methods OTP Amazon SNS email](/assets/aws-ses-otp.webp) You can even create custom templates to adjust the OTP message. ![authentication methods otp email Amazon SNS custom template](/assets/aws-ses-otp-template.webp) 2. Send Email Messages in Flow The second option is to add the Amazon SNS widget in the flow to send email messages. Simply navigate to the [flow](https://app.descope.com/flows) and add the connector widget. ![Amazon SNS widget in flow](/assets/aws-ses-flow.webp) # AWS SNS (/connectors/connector-configuration-guides/messaging/aws-sns) Learn how to send SMS messages using the AWS SNS Connector within your applications. # AWS SNS Connector Send SMS messages with ease using the AWS SNS Connector. This connector utilizes Amazon Web Services' Simple Notification Service (SNS) to enable SMS messaging capabilities. For comprehensive details on AWS SNS, visit the [official documentation](https://docs.aws.amazon.com/sns/latest/dg/sms_publish-to-phone.html). You'll need to make sure you have an organization phone number [registered](https://docs.aws.amazon.com/sns/latest/dg/sms_manage.html#sms_manage_console), then you can head to your account page to get your AWS Access Key ID and Secret Access Key. ## Configuration To integrate the AWS SNS Connector into your project, head to the [Connectors page](https://app.descope.com/connectors), select the AWS SNS connector, and configure the following parameters: - **Connector Name**: Assign a custom name to your connector. This is especially useful for distinguishing among multiple connectors derived from the same template. - **Connector Description**: Provide a brief description of your connector's purpose. - **Authentication**: Choose whether to use AWS credentials or role-based permissions to authenticate. - **AWS Credentials**: Enter your AWS Access Key ID and Secret Access Key. - **Role-based Permissions**: Select the role you want to use for authentication, e.g. `arn:aws:iam::312892722078:role/prod-external-role-us-east-1`. - **Endpoint** (Optional): Specify an endpoint URL. This can be either the hostname alone or a fully qualified URI. - **Region**: The AWS region to which requests should be sent (e.g., `us-west-2`). - **Sender ID** (Optional): The sender name that will appear on outgoing SMS messages. Refer to the SNS documentation for acceptable IDs and information on supported regions/countries. - **Origination Number** (Optional): The phone number from which the SMS will be sent. Ensure this number is correctly registered in your server. ![aws sns connector authentication](/assets/aws-sns-setup.webp) ### Testing Before deploying your connector, it's crucial to verify its configuration: 1. Enter a phone number to which the test SMS should be sent. 2. Use the **Test** button to initiate a test SMS message. This test ensures that your connector is properly set up and can send SMS messages successfully. For further assistance or troubleshooting, refer to the AWS SNS documentation or contact Descope support. ## Activating Connectors 1. One-time Password (OTP) Short Message Service (SMS) The first way to use the AWS SNS connector is to send OTP messages. Simply head to Authentication Methods, click [One-time Password](https://app.descope.com/settings/authentication/otp), and choose a different Connector. ![aws sns connector sms otp](/assets/aws-sns-otp.webp) 2. Send SMS Messages in Flow The second option is to add the AWS SNS widget in the flow to send SMS messages. Simply navigate to the [flow](https://app.descope.com/flows) and add the connector widget. ![aws sns flow widget config](/assets/aws-sns-flow-widget.webp) Then, you can configure the message you'd like to send and the phone number to send to. ![aws sns flow connector](/assets/aws-sns-flow-connector.webp) # Customer.io (/connectors/connector-configuration-guides/messaging/customer-io) Using Descope's Customer.io connector allows you to send authentication emails through Customer.io in your authentication flow. # Customer.io Connector Using Descope's connectors allows you to use [Customer.io](https://customer.io/), a customer engagement platform, to send transactional emails (such as OTP codes and Magic Links) in your authentication flow with your own Customer.io account. This article will guide you through setting up the connector and incorporating it into your flow. ## Configure Customer.io connector To integrate the Customer.io connector into your project, head to the [Connectors](https://app.descope.com/connectors) page, search for Customer.io, select the connector tile, and configure the following parameters: - **Name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Description**: (Optional) Describe what your connector is used for. - **Host URL**: The Customer.io transactional email API base URL (for example, `https://api.customer.io`). - **App API Key**: The Customer.io [App API key](https://docs.customer.io/integrations/api/app/) used to authenticate transactional email requests. - **From Email**: The email address from which the emails are sent. Make sure it is verified in your Customer.io account. - **From Name**: (Optional) The name of the sender to be added to the sender address. ![Descope example Customer.io connector configuration](/assets/customer-io-connector-creation.webp) ### Testing Before creating your connector, it is important to verify that the configuration works. In the **Test Configuration** section, enter a **From Email** and the **Transactional Message ID** of the Customer.io transactional message to use, then click **Test** and review the Test Results panel. When it succeeds, click **Save**. ## Using the Connector in Authentication Methods The Customer.io connector can be used to send OTP codes and Magic Links in your authentication methods. ### One-time Password (OTP) To use the Customer.io connector for email OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **Email** section, select your configured Customer.io connector from the dropdown 3. Customize your email template if needed Your OTP emails will now be sent through Customer.io. ### Magic Link To use the Customer.io connector for Magic Link emails: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **Email** section, select your configured Customer.io connector from the dropdown 3. Customize your email template if needed Your Magic Link emails will now be sent through Customer.io. ## Implementing the Customer.io connector in your flow You can also use the Customer.io connector to send custom emails within your flows. ### 1. Select or Create a flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Add the Customer.io connector to the flow screen - In the flow editor, tap the `+` icon, select **Connector**, and choose **Customer.io / Send Email**. - Click to configure the connector and fill out the fields: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. #### Using an external template Instead of composing the subject and body in Descope, you can render the email from a transactional message template that you manage in Customer.io. Enable the **Use External Template** toggle and enter the Customer.io **Transactional Message ID** in the **External Template ID** field. When **Use External Template** is enabled: - Customer.io renders the email from the referenced transactional message template, so the **Subject** and **Body** fields entered in Descope are not used. - You can still pass dynamic values from your flow (such as `{{user.email}}` in the **To** field). Any properties your Customer.io template expects should be provided as message data - see Customer.io's [transactional email](https://docs.customer.io/journeys/send/transactional/email/) documentation for the properties your template supports. Leave the toggle off to compose the email content directly in Descope using the **Subject** and **Body** fields. ![Descope example Customer.io connector configuration within flow](/assets/customer-io-connector-configuration.webp) Once configured, the Customer.io connector step runs as part of your flow: ![Descope example Customer.io connector placement within flow](/assets/customer-io-connector-placement.webp) This connector uses Customer.io's transactional email to deliver Descope authentication messages (OTP codes, Magic Links). It is separate from Customer.io's marketing features such as Journeys and broadcasts. # Firebase Cloud Messaging (FCM) (/connectors/connector-configuration-guides/messaging/firebase-cloud-messaging) Learn how to send push notifications using Google's Firebase Cloud Messaging service with the Descope FCM connector. # Firebase Cloud Messaging (FCM) Connector Send push notifications using Google's Firebase Cloud Messaging service. For more information about Firebase Cloud Messaging, visit the [Firebase documentation](https://firebase.google.com/docs/cloud-messaging). ## Configuration To configure the Firebase Cloud Messaging connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Firebase Cloud Messaging (FCM), and select the connector card. ![Firebase Cloud Messaging Connector](/assets/firebase-cloud-messaging-connector.webp) ### Settings Below is a list of applicable settings for the Firebase Cloud Messaging (FCM) connector: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: Describe what your connector is used for. - **Service Account Key**: The JSON content of your Firebase service account key file. You can generate this in the Firebase Console under *Project settings > Service accounts > Generate new private key*. Accepted formats: `.pem`, `.crt`, `.cer`, `.key`, and `.txt`. - **Notification Channel ID** (Optional): The Android notification channel ID to use for notifications. If not specified, the default channel will be used. ### Testing You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. For this connector, enter the device token to which the test notification will be sent. Save your configuration by hitting `Create.` ## Activating the Connector Use the Firebase Cloud Messaging (FCM) connector to deliver push authentication requests on Android. After you create the connector, select it in [Push Authentication settings](/auth-methods/push/settings). For full setup of enrollment, notification payloads, and SDK usage, see [Push Authentication](/auth-methods/push). # Messaging Connectors (/connectors/connector-configuration-guides/messaging) Messaging Connectors Overview # Messaging Connectors Messaging Connectors in Descope enable you to configure and use your own messaging providers instead of Descope's default messaging service. You can integrate with your preferred email, SMS, push notification, or other notification providers to deliver OTP codes, Magic Links, verification emails, push authentication requests, and custom notifications during authentication flows. ## How Messaging Connectors Work Messaging Connectors are configured in the [Connectors page](https://app.descope.com/connectors) and used in two ways: ### Authentication Method Integration Configure your messaging provider connector and select it in authentication method settings (OTP, Magic Link, Push, etc.) to deliver authentication messages. Once configured, Descope automatically uses your connector to send codes or notifications during flows. ### Flow-Based Messaging Add messaging connector steps within your flows to send custom messages. After configuring your connector, add the connector step to your flow and specify the recipient and message content. ## Messaging Templates Messaging templates are used to customize the messaging content that is sent to the user. You can read more about how to create and use email templates in our [Messaging Templates](/management/messaging-templates) guide. ## Available Messaging Connectors Descope supports various email, SMS, and notification platforms. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Infobip (/connectors/connector-configuration-guides/messaging/infobip) Using Descope's connectors allows you to use Infobip to send SMS with your own Infobip account # Infobip Connector Descope's Infobip connector allows you to send SMS from authentication flows using your own Infobip account. This article will guide you through setting up and using the Infobip connector. ## Configuration Descope uses your Infobip Base URL and API Key for integration. To configure an Infobip connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Infobip, and select the connector tile. ### Settings Below is a list of applicable settings for the Infobip connector. - **Connector name:** Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description:** (Optional) Briefly describe the purpose of this connector. - **Base URL:** Your personalized Infobip base URL. Log in to the [Infobip Portal](https://portal.infobip.com/login/) and find your account-specific base URL on your homepage/dashboard (e.g. `xxxxx.api.infobip.com`). You don't need to include a scheme — `https://` is added automatically if omitted. - **API Key:** Infobip API Key, used to authenticate Infobip's services. To obtain this, go to the [Infobip Portal](https://portal.infobip.com/login/) and navigate to API Keys. - **From:** (Optional) The sender ID or number that the end user sees at the top of an incoming message on their phone. ![Infobip connector initialization](/assets/infobip-config.png) ### Testing You can test if your connector's configuration is working properly by hitting the `Test` button and viewing the `Test Results` panel. Save your configuration by hitting `Create`. ## Using the Connector in Authentication Methods The Infobip connector can be used to send OTP codes and Magic Links in your authentication methods. ### One-time Password (OTP) To use the Infobip connector for SMS OTP messages: 1. Navigate to Authentication Methods -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the SMS section, select your configured Infobip connector from the dropdown 3. Customize your SMS template if needed Your OTP SMS will now be sent through Infobip. ### Magic Link To use the Infobip connector for Magic Link SMS: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **SMS** section, select your configured Infobip connector from the dropdown 3. Customize your SMS template if needed Your Magic Link SMS will now be sent through Infobip. ![Configuring Infobip as the default OTP connector within Descope Authentication Methods](/assets/infobip-default-sms.png) ## Using Infobip Explicitly in Flows To override the default OTP connector within your flow to utilize Infobip, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Infobip, select and edit the action that starts the OTP process, such as **Sign In**, **Sign Up**, or **Sign Up or In with OTP via SMS**. If you have not yet added the OTP action: 1. Click the blue `+` icon at the top left 2. Search for the SMS OTP action and select it 3. Select it, then select and edit the initiating action In this example, we will update the `Sign Up or In / OTP / SMS` action to utilize the Infobip connector and override the configured default for the SMS OTP auth method. Once in edit mode for the initiating OTP action: 1. Select the dropdown for the connector and choose your Infobip connector 2. Click `Done`, and save your flow You can also select a customized template; for more details, see our guide for [Using SMS Templates](/flows/actions/email-sms-templates-in-flows). This action within your flow will utilize the Infobip connector and override the configured default SMS OTP connector. ![Configuring Infobip as the OTP connector to be used within Descope flow actions](/assets/infobip-flow-action-config.png) ## Sending Custom Messages via Infobip in Flows There may be a use case where you want to send a text message as part of your Descope flow that is not directly related to the OTP Auth flow; this section will describe how to use Infobip to send custom messages. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose Infobip connector. Click to place the connector in your flow editor and double click the action to add required fields: - **To:** Who the message should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.phone}}`). As you type in the input field, options appear. - **Message:** Enter the message content of the SMS. ![Infobip connector widget input](/assets/infobip-connector-placement.png) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after the phone number has been submitted by the user. ![Infobip connector placement](/assets/infobip-flow.png) # MailerSend (/connectors/connector-configuration-guides/messaging/mailersend) Using Descope's MailerSend connector allows you to send emails through MailerSend in your authentication flow. # MailerSend Connector Using Descope's connectors allows you to use [MailerSend](https://www.mailersend.com/), a transactional email platform, to send emails in your authentication flow. This article will guide you through setting up the connector and incorporating it into your flow. ## Configure MailerSend connector ### Settings To integrate the MailerSend connector into your project, head to the [Connectors](https://app.descope.com/connectors) page, select the MailerSend connector, and configure the following parameters: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: (Optional) Describe what your connector is used for. - **API Key**: MailerSend API Key. - **From Email address**: A verified email address you own. Defines the email sender. ![MailerSend connector configuration](/assets/mailersend-connector-creation.webp) ### Testing Before creating your connector, it is important to verify that the configuration works. Click **Test** and review the Test Results panel. When it succeeds, click **Create**. ## Using the Connector in Authentication Methods The MailerSend connector can be used to send OTP codes and Magic Links in your authentication methods. ### One-time Password (OTP) To use the MailerSend connector for email OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **Email** section, select your configured MailerSend connector from the dropdown 3. Customize your email template if needed Your OTP emails will now be sent through MailerSend. ### Magic Link To use the MailerSend connector for Magic Link emails: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **Email** section, select your configured MailerSend connector from the dropdown 3. Customize your email template if needed Your Magic Link emails will now be sent through MailerSend. ## Implementing the MailerSend connector in your flow You can also use the MailerSend connector to send custom emails within your flows. ### 1. Select or Create a flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Add the MailerSend connector to the flow screen - In the flow editor, tap the `+` icon, select **Connector**, and choose **MailerSend / Send Email**. - Click to configure the connector and fill out the fields: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. ![MailerSend connector in flow](/assets/mailersend-connector-configuration.webp) # Mandrill (/connectors/connector-configuration-guides/messaging/mandrill) # Mandrill Connector Using Descope's connectors allows you to use Mandrill to send email messages with your own Mandrill account and trigger these actions within the authentication flow. This article will guide you through setting up and using the Mandrill connector. ## Configure Mandrill connector Descope uses your Mandrill API key and sender details for integration. To configure a Mandrill connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Mandrill, and select the connector tile. ### Settings Below is a list of applicable settings for the Mandrill connector. - Connector name: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - Connector Description: (Optional) Describe what your connector is used for. - API Key: Mandrill API Key. To obtain this, go to the Mandrill "SMTP & API Info" Page. - Sender address: The email address from which the emails are going to be sent. Make sure those are registered properly in your server. - Sender name: (Optional) The name of the sender to be added to sender address. - Email: Use this email for testing. ### Dynamic Value Configuration You may want to dynamically configure sender details depending on the tenant sending the message. To do so, configure the value of **Sender Address** or **Sender Name** to be `{{options_}}`, replacing `` with the name of the [dynamic value](/flows/dynamic-keys) you would like to utilize. Refer to the [Template Options doc](/flows/actions/email-sms-templates-in-flows#using-template-options-dynamic-keys) to learn how to set these dynamic values when using messaging connectors with our Flows or SDKs. ### Testing You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Mandrill connector initialization](/assets/mandrill-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your Mandrill connector to a flow ### Configure Mandrill as the Default OTP Connector To configure Mandrill as your default connector for the various OTP auth methods for email, navigate to [OTP within Authentication Methods](https://app.descope.com/settings/authentication/otp), and select the configured Mandrill connector. You can further configure the default OTP template for your Mandrill messages; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). Once you have configured Mandrill as the default connector within the OTP Auth method, Descope flows and any OTP calls via the API/SDK will utilize Mandrill for OTP verification. ![Configuring Mandrill as the default OTP connector within Descope Authentication Methods](/assets/mandrill-default-email-connector.webp) ### Using Mandrill Explicitly in Flows To override the default OTP connector within your flow to utilize Mandrill, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Mandrill, select and edit the action that starts the OTP process, such as Sign In, Sign Up, or Sign Up or In with OTP via Email. If you have not yet added the OTP action, you can do so by clicking the blue `+` icon at the top left, searching and selecting the OTP action, and then selecting and editing the initiating action. In this example, we will update the `Sign Up or In / OTP / Email` action to utilize the Mandrill connector and override the configured default for the OTP auth method. Once in edit mode for the initiating OTP action, select the dropdown for the connector and choose your Mandrill connector. After selecting the Mandrill connector, click done, and save your flow. You can also select a customized template; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). This action within your flow will utilize the Mandrill connector and override the configured default OTP connector. ![Configuring Mandrill as the OTP connector to be used within Descope flow actions](/assets/mandrill-flow-action-config.webp) ### Sending Custom Messages via Mandrill in Flows There may be a use case where you want to send an email message as part of your Descope flow that is not directly related to the OTP Auth flow; this section will describe how to use Mandrill to send custom messages. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose Mandrill. Click to set down the connector in your flow editor and double click the action to add required fields: - To: Who the message should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). As you type in the input field, options appear. - Message content: Enter the content of the email. ![Mandrill connector widget input](/assets/mandrill-connector-placement.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after the email has been submitted by the user. ![Mandrill connector placement](/assets/mandrill-connector-placement-2.webp) That's it! Whenever a user goes through your authentication flow, the Mandrill connector will be automatically triggered. # Mitto (/connectors/connector-configuration-guides/messaging/mitto) # Mitto Connector Using Descope's connectors allows you to use Mitto to send SMS with your own Mitto account. This article will guide you through setting up and using the Mitto connector. ## Configure Mitto connector Descope uses your Mitto account details such as API token, API secret, and sender information for integration. To configure a Mitto connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Mitto, and select the connector card. ### Settings Below is a list of applicable settings for the Mitto connector: - **Connector name**: A custom name for your connector. This is useful if you're creating multiple connectors from the same template. - **Connector description**: A brief description of the connector's purpose. - **API Token**: Your Mitto API token required for authentication. - **API Secret**: The secret key associated with your Mitto account. - **From**: You can define the sender of the message in the following ways: - **Phone Number**: A number associated with your Mitto account, formatted with a country code. - **Sender ID**: If a custom sender ID is configured, this will be used to send the SMS. You can test the connector configuration by clicking the `Test` button and viewing the `Test Results` panel. ![Mitto connector initialization](/assets/mitto-connector-creation.png) Once complete, save your configuration by clicking `Create.` ## Add your Mitto connector to a flow ### Configure Mitto as the Default OTP Connector To configure Mitto as your default connector for SMS-based OTP authentication, navigate to [OTP within Authentication Methods](https://app.descope.com/settings/authentication/otp) and select the configured Mitto connector. You can further customize the OTP message template for Mitto; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). Once you have configured Mitto as the default connector for OTP authentication, Descope flows and any OTP calls via the API/SDK will utilize Mitto for OTP verification. ![Configuring Mitto as the default OTP connector within Descope Authentication Methods](/assets/mitto-default-messaging-connector.png) ### Using Mitto Explicitly in Flows To override the default OTP connector in a flow to use Mitto, navigate to [Flows](https://app.descope.com/flows) and select your desired flow. Edit the action that initiates the OTP process (e.g., Sign In, Sign Up, or Sign Up or In with OTP via SMS). If the OTP action has not been added yet, you can add it by clicking the blue `+` icon at the top left, searching for the OTP action, and selecting it. In this example, we update the `Sign Up or In / OTP / SMS` action to use the Mitto connector, overriding the configured default OTP method. Once in edit mode for the OTP action, select the dropdown for the connector and choose the Mitto connector. After selecting Mitto, click done, and save your flow. You can also choose a customized template for more control; for details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). This action in your flow will now use the Mitto connector instead of the default OTP connector. ![Configuring Mitto as the OTP connector to be used within Descope flow actions](/assets/mitto-sign-up-or-in.png) ### Sending Custom Messages via Mitto in Flows There may be cases where you want to send a custom SMS that isn’t directly tied to the OTP authentication process. This section will explain how to use Mitto for custom messaging in your flows. Navigate to: [Flows](https://app.descope.com/flows), choose your flow, and tap the `+` icon to create a new action. Select **Connector**, and choose **Mitto**. Click to set the connector in your flow editor and double-click the action to add the required fields: - **To**: The recipient's phone number. You can set this dynamically based on previous flow attributes, such as `{{user.phone}}`. - **Message content**: Enter the SMS content you want to send. ![Mitto action config in flow](/assets/mitto-flow-action-config.png) Now, place your connector at the appropriate point in the flow, such as after the user submits their phone number. ![Mitto action in flow](/assets/mitto-flow-action.png) That's it! Whenever a user goes through your authentication flow, the Mitto connector will send an SMS automatically. # OneSignal (/connectors/connector-configuration-guides/messaging/onesignal) Using Descope's OneSignal connector allows you to send email and SMS messages through OneSignal in your authentication flow. # OneSignal Connector Using Descope's connectors allows you to use [OneSignal](https://onesignal.com/), a customer engagement platform, to send email and SMS messages in your authentication flow. This article will guide you through setting up the connector and incorporating it into your flow. ### Configure OneSignal connector To integrate the OneSignal connector into your project, head to the [Connectors](https://app.descope.com/connectors) page, select the OneSignal connector, and configure the following parameters: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: (Optional) Describe what your connector is used for. - **App ID**: Your OneSignal App ID. You can find this in your OneSignal dashboard under Settings -> Keys & IDs. - **API Key**: Your OneSignal REST API Key. You can find this in your OneSignal dashboard under Settings -> Keys & IDs. ![OneSignal Connector Configuration](/assets/onesignal-config.webp) ### Testing Before creating your connector, it is important to verify that the configuration works. Click **Test** and review the Test Results panel. When it succeeds, click **Create**. ## Using the Connector in Authentication Methods The OneSignal connector can be used to send OTP codes and Magic Links over email, and OTP codes over SMS, in your authentication methods. ### One-time Password (OTP) via Email To use the OneSignal connector for email OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **Email** section, select your configured OneSignal connector from the dropdown 3. Customize your email template if needed Your OTP emails will now be sent through OneSignal. ### One-time Password (OTP) via SMS To use the OneSignal connector for SMS OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **SMS** section, select your configured OneSignal connector from the dropdown 3. Customize your SMS template if needed Your OTP SMS messages will now be sent through OneSignal. ### Magic Link To use the OneSignal connector for Magic Link emails: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **Email** section, select your configured OneSignal connector from the dropdown 3. Customize your email template if needed Your Magic Link emails will now be sent through OneSignal. ## Implementing the OneSignal connector in your flow You can also use the OneSignal connector to send custom email and SMS messages within your flows. ### 1. Select or Create a flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Add the OneSignal connector to the flow screen - In the flow editor, tap the `+` icon, select **Connector**, and choose **OneSignal / Send Email** or **OneSignal / Send SMS**. - Click to configure the connector and fill out the fields: For **Send Email**: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. ![OneSignal Send Email](/assets/onesignal-send-email.webp) For **Send SMS**: - **To**: Who the SMS should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.phone}}`). - **Message**: Enter the content of the SMS. # Postmark (/connectors/connector-configuration-guides/messaging/postmark) Using Descope's Postmark connector allows you to send transactional emails through your Postmark account in your authentication flow. # Postmark Connector Using Descope's connectors allows you to use Postmark, a fast and reliable email delivery service designed for transactional emails, in your authentication flow. This article will guide you through setting up the connector and incorporating it into your flow. ## Configure Postmark connector Descope uses your Postmark Server API Token and Sender address to integrate with the service. Start at your [dashboard](https://app.descope.com/connectors). Navigate to the Postmark connector: Dashboard -> Connectors -> Postmark Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: (Optional) Describe what your connector is used for. - **Server API Token**: The Server API Token from your Postmark server. You can find this in your Postmark account under Servers -> API Tokens. - **Message Stream ID**: The ID of the message stream to use for the emails. - **From Email Address**: The email address that will appear in the 'From' field of the sent email. ![Descope example Postmark connector configuration](/assets/postmark-connector-creation.webp) ### Testing You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. Save your configuration by hitting `Create.` ## Using the Connector in Authentication Methods The Postmark connector can be used to send OTP codes and Magic Links in your authentication methods. ### One-time Password (OTP) To use the Postmark connector for email OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **Email** section, select your configured Postmark connector from the dropdown 3. Customize your email template if needed Your OTP emails will now be sent through Postmark. ### Magic Link To use the Postmark connector for Magic Link emails: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **Email** section, select your configured Postmark connector from the dropdown 3. Customize your email template if needed Your Magic Link emails will now be sent through Postmark. ## Add your Postmark connector to a flow You can also use the Postmark connector to send custom emails within your flows. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose Postmark. Click to set down the connector in your flow editor and fill out the fields: - **Recipient**: The email address of the recipient. Setting this dynamically based on previous attributes is recommended (eg. `{{form.email}}`). - **Subject**: The subject of the email. - **Body**: The body text of the email. ![Postmark connector widget input](/assets/postmark-connector-placement.webp) # Salesforce Marketing Cloud (SFMC) (/connectors/connector-configuration-guides/messaging/salesforce-marketing-cloud) Using Descope's Salesforce Marketing Cloud connector allows you to send emails using your SFMC account in your authentication flow. # Salesforce Marketing Cloud Connector Using Descope's connectors allows you to use Salesforce Marketing Cloud (SFMC), a comprehensive digital marketing platform, to send emails in your authentication flow. This article will guide you through setting up the connector and incorporating it into your flow. ## Configure Salesforce Marketing Cloud connector ### Configuring the connector To integrate the Salesforce Marketing Cloud Connector into your project, head to the [Connectors](https://app.descope.com/connectors) page, select the Salesforce Marketing Cloud connector, and configure the following parameters: - **Connector name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector description (optional)**: Briefly explain the purpose of this connector. - **Subdomain**: Your Salesforce Marketing Cloud subdomain only (without the full URL). - **Client ID**: The consumer key of the API integration. - **Client Secret**: The consumer secret of the API integration. - **Scope (optional)**: Space-separated list of data-access permissions for your connector. - **Account ID (optional)**: Account identifier, or MID, of the target business unit. ![Salesforce Marketing Cloud connector configuration](/assets/sfmc-connector-creation.webp) ### Testing Before creating your connector, its important to verify if the connector configurations works. For this simply click on **Test** and view the Test Results panel. Confirm it works and click **Create**. ## Using the Connector in Authentication Methods The Salesforce Marketing Cloud connector can be used to send OTP codes and Magic Links in your authentication methods. ### One-time Password (OTP) To use the SFMC connector for email OTP messages: 1. Navigate to **Authentication Methods** -> [One-time Password](https://app.descope.com/settings/authentication/otp) 2. In the **Email** section, select your configured Salesforce Marketing Cloud connector from the dropdown 3. Customize your email template if needed Your OTP emails will now be sent through Salesforce Marketing Cloud. ### Magic Link To use the SFMC connector for Magic Link emails: 1. Navigate to **Authentication Methods** -> [Magic Link](https://app.descope.com/settings/authentication/magiclink) 2. In the **Email** section, select your configured Salesforce Marketing Cloud connector from the dropdown 3. Customize your email template if needed Your Magic Link emails will now be sent through Salesforce Marketing Cloud. ## Implementing the Salesforce Marketing Cloud connector in your flow You can also use the Salesforce Marketing Cloud connector to send custom emails within your flows. ### 1. Select or Create a flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Add the Salesforce Marketing Cloud connector to the flow screen - In the flow editor, tap the `+` icon, select **Connector**, and choose **Salesforce Marketing Cloud / Send Email** or **Salesforce Marketing Cloud / Upsert Rowset** based on your use case. - **Send Email**: Use this to send a custom email to the user. - **Upsert Rowset**: Use this to upsert a rowset to Salesforce Marketing Cloud. - Click to configure the connector and fill out the fields: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended. - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. ![Salesforce Marketing Cloud connector configuration](/assets/salesforce-marketing-cloud-connector-configuration.webp) # SendGrid (/connectors/connector-configuration-guides/messaging/sendgrid) Using Descope's SendGrid connector allows you to send emails without having to maintain email servers in your authentication flow. # SendGrid Connector Using Descope's connectors allows you to use SendGrid, a cloud-based SMTP provider that allows you to send emails without having to maintain email servers, in your authentication flow. This article will go through setting up the connector and including it in your flow. ## Configure SendGrid connector Descope uses your SendGrid API Key and Sender address to integrate with our service. Start at your [dashboard](https://app.descope.com/connectors). Navigate to the SendGrid connector: Dashboard -> Connectors -> SendGrid Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector description**: (Optional) Describe what your connector is used for. - **API Key**: The API key used to authenticate with SendGrid services. - **Region**: The region of the SendGrid API to use (Global or EU). - **Sender address**: The email address from which the emails are going to be sent. Make sure those are registered properly in your server. - **Sender name**: (Optional) The name of the sender to be added to sender address. ### Dynamic Value Configuration You may want to dynamically configure sender details depending on the tenant sending the message. To do so, configure the value of **Sender Address** or **Sender Name** to be `{{}}`, replacing `` with the name of the [dynamic value](/flows/dynamic-keys) you would like to utilize. Refer to the [Template Options doc](/flows/actions/email-sms-templates-in-flows#using-template-options-dynamic-keys) to learn how to set these dynamic values when using messaging connectors with our Flows or SDKs. ### Testing You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Descope example sendgrid connector configuration](/assets/sendgrid-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your SendGrid connector to a flow Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose SendGrid. Click to set down the connector in your flow editor and fill out the fields: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). As you type in the input field, options appear. - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. - **Use External Template**: (Optional) Enable this to use a SendGrid template instead of the subject and body fields. - **External Template ID**: (Optional) If `Use External Template` is enabled, enter the SendGrid template ID here. This will override the subject and body fields. ![Descope example sendgrid connector configuration within flow](/assets/sendgrid-connector-placement-configuration.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after email has been submitted by the user. ![SendGrid connector placement](/assets/sendgrid-connector-placement.webp) That's it! Whenever a user goes through your authentication flow, the SendGrid connector will be automatically triggered. ## Using SendGrid connector with Email Based Authentication When using email-based authentication methods like Magic Link, Enchanted Link, or Email OTP, you can use the SendGrid connector to send the email to the user. To do so, choose `SendGrid` for the connector in the corresponding action in your flow. You can also use SendGrid email templates instead of the default Descope messaging templates when using the SendGrid connector with email-based authentication. To do so, select `Referenced SendGrid Template ID` from the `Template` dropdown and provide the SendGrid template ID in the `External Template ID` field. In order to pass dynamic values to your SendGrid template, add a row to `Template Dynamic Keys` and provide the key and value. The key should match the dynamic variable in your SendGrid template, and the value can be a static value or a dynamic value from your flow. ![SendGrid template in enchanted link flow action](/assets/sendgrid-template-in-enchanted-link-flow-action.webp) ## Using SendGrid Connector for User Invites You can also use a SendGrid external template for [user invitation](/management/user-management/invite-users) emails. Configure SendGrid as the invitation connector in [Project Settings](https://app.descope.com/settings/project) under **Sign Ups and User Invitations**, then select `Referenced SendGrid Template ID` from the `Template` dropdown and provide the SendGrid template ID in the `External Template ID` field — the same as with email-based authentication methods. To pass dynamic values into the invite template, add a row to `Template Dynamic Keys` for each value you want to inject. The **Key** should match the dynamic variable name in your SendGrid template (e.g. `inviteId`), and the **Value** should be `{{options_}}` using that same name (e.g. `{{options_inviteId}}`). That key then resolves to whatever you pass in `templateOptions` when inviting the user via the [Management SDK](/management/user-management/sdks#invite-user) — for example, `templateOptions: { inviteId: '123' }` makes `123` available as the `inviteId` substitution tag in your SendGrid dynamic template. # Slack (/connectors/connector-configuration-guides/messaging/slack) # Slack Connector Using Descope's connectors allows you to use Slack to send messages to a slack channel of your choice and trigger these actions within the authentication flow. This article will guide you through setting up and using the Slack connector. ## Configure Slack connector To configure a Slack connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Slack, and select the connector tile. ### Settings Below is a list of applicable settings for the Slack connector. - Connector name: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - Connector Description: (Optional) Describe what your connector is used for. - Token: The OAuth token for Slack's Bot User, used to authenticate API requests. To create a token: 1. Navigate to the Slack App configuration page of the relevant app here, and select OAuth & Permissions. ![Slack connector config](/assets/slack-config.webp) 2. Click `Install to Workspace`. 3. Copy `Bot User OAuth Token`. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Slack connector initialization](/assets/slack-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your Slack connector to a flow To utilize Slack within your flow, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Slack, tap the create button (designated via the `+` icon), select Connector, and choose Slack. Enter all the fields required: - Channel ID: ID of the slack channel you are connecting to. Get this from the channel details by click on the 3 dots, click on `Open channel details` and grab the id displayed at the bottom. - Message: Message content being sent to the channel. This field also accepts dynamic values for example `{{user.name}}` as shown in the below screenshot. ![Configuring Slack as connector to be used within Descope flow actions](/assets/slack-flow-action-config.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow for the message to be sent. ![Slack as connector within Descope flow](/assets/slack-flow.webp) That's it! Whenever a user goes through your authentication flow, the slack connector will be automatically triggered and will send the message to the connected channel. # Generic SMS Gateway (/connectors/connector-configuration-guides/messaging/sms-gateway) Use Descope's Generic SMS Gateway connector to send SMS messages through your own API. # Generic SMS Gateway Connector Descope's **Generic SMS Gateway** connector allows you to send SMS messages using your own API. This guide walks you through setting up and using the Generic SMS Gateway connector within your Descope project. ## Configure Generic SMS Gateway Connector To integrate your SMS Gateway, you'll need your **Post Message URL**, **authentication details**, and the sender information. 1. Go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console. 2. Search for **Generic SMS Gateway** and select the connector tile. ### Settings Below are the required settings for configuring the Generic SMS Gateway connector: - **Connector Name:** A custom name for your connector. Useful for managing multiple connectors. - **Connector Description:** A brief description of this connector’s purpose. - **Post Message URL:** The API endpoint where Descope will send SMS requests. - **Authentication:** - **API Key or Token**: Provide the authentication method required by your SMS gateway. - **Use Static IPs**: If enabled, the connector uses a predetermined pool of IPs from which all requests will be made. These IPs are displayed in the UI for you to copy and allow-list on your API gateway, firewall, or server. - **Sender:** - A phone number or string of numbers that will appear as the sender of the SMS. Once configured, you can test the connection using the **Test** button. Review the **Test Results** panel to verify successful integration. ![Generic SMS Gateway Connector Setup](/assets/generic-sms-gateway-setup.webp) Click **Create** to save the configuration. ## SMS Payload Schema Descope sends SMS messages to your API using the following schema. Ensure your API is set up to handle this format: ```json { "recipient": "+1 399 999", "body": "Your own template", "sender": "+1 234 567", "token": "Just the code itself" } ``` - **recipient:** The phone number receiving the SMS (in international format). - **body:** The content of the SMS message. - **sender:** The phone number or identifier sending the message. - **token:** The dynamic token/code to be sent (e.g., OTP). ## Using Your Generic SMS Gateway Connector ### Sending OTP/Magic Link SMS Messages To use your Generic SMS Gateway for your OTP / Magic Link authentication method messages: 1. Navigate to [Authentication Methods](https://app.descope.com/settings/authentication), and select either OTP or Magic Link. 2. Select your configured **Generic SMS Gateway** connector. 3. Define your **OTP Template** for messages. For details on template creation, see [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). All OTP flows and API/SDK OTP calls will now utilize your SMS Gateway for message delivery. ### Sending Custom SMS Messages To send non-OTP/Magic Link SMS messages: 1. Open [Flows](https://app.descope.com/flows) and select a flow. 2. Click the **+** icon to add a new action. 3. Select **Connector > Generic SMS Gateway**. 4. In the action settings: - **To:** Recipient's phone number (e.g., `{{user.phone}}`). - **Message Content:** The custom message you want to send. Link this action logically in your flow (e.g., after a user completes a specific step). ![Sending Custom SMS](/assets/generic-sms-custom-message.webp) ## Handling Errors with the Generic SMS Gateway If your API returns errors (e.g., invalid recipient or server issues), Descope can capture and handle these within your flows. For example, if your API returns a "number blocked" error, you can: 1. Use Descope’s error handling features to catch the error. 2. Display a custom error message or retry the action. For more on error handling, see [Customizing Flow Errors](/handling-flow-errors/customizing-flow-errors). ![Error Handling for SMS Gateway](/assets/generic-sms-error-handling.webp) # SMTP (/connectors/connector-configuration-guides/messaging/smtp) # SMTP Connector Using Descope's connectors allows you to use SMTP to send emails with your own email servers and trigger sending within the authentication flow. This article will go through setting up and using the connector. ## Configure SMTP connector Descope uses your server hostname, SMTP port, username, password, and sender address for integration. Start at your [dashboard](https://app.descope.com/connectors). Navigate to the SMTP connector: Dashboard -> Connectors -> SMTP Now, complete the required fields: - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Custom base URL**: The base URL of the Segment API, when using a custom domain in Segment. - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Server hostname**: The SMTP server's hostname. Can be either domain and IP. - **SMTP port**: The port in which the SMTP service is configured to. Default value is 25. - **Username**: The username used for authenticating with the SMTP server. - **Password**: The password used for authenticating with the SMTP server. - **Sender address**: The email address from which the emails are going to be sent. Make sure those are registered properly in your server. - **Sender name (Optional)**: The name of the sender to be added to sender address. - **Connector description**: Describe what your connector is used for. - **Use Static IPs**: If enabled, the connector uses a predetermined pool of IPs from which all requests will be made. These IPs are displayed in the UI for you to copy and allow-list on your API gateway, firewall, or server. ### Dynamic Value Configuration You may want to dynamically configure sender details depending on the tenant sending the message. To do so, configure the value of **Sender Address** or **Sender Name** to be `{{options_}}`, replacing `` with the name of the [dynamic value](/flows/dynamic-keys) you would like to utilize. Refer to the [Template Options doc](/flows/actions/email-sms-templates-in-flows#using-template-options-dynamic-keys) to learn how to set these dynamic values when using messaging connectors with our Flows or SDKs. ### Testing You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Descope example SMTP connector configuration](/assets/smtp-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your SMTP connector to a flow Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose SMTP. Click to set down the connector in your flow editor and fill out the fields: - **To**: Who the email should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.email}}`). As you type in the input field, options appear. - **Subject**: Enter the subject of the email. - **Body**: Enter the body text of the email. ![SMTP connector widget input](/assets/smtp-connector-placement.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after email has been submitted by the user. ![Descope example SMTP connector within Flow](/assets/smtp-connector-placement-2.webp) That's it! Whenever a user goes through your authentication flow, the SMTP connector will be automatically triggered. # Telesign Messaging (/connectors/connector-configuration-guides/messaging/telesign-messaging) # Telesign Messaging Connector Using Descope's connectors allows you to use Telesign to send SMS with your own Telesign account and trigger it within the authentication flow. This article will guide you through setting up and using the Telesign Messaging connector. ## Configure Telesign Messaging Connector Descope uses your Telesign Customer ID and API key for integration. To configure a Telesign Messaging connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Telesign Messaging, and select the connector tile. ### Settings Below is a list of applicable settings for the Telesign Messaging connector. - Connector name: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - Connector Description: Briefly describe the purpose of this connector. - Customer ID: Telesign Customer ID. To obtain this, go to the [Telesign Portal](https://portal.telesign.com/). - API Key: Telesign API Key, used to authenticate Telesign's services. To obtain this, go to the [Telesign Portal](https://portal.telesign.com/). - Sender ID (Optional): A Telesign approved sender ID is the number or name that the end user sees at the top of an incoming message on their phone. - Template ID (Optional): The ID of the DLT template used for this message (only for sending SMS messages to recipients in India). - Entity ID (Optional): The ID of the entity sending this message (only for sending SMS messages to recipients in India). You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Telesign Messaging connector initialization](/assets/telesign-messaging-configuration-screen.webp) Save your configuration by hitting `Create.` ## Add your Telesign Messaging connector to a flow ### Configure Telesign Messaging as the Default OTP Connector To configure Telesign Messaging as your default connector for SMS OTP, navigate to [OTP within Authentication Methods](https://app.descope.com/settings/authentication/otp), and select the configured Telesign Messaging connector. You can further configure the default OTP template for your Telesign messages; for more details, see our guide for [Using SMS Templates](/flows/actions/email-sms-templates-in-flows). Once you have configured Telesign Messaging as the default connector within the OTP Auth method, Descope flows and any OTP calls via the API/SDK will utilize Telesign Messaging for OTP verification. ![Configuring Telesign Messaging as the default OTP connector within Descope Authentication Methods](/assets/telesign-messaging-default-sms.webp) ### Using Telesign Messaging Explicitly in Flows To override the default OTP connector within your flow to utilize Telesign Messaging, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Telesign Messaging, select and edit the action that starts the OTP process, such as Sign In, Sign Up, or Sign Up or In with OTP via SMS. If you have not yet added the OTP action, you can do so by clicking the blue `+` icon at the top left, searching and selecting the SMS OTP action, and then selecting and editing the initiating action. In this example, we will update the `Sign Up or In / OTP / SMS` action to utilize the Telesign Messaging connector and override the configured default for the SMS OTP auth method. Once in edit mode for the initiating OTP action, select the dropdown for the connector and choose your Telesign Messaging connector. After selecting the Telesign Messaging connector, click done, and save your flow. You can also select a customized template; for more details, see our guide for [Using SMS Templates](/flows/actions/email-sms-templates-in-flows). This action within your flow will utilize the Telesign Messaging connector and override the configured default SMS OTP connector. ![Configuring Telesign Messaging as the OTP connector to be used within Descope flow actions](/assets/telesign-messaging-flow-action-config.webp) ### Sending Custom Messages via Telesign Messaging in Flows There may be a use case where you want to send a text message as part of your Descope flow that is not directly related to the OTP Auth flow; this section will describe how to use Telesign Messaging to send custom messages. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose Telesign Messaging connector. Click to set down the connector in your flow editor and double click the action to add required fields: - To: Who the message should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.phone}}`). As you type in the input field, options appear. - Message content: Enter the content of the SMS. ![Telesign Messaging connector widget input](/assets/telesign-messaging-connector-placement.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after the phone number has been submitted by the user. ![Telesign Messaging connector placement](/assets/telesign-messaging-flow.webp) That's it! Whenever a user goes through your authentication flow, the Telesign Messaging connector will be automatically triggered. # Twilio Verify (/connectors/connector-configuration-guides/messaging/twilio-verify) Learn how to configure Descope's Twilio Verify connector for user authentication through voice, SMS, and email OTP verification # Twilio Verify Connector Descope's Twilio Verify connector allows you to utilize the Twilio Verify SDK and service to authenticate users via Voice, SMS, and email for OTP rather than utilizing Descope OOTB OTP verification functions. This article will guide you through setting up and using the Twilio Verify connector. You cannot customize the OTP template within Descope when utilizing the Twilio Verify connector. You must rely on the configured templates within your Twilio Verify account. ## Configure Twilio Verify Connector Descope uses your Twilio Verify Account SID, Service SID, and the API Key and Secret or the Auth token to authenticate the connector to the Twilio Verify API. To configure the Twilio Verify connector, navigate to the [Connectors page](https://app.descope.com/connectors) within the Descope console, search for `Twilio Verify`, and click the tile. ### Settings Below is the list of settings and configurations available that you can customize for your Twilio Verify Connector. - **Connector name**: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - **Connector Description**: Briefly describe the purpose of this connector. - **Account SID**: A unique key provisioned for Twilio customers that acts as a username. - **Service SID**: A unique key that identifies the Twilio Verify service. - **Authentication**: There are two ways of authenticating with Twilio service: - **Auth token**: The authentication token associated with the Account SID. - **API key and secret**: The preferred way to authenticate to Twilio's services; consist of a SID and a secret. - **From Email address (only for email verification)**: A verified Twilio (Sendgrid) email address you own. Defines the email sender. - **Sender Name (only for email verification)**: The human-readable name displayed as the sender of verification emails. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. ![Configuring and testing Descope's Twilio Verify connector](/assets/twilio-verify-connector-creation.webp) Save your configuration by hitting `Create.` ## Using Twilio Verify in Auth Methods and Flows ### Configure Twilio Verify as the Default OTP Connector To configure Twilio Verify as your default connector for the various OTP auth methods for SMS, Voice, and Email, navigate to [OTP within Authentication Methods](https://app.descope.com/settings/authentication/otp), and select the configured Twilio Verify connector. Note that you will see the templates option disappear because you cannot customize the OTP template within Descope when utilizing the Twilio Verify connector. You must rely on the configured templates within your Twilio Verify account. Once you have configured Twilio Verify as the default connector within the OTP Auth method, Descope flows and any OTP calls via the API/SDK will utilize Twilio Verify for OTP verification. ![Configuring Twilio Verify as the default OTP connector within Descope Authentication Methods](/assets/twilio-verify-otp-connector.webp) ### Using Twilio Verify Explicitly in Flows To override the default OTP connector within your flow to utilize Twilio Verify, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Twilio Verify, select and edit the action that starts the OTP process, such as Sign In, Sign Up, or Sign Up or In with OTP via SMS, Email, or Voice. If you have not yet added the OTP action, you can do so by clicking the blue `+` icon at the top left, searching and selecting the OTP action, and then selecting and editing the initiating action. In this example, we will update the `Sign Up or In / OTP / SMS` action to utilize the Twilio Verify connector and override the configured default for the OTP auth method. Once in edit mode for the initiating OTP action, select the dropdown for the connector and choose your Twilio Verify connector. After selecting the Twilio Verify connector, click done, and save your flow. Note that you will see the templates option disappear because you cannot customize the OTP template within Descope when utilizing the Twilio Verify connector. You must rely on the configured templates within your Twilio Verify account.This action within your flow will utilize the Twilio Verify connector and override the configured default OTP connector. ![Configuring Twilio Verify as the default OTP connector within Descope Authentication Methods](/assets/twilio-verify-flow-connector.webp) ## Localized Twilio Verify Messages Twilio Verify allows you to send messages in different languages based on the supplied locale. Descope integrates with this functionality by utilizing the browser's locale or the locale configured within the Descope front-end SDK component when sending the request to Twilio Verify. If you want the user's browser to be used when sending messages, no configuration is necessary; Descope will automatically capture the locale and send it to Twilio Verify to send the message in the correct language. If you want to override the language to send the message, you can set the locale within the Descope component, like the example below. ```html { navigate("/"); console.log("Logged in!"); }} onError={(e) => console.log("Error!")} locale="es-MX" // override the locale to be es-MX which will enforce sending the message in Spanish /> ``` ## Handling Twilio Blocked Calls/SMS Twilio may block voice calls to certain numbers; this results in [Twilio error code 21216](https://www.twilio.com/docs/api/errors/21216). Essentially, the number is blocked by Twilio due to: - The destination has a high-risk of fraud - Due to regulatory reasons, the destination cannot be reached - You are placing a call to a +1 destination and your account is missing a Primary Customer Profile Descope allows you to capture this returned error from Twilio and handle it how you would like. See the example image below; however, for further details on handling errors within flows, see [this guide](/handling-flow-errors/customizing-flow-errors). ![Handling Twilio Verify blocking calls within flow errors](/assets/provider-blocked-calls-error-handling.webp) A similar error handling in Descope is also available for sms. In scenarios where a number has been blocked by twilio due to the reasons mentioned above, the error can be captured as a part of "SMS blocked by provider" and handled as required. ![Handling Twilio Verify blocking sms within flow errors](/assets/provider-blocked-sms-error-handling.webp) ## WhatsApp OTP using Twilio Verify By using the Twilio Verify connector, Descopers can utilize WhatsApp functionality to send OTPs (one-time passwords) instead of the standard SMS option. This feature is available both at the project level and at the flow step level. Once you have configured the Twilio Verify connector as shown above and verified that it is working, navigate to Authentication Methods in the console for project-level configuration and click on One-time Password. Scroll down to the new subcategory section named 'Instant Messaging' in the configuration. There is no need to configure a message template. ![IM in Authentication Methods](/assets/instant-messaging.webp) To implement this in a flow, click on the flow where you want to add this feature. Add new actions specific to Instant Messaging, and ensure that your Twilio Verify connector is selected in the action you add. ![IM actions in flow](/assets/instant-messaging-actions.webp) ![Action Config for IM](/assets/instant-messaging-config-in-actions.webp) ![IM in flow](/assets/instant-messaging-flow.webp) That's it! Once you run the flow and provide your phone number, an OTP will be sent to WhatsApp, which can then be validated to complete the flow. # Twilio (/connectors/connector-configuration-guides/messaging/twilio) # Twilio Connector Using Descope's connectors allows you to use Twilio to send SMS and make voice calls with your own Twilio account and trigger these actions within the authentication flow. This article will guide you through setting up and using the Twilio connector. ## Configure Twilio connector Descope uses your Twilio Account SID, Authentication token, API key, secret and sender details for integration. To configure a Twilio connector, go to the [Connectors](https://app.descope.com/connectors) page within the Descope Console, search for Twilio, and select the connector tile. ### Settings Below is a list of applicable settings for the Twilio connector. - Connector name: Custom name for your connector. This will come in handy when creating multiple connectors from the same connector template. - Connector Description: Briefly describe the purpose of this connector. - Account SID: A unique key provisioned for Twilio customers that acts as a username. - Authentication: There are two ways of authenticating with Twilio service: - Auth token: The authentication token associated with the Account SID. - API key and secret: The preferred way to authenticate to Twilio's services; consist of a SID and a secret. - From: There are two ways to define the sender of the message: - Phone Number: A Twilio number you own. Must start with a country code. - Messaging Service SID: If a messaging service is configured, one of the numbers in that service pool will be used. And if you'd like, the optional field: - From Phone number (for voice calls): A Twilio number you own. Defines the sender of the voice call. Must start with a `+` and country code. You can test if your connector's configuration is working properly simply by hitting the `Test` button and viewing the `Test Results` panel. For sending SMS messages to numbers outside of the US, you must configure [SMS Geo Permissions](https://www.twilio.com/docs/messaging/guides/sms-geo-permissions) in the [Twilio admin console](https://www.twilio.com/console/sms/settings/geo-permissions). ![Twilio connector initialization](/assets/twilio-connector-creation.webp) Save your configuration by hitting `Create.` ## Add your Twilio connector to a flow ### Configure Twilio as the Default OTP Connector To configure Twilio as your default connector for the various OTP auth methods for SMS and Voice, navigate to [OTP within Authentication Methods](https://app.descope.com/settings/authentication/otp), and select the configured Twilio connector. You can further configure the default OTP template for your Twilio messages; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). Once you have configured Twilio as the default connector within the OTP Auth method, Descope flows and any OTP calls via the API/SDK will utilize Twilio for OTP verification. ![Configuring Twilio as the default OTP connector within Descope Authentication Methods](/assets/twilio-default-messaging-connector.webp) ### Using Twilio Explicitly in Flows To override the default OTP connector within your flow to utilize Twilio, navigate to [Flows](https://app.descope.com/flows) and choose your flow. Once you are in the flow that you'd like to use Twilio, select and edit the action that starts the OTP process, such as Sign In, Sign Up, or Sign Up or In with OTP via SMS, Email, or Voice. If you have not yet added the OTP action, you can do so by clicking the blue `+` icon at the top left, searching and selecting the OTP action, and then selecting and editing the initiating action. In this example, we will update the `Sign Up or In / OTP / SMS` action to utilize the Twilio connector and override the configured default for the OTP auth method. Once in edit mode for the initiating OTP action, select the dropdown for the connector and choose your Twilio connector. After selecting the Twilio connector, click done, and save your flow. You can also select a customized template; for more details, see our guide for [Using Email and SMS Templates](/flows/actions/email-sms-templates-in-flows). This action within your flow will utilize the Twilio connector and override the configured default OTP connector. ![Configuring Twilio as the OTP connector to be used within Descope flow actions](/assets/twilio-flow-action-config.webp) ### Sending Custom Messages via Twilio in Flows There may be a use case where you want to send a text message or voice message as part of your Descope flow that is not directly related to the OTP Auth flow; this section will describe how to use Twilio to send custom messages. Navigate to: [Flows](https://app.descope.com/flows), choose your flow. Then, tap the create button (designated via the `+` icon), select Connector, and choose Twilio. Click to set down the connector in your flow editor and double click the action to add required fields: - To: Who the message or call should be sent to. Setting this dynamically based on previous attributes is recommended (eg. `{{user.phone}}`). As you type in the input field, options appear. - Message/Call content: Enter the content of the SMS or call. ![Twilio connector widget input](/assets/twilio-connector-placement.webp) Now, link your connector at a logical point in your authentication flow, likely at the end of the flow after the phone number has been submitted by the user. ![Twilio connector placement](/assets/twilio-connector-placement-2.webp) That's it! Whenever a user goes through your authentication flow, the Twilio connector will be automatically triggered. ## Handling Twilio Blocked Calls/SMS Twilio may block voice calls to certain numbers; this results in [Twilio error code 21216](https://www.twilio.com/docs/api/errors/21216). Essentially, the number is blocked by Twilio due to: - The destination has a high-risk of fraud - Due to regulatory reasons, the destination cannot be reached - You are placing a call to a +1 destination and your account is missing a Primary Customer Profile Descope allows you to capture this returned error from Twilio and handle it how you would like. See the example image below; however, for further details on handling errors within flows, see [this guide](/handling-flow-errors/customizing-flow-errors). ![Handling Twilio blocking calls within flow errors](/assets/provider-blocked-calls-error-handling.webp) A similar error handling in Descope is also available for sms. In scenarios where a number has been blocked by twilio due to the reasons mentioned above, the error can be captured as a part of "SMS blocked by provider" and handled as required. ![Handling Twilio blocking sms within flow errors](/assets/provider-blocked-sms-error-handling.webp) # WhatsApp Cloud API (/connectors/connector-configuration-guides/messaging/whatsapp-api) Learn how to send messages via Whatsapp using the WhatsApp Cloud API Connector within your flows. # WhatsApp Connector Send messages via Whatsapp including OTP and Magic Link using the Descope Whatsapp Cloud API Connector. For more information on the WhatsApp Cloud API, check out the WhatsApp [docs](https://developers.facebook.com/docs/whatsapp/cloud-api/reference). For information on the Descope WhatsApp Chat Connector, use the guide for the [nOTP Authentication Method](/auth-methods/notp) ## Configuration To configure the WhatsApp Cloud API connector, you will need to specify the following settings: - **Connector Name**: Assign a custom name to your connector. This is especially useful for identifying the connector within a list of multiple connectors derived from the same template. - **Connector Description**: Provide a brief description of what your connector is used for. This helps others understand the purpose and functionality of the connector at a glance. - **Phone Number ID**: This is the unique identifier for the WhatsApp phone number associated with your account. For more details on locating your Phone Number ID, consult the [WhatsApp documentation](https://developers.facebook.com/docs/whatsapp/cloud-api/reference). - **Token**: Input the authentication token associated with your Phone Number ID. This token is crucial for authenticating API requests to the WhatsApp Cloud API. ### Testing Before deploying your connector, it's crucial to verify its configuration: 1. Enter a phone number to which the test message should be sent. 2. Use the **Test** button to initiate a test Whatsapp message. This test ensures that your connector is properly set up and can send WhatsApp messages successfully. ## Activating Connectors 1. One-time Password (OTP) The first way to use the WhatsApp connector is to send OTP messages. Simply head to Authentication Methods, click [One-time Password](https://app.descope.com/settings/authentication/otp), and choose a different Connector to send messages. ![Authentication methods OTP whatsapp](/assets/whatsapp-otp-authentication-method.webp) You can even create custom templates to adjust the OTP message. ![authentication methods otp whatsapp custom template](/assets/whatsapp-otp-authentication-method-custom-template.webp) 2. Send WhatsApp Messages in Flow The second option is to add the WhatsApp widget in the flow to send WhatsApp messages. Simply navigate to the [flow](https://app.descope.com/flows) and add the connector widget. ![whatsapp widget in flow config](/assets/whatsapp-widget-in-flow-config.webp) ![whatsapp widget in flow](/assets/whatsapp-widget-in-flow.webp) # Audit Webhook (/connectors/connector-configuration-guides/network/audit-webhook) Descope's Audit Webhook Connector is used to send audit logs to your own API. # Audit Webhook Connector Descope's Audit Webhook Connector allows Descopers to stream audit logs to their own API. ### 1. Deploy an API to Capture Webhook Events Deploy an API to a service of your choice that accepts HTTP requests. This API will be used to receive the audit logs from Descope. This connector sends outbound requests from Descope's [static IPs](/how-to-deploy-to-production/public-static-ips). Allowlist these on your API gateway, firewall, or server as needed. ```typescript title="app/api/audit/route.tsx" export async function POST(request: Request) { const body = await request.json(); // Handle audit logs return new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' }, }); } ``` ### 2. Configuring the Webhook Audit Connector Navigate to the Descope's Audit Webhook Connector configuration page and fill in the required parameters: - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Base URL**: Input the API URL where you'd like to send audit events to. This should start with either `http://` or `https://`. Use the URL from Step 1. - **Authentication Type**: Descope supports various methods to authenticate with your service. Choose the method that suits your API: - **Bearer Token**: Used for access keys such as JWTs. - **API Key**: This usually involves a key-value pair. - **Basic Authentication**: The traditional username and password method. - **None**: Select this if your API doesn't require any authentication. - **Headers (Optional)**: Some APIs need specific headers, usually key-value pairs, to provide more details about the impending action. - **HMAC Secret (Optional)**: HMAC is a symmetric key method for message signing. The provided secret will be used to sign the payload. The outcome signature will be sent in the `x-descope-webhook-s256` header. The recipient service should use this secret to validate the payload's integrity and authenticity by verifying the supplied signature. - **Trust Any Certificate**: By default, this option is turned off. If enabled, the client will overlook any certificate errors. While convenient for testing, it's crucial to remember that this is an insecure choice for production. - **Stream Audit Events**: Select this if you want to stream audit events to your API. ![Twilio connector widget input](/assets/audit-webhook-connector.webp) ### 3. View Audit Logs Once you've configured the Audit Webhook Connector, your events will be sent to the API you specified. You can view the audit logs in the Audit page, which should match the events you receive in your API. ![Twilio connector widget input](/assets/api-logs.webp) ![Twilio connector widget input](/assets/api-logs-2.webp) Each event includes the originating client IP as a top-level `remoteAddress` field. See [Audit Event `remoteAddress` Field](/audit-trails-and-integrations/audit-trail-streaming#audit-event-remoteaddress-field). ### Use Cases #### Roles Revoked From A User As a security best practice, monitoring changes in the association of roles to users is crucial. As the identity provider, Descope can provide the role name as part of the "UserModified" audit event. ```json { "Change": { "removed_multi_tenant_roles": { "T2lTcqSm7f5GD8lRwCyo4aHvC6wo": [ { "id": "ROL2lKYLkbHroW5IntCAYTWPX86rRs", "name": "test1" } ] } }, "browser": "Chrome", "correlation_id": "2nQJ0PEyu5EHrMJChqNgPC4AeVp", "device": "Desktop", "os": "macOS", "osVersion": "10.15.7" ... } ``` In this example the role name "test1" was removed from the user. #### Flow Modified In Production Monitoring when authentication flows are modified is critical for maintaining security and compliance. As the identity provider, Descope can provide the modified flow and associated user as part of the "FlowUpdated" audit event. ```json { "action": "FlowUpdated", "actor_id": "user@company.com", "occurred": "2024-01-15T14:30:22Z", "device": "Desktop", "browser": "Chrome", "correlation_id": "3mQK1PFzv6FIsNKDhrOhQD5BfWq", "data": { "flow_id": "FL2mLZYMlcIsqX6JouDBZUXQY97sSt", "flow_name": "Production Login Flow", "changes": { "modified_steps": ["email_verification", "mfa_setup"] } }, "os": "macOS", "osVersion": "10.15.7", "remoteAddress": "203.0.113.42" ... } ``` In this example the user `user@company.com` modified the "Production Login Flow" from `203.0.113.42`. #### Project Setting Modified Tracking changes to project-level configurations helps maintain security posture and compliance. Descope can provide the modified setting and associated actor as part of the "ProjectSettings" audit event. ```json { "action": "ProjectSettings", "actor_id": "admin@company.com", "occurred": "2024-01-15T16:45:18Z", "device": "Desktop", "browser": "Chrome", "correlation_id": "4nRL2QGzw7GJtOLEisPiRE6CgXr", "data": { "settings_changed": { "jwt_expiration": { "old_value": "3600", "new_value": "7200" }, "allowed_origins": { "added": ["https://newapp.company.com"], "removed": [] } } }, "os": "Windows", "osVersion": "11" ... } ``` In this example the user `admin@company.com` edited the "jwt_expiration" and "allowed_origins" settings. # Generic HTTP (/connectors/connector-configuration-guides/network/generic-http) Learn how to configure and utilize advanced features of Descope's generic HTTP connector. # Generic HTTP Connector The **Generic HTTP Connector** enables integration between Descope and any third-party HTTP API. Use it to pull data into Descope flows (e.g., user information) or push data to external services. ## Configuration To integrate the Generic HTTP Connector into your project, navigate to the [Connectors page](https://app.descope.com/connectors), select the Generic HTTP connector, and configure the following parameters: - **Connector Name**: Assign a custom name to your connector. This is especially useful for distinguishing among multiple connectors derived from the same template. - **Connector Description**: Provide a brief description of your connector's purpose. - **Base URL**: Root URL of the third-party API (e.g., `https://api.example.com`). - **Authentication Type**: Choose [how to authenticate](/connectors/connector-configuration-guides/network/generic-http#authentication-methods) with the third-party API. - **Request Headers** (Optional): Key-value pairs to be included in API requests. For each header, you can choose to save the value as **plain text** or mark it as a **secret**. Secret values are encrypted and are not displayed again after saving. ![Generic HTTP Connector Configuration secret headers](/assets/generic-http-connector-configuration-secret-headers.webp) - **HMAC Secret** (Optional): Signs the request and sends the result in an `x-descope-webhook-s256` header. [Learn more](/connectors/connector-hmac-usage), including how to [customize what gets signed](#hmac-signature-customization). - **AWS Signature V4** (Optional): Enable AWS Signature V4 for signing requests. - **RFC 9421 HTTP Message Signatures** (Optional): Enable RFC 9421 HTTP Message Signatures for cryptographically signing requests. Supports multiple algorithms including ECDSA, Ed25519, RSA, and HMAC. - **Trust any certificate**: If enabled, disables SSL verification. Default is disabled (recommended). - **Include response headers and status code in Context**: Adds response headers and status code to the connector context (`context.headers` and `context.statusCode`). Default is off. See [Accessing HTTP Response Headers and Status Code](#accessing-http-response-headers-and-status-code) below. - **Use Static IPs**: If enabled, the connector uses a predetermined pool of IPs from which all requests will be made. See [Static IP Addresses](#static-ip-addresses) below. Once you've configured all of these settings, you can [test the connector](#testing) in the connector configuration page. ### Authentication Methods The Generic HTTP Connector supports several authentication mechanisms. Choose the one that matches the requirements of the third-party API you are connecting to: - **Bearer Token**: Requires a token. It is included in the `Authorization` header as `Bearer `. - **API Key**: Requires a **key name** and **key value**. The key is sent as a header (e.g., `x-api-key: `). - **Basic Authentication**: Requires a **username** and **password**, which are sent as a `Basic` Authorization header (`Basic `). - **OAuth 2.0 (Client Credentials)**: Requires client credentials to dynamically obtain and use an access token. See details below. #### OAuth 2.0 (Client Credentials Flow) When using OAuth 2.0, Descope will handle token retrieval and injection into requests automatically. You must provide the following: - **Client ID** - **Client Secret** - **Token Endpoint URL** - **Scopes** (optional; space-separated string) - **Credential Placement**: Choose whether to send the credentials: - In the request **body** - In the **Authorization header** using HTTP Basic (default) ![Client Credentials with Generic HTTP Connector](/assets/client-credentials-generic-http.webp) Descope will fetch and cache the access token using the client credentials flow and include it in the `Authorization` header for subsequent requests. Authentication field values (token, key value, username, password, client ID, client secret) also support [dynamic values](#dynamic-values-in-authentication) instead of static text. ### Testing Before deploying your connector, it's crucial to verify its configuration: 1. Use the Descope Console to test your HTTP connector and verify the integration—for example, pulling user details from an external API. 2. Use the **Test** button to review the response in the test panel. This test ensures that your connector is properly set up and can communicate with the third-party API successfully. ![Test Connector](/assets/generic-http-connector-test.webp) ## Handling HTTP Status Codes You can handle different HTTP responses in your flow using conditionals based on the status code. ### Step 1: Enable Custom Error Handling Configure the connector action to use **Custom** error handling for the `Generic HTTP Error` event. Doing this will result in the flow redirecting to a new path in the flow if the response code is not 200. ![Custom Error Handling](/assets/generic-http-connector-error-handling.webp) You can also set Connector Execution error handling to **Custom** to handle errors with the connector itself. ### Step 2: Add Conditional Logic You'll need to rely on the context key for your connector to access the response code in a condition block. Use the key `connectors..statusCode` to access the response code in flow conditions. Example use cases: - **404** → Show "User Not Found" screen. - **429** → Return to login screen. - Other errors → Restart the flow. ![Conditional Logic](/assets/generic-http-connector-condition.webp) For a complete list of response codes, you can visit the [HTTP Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) page. ### Step 3: Build Flow Logic Connect conditions and screens to build the complete flow logic. This is an example of a flow that will restart the process if a 429 rate limit error occurs, but will show a "User Not Found" screen if a 404 error occurs. ![Flow Overview](/assets/generic-http-connector-flow.webp) The base URL of your connector can be dynamic and configured in your connector settings by including `{{value}}` for your dynamic value. This value can also be data from other connectors and scriptlets. For example, if you have a region-specific URL, you can dynamically switch the region in the link without having to make new connectors. ![Dynamic URL Example](/assets/dynamic-connector-url-example.webp) ## Accessing HTTP Response Headers and Status Code Enable the **Include response headers and status code in Context** option in the connector settings to access them in your flow as `context.headers` and `context.statusCode`. For example, you can: - Extract custom headers. - Use them in a **Custom Claims** action. - Add the values to the JWT. ```json { "customHeader": "My header value", ... } ``` ![Enable Headers](/assets/generic-http-connector-headers.webp) ![Use Headers in Flow](/assets/generic-http-connector-headers-flow.webp) ## HMAC Signature Customization By default, setting an **HMAC Secret** (see [Configuration](#configuration)) signs the entire JSON request body and sends the result in the `x-descope-webhook-s256` header, as described in [Using HMAC Authentication Type](/connectors/connector-hmac-usage). If you need to sign something other than just the body—for example, the request method, URI, or specific headers—you can customize the signature template using the same HMAC Secret. ### Configure Signature Template In the connector settings, set a `x-descope-signature-template` header with a comma-separated list of elements to sign: Example template: `METHOD,URI,HEADER_x-descope-timestamp,BODY` Descope will compute the HMAC and send it in the `x-descope-webhook-s256` header. ![Configure HMAC](/assets/generic-http-connector-hmac.webp) ### Example Headers Sent ```json { "x-descope-webhook-s256": "xxxxx", "x-descope-timestamp": "1717787565548", "x-descope-signature-template": "METHOD,URI,HEADER_x-descope-timestamp,BODY" } ``` ### Validating the Signature Use the following example to validate the HMAC signature in your backend: ```js import crypto from "crypto"; function verifyHmacSignature(payload, secret, sentHmac) { const hmac = crypto.createHmac("sha256", secret).update(payload).digest("base64"); return sentHmac === hmac; } export async function validateHMAC(req) { const secret = "ConfiguredSigningSecret"; const template = req.headers["x-descope-signature-template"]; let signature = template.replace("METHOD", req.method).replace("URI", req.url); if (template.includes("BODY")) { signature = signature.replace("BODY", JSON.stringify(req.body || {})); } const headerMatches = template.match(/HEADER_([a-zA-Z0-9-_?~]+)/g) || []; headerMatches.forEach(h => { const name = h.replace("HEADER_", ""); signature = signature.replace(h, req.headers[name] || ""); }); const sentHmac = req.headers["x-descope-webhook-s256"]; return verifyHmacSignature(signature, secret, sentHmac); } ``` ## Dynamic Values in Headers You can inject dynamic values (e.g., form fields or flow inputs) into headers. These can also be dynamic values generated from a connector or scriptlet. ### Using Values from Other Connectors A common use case is chaining connectors together, where one connector's output becomes another connector's input. You can use the output of a connector and include it in the request body of your generic HTTP connector in the flow itself. However, if the output of the one connector must be included in the headers of your generic HTTP connector, you can do that as well. Here's an example: 1. First, use a **Database Connector** to fetch a user's profile 2. Then, use the Generic HTTP connector to call an external API, passing the user's data in headers Example flow: ```json { "x-user-id": "{{connectors.dbResult.user.id}}", "x-user-role": "{{connectors.dbResult.user.role}}", "x-tenant-id": "{{connectors.dbResult.tenant.id}}" } ``` This can also be used if there's a dynamic token that needs to be included in the headers of the Generic HTTP connector for authentication, without relying on the OAuth 2.0 Client Credentials flow. ### Example Header configuration in the connector: ```json { "x-descope-form-email": "{{form.email}}", "x-descope-testid": "{{form.testID}}" } ``` Example of received headers: ```json { "x-descope-form-email": "chris+200@descope.com", "x-descope-testid": "12345678" } ``` ![Dynamic Headers](/assets/dynamic-values-in-headers-generic-http-connector.webp) ## Dynamic Values in Authentication You can use the same `{{}}` syntax from [headers](#dynamic-values-in-headers) in the connector's [authentication](#authentication-methods) fields. Set the Bearer token, API key value, Basic username or password, or OAuth 2.0 client ID and secret as a dynamic value from a form field, scriptlet output, or another connector's result. For example, generate a unique correlation ID with a scriptlet and pass it into an authentication field, such as a Basic Authentication username or an OAuth 2.0 client secret. ### Example Basic Authentication configuration in the connector, with a dynamic username and a static password: ```json { "username": "{{scripts.scriptletResult.correlationId}}", "password": "static-password" } ``` ## Static IP Addresses When using the Generic HTTP Connector, you may want to restrict access to your third-party API by IP address. Descope provides the option to route all requests from the connector through a set of **static IPs**. In the connector configuration screen, you can enable **Use Static IPs**. When this option is selected: - Descope will route all outgoing requests through a predefined set of IP addresses. - These IPs are displayed in the UI for you to copy and allow-list on your API gateway, firewall, or server. ![Static IP Option](/assets/generic-http-connector-static-ip-option.webp) # Webhook Connectors (/connectors/connector-configuration-guides/network) Webhook Connectors Overview # Webhook Connectors Webhook Connectors in Descope enable you to make HTTP requests to any API or webhook during authentication flows. Use these connectors to connect to your own backend services, integrate with third-party providers that don't have dedicated connectors, pull data into your flows, push data to external services, and stream audit logs to your own endpoints. ## Available Webhook Connectors Descope supports Webhook Connectors for various API integration scenarios. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Docebo (/connectors/connector-configuration-guides/other/docebo) Use Descope's Docebo Connector to fetch user information from Docebo's User API endpoint. # Docebo Connector Docebo is a cloud-based learning management system (LMS) that offers various tools for eLearning. Descope's Docebo Connector allows you to fetch user information from Docebo using the /manage/v1/user API endpoint by passing a dynamic value for the `search_text` query parameter, as a filter. This guide will walk you through configuring the connector and incorporating it into your Descope Flows. ## Items to Note - The Docebo Connector requires [OAuth 2.0 Password Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.3) for API access due to Docebo's authentication policies. - You need valid Docebo credentials with the necessary permissions to access the API. ## How to Configure You can begin the configuration with the steps listed below. 1. **Obtain Docebo API Credentials:** - Login to your Docebo account. - Navigate to the API and Integrations section. - Select `Add OAuth2 App`, and then fill in an `App Name` and `App Description`. - Generate a new `Client ID` and `Client Secret`, along with the respective `Redirect URI`. ![Docebo OAuth configuration](/assets/docebo-oauth.webp) Make sure that you select `Resource Owner Password Credentials` under: Show Advanced Settings -> Grant Types 2. **Configure the Connector:** - In your Descope project, navigate to the Connectors section. - Select the Docebo Connector. - Enter the API base URL: `https://api.docebo.com/manage/v1/user` - Provide the OAuth credentials obtained in the previous steps. - Save your configuration by clicking `Create`. ![Docebo Connector configuration](/assets/docebo-connector-setup.webp) After configuring the connector, you can use it in your Flows to fetch user information, via the `context key`. ## How to Use the Connector After configuration is complete, you should see this Connector under the list of Connectors in your flow: ![Docebo Connector in list](/assets/docebo-connector-list.webp) You can alter the `Step name`, handle errors, and test the configuration just like with all of the other connectors. ![Docebo Connector](/assets/docebo-connector-flow.webp) If the connector runs without a `search_text` parameter, it will return user information for all of your Docebo users. The `search_text` parameter therefore operates as a filter, returning users that have identifiers or attributes that include whatever you've provided in the `search_text`. ![Docebo Connector search text](/assets/docebo-connector-search-text.webp) ## Error Handling The Docebo connector can handle specific errors related to the fetching of user information. Depending on the status code or the JSON response returned by Docebo, you can handle either of the following in your flow: - **Failed Getting Users** - **Failed to Authenticate** Any errors relating to the authentication or token exchange with OAuth Password Grant will fall under the error `Failed to Authenticate`. This is useful in case the password in your connector configuration being used to authenticate with Docebo expires. All other errors related to an incorrect `search_text`, or issues with the user information response itself will be returned with `Failed Getting Users`. ## Conclusion With Descope's Docebo Connector, you can easily fetch user information from Docebo using dynamic search criteria. This functionality enables you to integrate Docebo's user data into your Descope flows, as well as perform lazy migrations of your Docebo users to Descope as an OIDC provider. # SQL (/connectors/connector-configuration-guides/other/generic-sql) Use Descope's SQL Connector to query relational databases such as PostgreSQL, MySQL, MariaDB, and Oracle directly from your Descope flows. # SQL Connector The **SQL Connector** enables you to query relational databases directly from Descope flows. Use it to look up user records, validate data, or fetch information from any supported SQL database engine at runtime. ## Supported Engines - CockroachDB - MariaDB - MySQL - Oracle - PostgreSQL - Redshift - SQL Server (MSSQL) ## Configure the SQL Connector Navigate to the [Connectors](https://app.descope.com/connectors) page in the Descope Console and select **SQL** to create a new connector. ### Connector Settings - **Connector Name**: A unique name for this connector. Helpful when you have multiple SQL connectors pointing to different databases. - **Connector Description** (optional): A brief note on the connector's purpose. - **Engine**: The database engine to connect to (e.g., PostgreSQL, MySQL, MariaDB, Oracle, CockroachDB, Redshift, SQL Server). - **Host**: The hostname or IP address of the database server (e.g., `db.example.com` or `10.0.0.5`). - **Port**: The port the database is listening on. Common defaults: PostgreSQL `5432`, MySQL/MariaDB `3306`, Oracle `1521`, SQL Server `1433`. - **Username**: The database user account used to authenticate the connection. - **Password**: The password for the database user. - **Database**: The name of the database (or schema) to connect to. The database host must be publicly accessible over the internet. Databases restricted to an internal network (VPN, private VPC) are not reachable unless you expose the endpoint or use a publicly accessible proxy. ![Create SQL Connector](/assets/create-sql-connector.webp) ### Testing After filling in all fields, click **Test** to verify the connection. The Test Results panel will confirm whether the connector can successfully connect to your database. Once the test passes, click **Create** to save the connector. ## Add the SQL Connector to a Flow ### 1. Select or Create a Flow Navigate to [Flows](https://app.descope.com/flows) in the Descope Console and open an existing flow or create a new one. ### 2. Add the Connector Action In the flow editor, add the **SQL / Execute Query** action. This action accepts a single SQL query and returns the results as an object. **Parameters:** - `query` (required): The SQL query to execute. You can inject dynamic flow values using `{{value}}` placeholders (e.g., `SELECT * FROM users WHERE email = '{{user.email}}'`). ![SQL Execute Query](/assets/sql-execute-query.webp) ### 3. Example Use Case A common pattern is to look up a user record during login and enrich the Descope session with custom claims: 1. Add the **SQL / Execute Query** action with a query like: ```sql SELECT role, tenant_id FROM users WHERE email = '{{user.email}}' ``` 2. Use the result to conditionally branch the flow based on the user's role. 3. Add a **Set Custom Claims** action to attach `role` and `tenant_id` to the session token. ## Security Considerations Queries are executed with the credentials configured in the connector, which have full access to the connected database. Follow the principle of least privilege: create a dedicated read-only database user for this connector and grant it access only to the tables it needs. When building query strings with dynamic values, take care to avoid SQL injection. Prefer parameterized query syntax where your database driver supports it, or limit dynamic values to well-validated inputs. # LDAP (/connectors/connector-configuration-guides/other/ldap) Use Descope's LDAP Connector to authenticate users against an LDAP directory server with support for username/password and mutual TLS authentication. # LDAP Connector Use the LDAP connector to authenticate users against an LDAP directory server. This connector supports both traditional username/password authentication and mutual TLS (mTLS) authentication. The LDAP Connector integrates directly with an LDAP IdP via webhook and requires the LDAP server to be publicly accessible over the internet (not restricted to an internal network). ## Setting Up the LDAP Connector To integrate the LDAP connector, follow the steps below: ### 1. Navigate to Connector - Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). - Choose **LDAP** from the list of connectors. ### 2. Connector Setup Set up the necessary inputs: - **LDAP Server URL**: The LDAP server URL (e.g., `ldap://localhost:389` for standard LDAP or `ldaps://localhost:636` for LDAP over SSL/TLS) - **When `Use mTLS` is disabled - Username/Password Authentication:** - **Bind DN**: The Distinguished Name to bind with for searching - **Bind Password**: The password for the bind DN - **When `Use mTLS` is enabled - Certificate-based Authentication:** - **Client Certificate (CRT)**: The client certificate in PEM format for mTLS authentication - **Client Private Key (KEY)**: The client private key in PEM format for mTLS authentication - **CA Certificate (PEM)**: The Certificate Authority certificate in PEM format for validating the server certificate - **Reject Unauthorized**: Reject connections to LDAP servers with invalid certificates (default: true) ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Once successful, click `Create` to save the connector. ## Implementing the LDAP Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Integration Add the **LDAP / Search User** command in the flow. The Search User command should be used whenever you need to search for a user in the LDAP directory. **Parameters:** - `baseDn` (required): The base Distinguished Name for LDAP searches (e.g., `dc=example,dc=com`) - `searchFilter` (required): The LDAP search filter template. Use any dynamic value by using `{{value}}` as a placeholder (e.g., `(mail={{user.email}})` or `(uid={{user.name}})`). - `userAttributes` (required): Comma-separated list of attributes to retrieve. The `dn` attribute is always included automatically. - `scope` (optional): The search scope for LDAP queries. Valid values are: - `sub` (default): Search the base DN and all its descendants (subtree search) - `one`: Search only the immediate children of the base DN (one-level search) - `base`: Search only the base DN itself (base object search) - `children`: Search all subordinates of the base DN to any depth, excluding the base DN itself - `derefAliases` (optional): Specifies how alias dereferencing is done during the search. Valid values are: - `never` (default): Never dereference aliases - `always`: Always dereference aliases - `search`: Dereference aliases during the search phase, but not when locating the base DN - `find`: Dereference aliases when locating the base DN, but not during the search phase **Response:** ```json { "status": "success", "data": { "found": true, "userDn": "cn=John Doe,ou=users,dc=example,dc=com", "attributes": { "cn": ["John Doe"], "mail": ["john.doe@example.com"], "uid": ["johndoe"], "sn": ["Doe"], "givenName": ["John"] } } } ``` Or when user is not found: ```json { "status": "success", "data": { "found": false } } ``` ![LDAP connector flow](/assets/ldap-flow.webp) Add the **LDAP / Authenticate User** command in the flow. The Authenticate User command should be used whenever you need to authenticate a user against the LDAP directory. **Parameters:** - `userDn` (required): The Distinguished Name for the user in LDAP. Use any dynamic value by using `{{value}}` as a placeholder (e.g., `mail={{user.mail}},dc=example,dc=com` or `(uid={{user.name}},dc=example,dc=com)`). - `password` (required): The user's password for authentication **Response:** ```json { "status": "success", "data": { "authenticated": true, "userDn": "uid=john,ou=users,dc=example,dc=com" } } ``` Or when authentication fails: ```json { "status": "success", "data": { "authenticated": false, "userDn": "uid=john,ou=users,dc=example,dc=com" } } ``` ![LDAP connector authenticate user](/assets/ldap-authenticate.webp) ## Error Handling The LDAP connector provides specific error codes for different failure scenarios: - `search_error`: An error occurred during the search operation - `authentication_error`: An error occurred during authentication - `connection_error`: Unable to connect to the LDAP server - `configuration_error`: Invalid or missing configuration parameters # PingDirectory (/connectors/connector-configuration-guides/other/ping-directory) Use Descope's PingDirectory Connector to authenticate LDAP users through PingDirectory's REST API with username/password. # PingDirectory Connector Use the PingDirectory connector to authenticate users against the PingDirectory LDAP directory server using its REST API. This connector supports traditional username/password authentication. ## Setting Up the PingDirectory Connector To integrate the PingDirectory connector, follow the steps below: ### 1. Navigate to Connector - Visit the Connectors page in the [Descope Console](https://app.descope.com/connectors). - Choose **PingDirectory** from the list of connectors. ### 2. Connector Setup Set up the necessary inputs: - **Name**: The name of your connector - **Description (optional)**: A short description of what your connector is used for - **Host**: The externally reachable hostname or IP address of your PingDirectory REST API server (does not include `https://` or the API path, like `/directory/v1`) - **Port**: The HTTPS port where the PingDirectory Directory REST API is exposed ### 3. Test & Save - Validate your configuration by clicking the `Test` button and observing the `Test Results` section. - Once successful, click `Create` to save the connector. ## Implementing the PingDirectory Connector in Your Flow ### 1. Select or Create a Flow - Access your [Dashboard](https://app.descope.com) -> Flows. - Select an existing flow or create a new one. ### 2. Integration In the flow builder, click the "+" icon, select Connector, and choose **PingDirectory / Authenticate**. Use this command whenever you need to authenticate a user against the PingDirectory directory server. **Parameters:** - `username` (required): The context key for the flow component where the user provides their PingDirectory username - `password`: The user's password for authentication. Automatically taken from the `form.password` component in your flow ![PingDirectory Connector Configuration Inside Flow](/assets/pingdirectory-connector-flow-config.webp) After the Authenticate step runs, access the results in the flow context under the key `connectors.ping-directory_authenticate`. **Response:** ```json { "connectors": { "ping-directory_authenticate": { "accessToken": "***", "resultCode": { "name": "success", "value": 0 }, "userAttributes": { "_dn": "uid=descope-test,ou=People,dc=example,dc=com", "c": [ "US" ], "cn": [ "Descope Test User" ], "objectClass": [ "top", "ubidPerson" ], "sn": [ "User" ], "ubidEmailJSON": [ "{ \"type\":\"work\", \"value\":\"descope-test@example.com\" }" ], "uid": [ "descope-test" ] } } } } ``` Or when authentication fails: ```json { "connectors": { "ping-directory_authenticate": { "statusCode": 401, "statusText": "Unauthorized" } }, "error": "Unexpected error occurred. Please try again later" } ``` You can access and parse the JSON response from your connector using a **Scriptlet Action** in your flow. To do this, reference the connector's context key to retrieve its results, as shown below. Within the Scriptlet, you can write custom JavaScript code to implement any logic you need based on the connector's output. ![PingDirectory Connector Scriptlet JSON parsing](/assets/pingdirectory-connector-scriptlet.webp) ## Error Handling Error handling is built in to the connector when you configure it in your Flow. You can set up custom or automatic error handling for different scenarios. Read more about error handling in Descope Flows [**here**](https://docs.descope.com/handling-flow-errors/customizing-flow-errors#drag-and-drop-flow-error-handling). ![PingDirectory Connector Error Handling](/assets/pingdirectory-cnnctr-error-hdlg.webp) # Firebase (/connectors/connector-configuration-guides/token/firebase) Use Descope's Firebase Token Connector to generate a Firebase token whenever authenticating # Firebase Connector If you wish to set up Descope as a federated provider of Firebase, you can read our guide [here](/identity-federation/applications/setup-guides/firebase-oidc). You can integrate Descope with Firebase to continue using Firebase's services while replacing its authentication system with Descope Flows. This is especially useful if your application is already built on Firebase but you want to take advantage of Descope's flexible and secure authentication experience. Here is a diagram of how the Firebase token is generated and included in the authentication response: ![Firebase token swimlane diagram](/assets/external-token-firebase.webp) With this integration, Descope can return a Firebase-compatible token as part of the authentication response—available through our SDKs or API. ## Configuring the Firebase Connector Find the Firebase connector on the [Connectors](https://app.descope.com/connectors) page of the Descope Console. - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Service Account**: This is the JSON data for your Firebase project's service account. Read below on how to find this. ![Firebase connector configuration screen](/assets/firebase-connector-config.webp) ### Getting the Service Account JSON ![Firebase console service account download](/assets/firebase-service-account.webp) 1. In Firebase, navigate to the `Project Settings` for your project by clicking the gear in the top left of the [Console](https://console.firebase.google.com/) after selecting your project. 2. Click on the `Service Accounts` tab. 3. Press the `Generate new private key` button to download the JSON data. 4. Open the JSON file and copy the entire file (including the brackets) into the `Service Account` section of the connector configuration. 5. The connector is now configured, you can test it like above by pressing the `Test` button to ensure the Service Account was added correctly. ## Enabling the Connector Now that the connector is configured, enable it under **External Token** in [Session Management](https://app.descope.com/settings/project/session). Learn more about using [External Token](/management/project-settings/external-token) to enable Firebase tokens in your flows. The Firebase token is included in the authentication response when the flow completes. Example: ```json { "cookieDomain": "", "cookieExpiration": 0, "cookieMaxAge": 0, "cookiePath": "/", "externalToken": "FIREBASE_TOKEN", "firstSeen": false, "idpResponse": null, "refreshJwt": "DESCOPE_REFRESH_TOKEN", "sessionExpiration": 1750879215, "sessionJwt": "DESCOPE_SESSION_TOKEN", "user": {} } ``` ![External token in response after flow runner](/assets/byot-flow-run.webp) # Generic Token (/connectors/connector-configuration-guides/token/generic-token) Use Descope's Generic Token Connector to generate a custom external token when a flow has completed # Generic HTTP Token Connector The Generic HTTP Token connector allows you to generate a custom external token when a Descope flow has completed. When a user logs in, the connector sends a POST request to your configured endpoint, including the user's information. Your API should respond with a JSON object containing the external token in the `token` field. ## Configuring the Generic Token Connector Find the Generic HTTP Token connector on the [Connectors](https://app.descope.com/connectors) page of the Descope Console. - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Endpoint**: Input the API URL endpoint where you'd like to request the external token from. This should start with either `http://` or `https://`. - **Authentication Type**: Descope supports various methods to authenticate with your service. Choose the method that suits your API: - **Bearer Token**: Used for access keys such as JWTs. - **API Key**: This usually involves a key-value pair. - **Basic Authentication**: The traditional username and password method. - **None**: Select this if your API doesn't require any authentication. - **Headers (Optional)**: Some APIs need specific headers, usually key-value pairs, to provide more details about the impending action. - **HMAC Secret (Optional)**: HMAC is a symmetric key method for message signing. The provided secret will be used to sign the payload. The outcome signature will be sent in the `x-descope-webhook-s256` header. The recipient service should use this secret to validate the payload's integrity and authenticity by verifying the supplied signature. - **Trust Any Certificate**: By default, this option is turned off. If enabled, the client will overlook any certificate errors. While convenient for testing, it's crucial to remember that this is an insecure choice for production. ![Configuring the generic token connector](/assets/generic-token-config.webp) ### Additional Configuration Details If the endpoint you have created requires authentication make sure to include the authentication header with the correct token type when setting up the connector. If you would like to test out the connection to the endpoint, press the `Test` button to check that it is working properly. ![Testing the generic token connector](/assets/generic-token-test.webp) The response should include a string containing the external token in the `token` field. This will be used in the authentication response at the end of your flows in the `externalToken` field. ## Enabling the Connector Now that the connector is configured, enable it under **External Token** in [Session Management](https://app.descope.com/settings/project/session). Read more on [External Token](/management/project-settings/external-token) to enable tokens in your flows. The token from your endpoint is included in the `externalToken` field from the authentication response when the flow completes. ![External token in response after flow runner](/assets/generic-token-output.webp) # External Token Connectors (/connectors/connector-configuration-guides/token) External Token Connectors Overview # External Token Connectors External Token Connectors in Descope enable hybrid authentication by allowing you to generate custom tokens at the end of authentication flows. These connectors allow you to keep using your existing token format and infrastructure—whether it's Firebase, Supabase, or a custom token format—while leveraging Descope Flows for identity orchestration. ## How External Token Connectors Work External Token Connectors are configured in the [Connectors page](https://app.descope.com/connectors) and generate tokens when a flow completes: 1. The configured token connector is invoked automatically 2. The connector generates a token in the format you've configured (Firebase, Supabase, or custom) 3. The external token is included in the authentication response alongside Descope's session tokens 4. Your backend can use the external token for authorization and integration with existing services If External Token is enabled at a flow level, it will override the project level setting. After you create a connector, enable it in either of these places: - If you want the same external token for every flow, enable it at the project level under [Session Management](https://app.descope.com/settings/project/session) → External Token. - If you only want it for certain flows, or different flows should use different connectors, set External Token Connector on each flow's [End](/flows/actions/end-action#external-token-connector) action instead. See [External Token](/management/project-settings/external-token) for the full feature setup. The token is returned in the authentication response's `externalToken` field. ## Available External Token Connectors Descope supports various platforms and custom token formats. Each connector is configured through the [Connectors page](https://app.descope.com/connectors) in the Descope Console. # Supabase (/connectors/connector-configuration-guides/token/supabase) Use Descope's Supabase Token Connector to generate a Supabase token whenever authenticating # Supabase Connector You can integrate Descope with Supabase to continue using Supabase's services while replacing its authentication system with Descope Flows. This is especially useful if your application is already built on Supabase but you want to take advantage of Descope's flexible and secure authentication experience. With this integration, Descope can return a Supabase-compatible token as part of the response returned after authenticating with flows, SDKs, or the API. Supabase also supports using external SAML providers, however this is only available to Supabase Pro tiers and up. If you're using the Free tier with Supabase, the approach used in this doc is the recommended approach for you. ## Configuring the Supabase Connector Find the Supabase connector on the [Connectors](https://app.descope.com/connectors) page of the Descope Console. - **Connector Name**: Provide a unique name for your connector. This assists in distinguishing it, especially when multiple connectors are derived from the same template. - **Connector Description**: Briefly explain the purpose of this connector. - **Signing Secret**: This is the JWT secret from your Supabase project. Read [below](/connectors/connector-configuration-guides/token/supabase#getting-the-signing-secret) on how to find this. - **Expiration Time**: Duration in minutes for which the external token is valid. - **Custom Claims**: (Optional) Map Descope user attributes to custom JWT claims in the Supabase token. See [Custom Claims Configuration](/connectors/connector-configuration-guides/token/supabase#custom-claims-configuration) below. - **Create Supabase User**: (Optional) Before generating the token, Descope can create a corresponding user in Supabase. This is useful if you want to ensure that a user exists in Supabase for database access or other services. If enabled, you will need to provide the following additional fields: - **Supabase URL**: The URL of your Supabase project (e.g., `https://.supabase.co`). - **Supabase Service Role Key**: The service role key from your Supabase project, which allows Descope to create users in Supabase. ![Supabase connector configuration screen](/assets/supabase-connector-config.webp) ### Getting the Signing Secret ![Locating Supabase JWT signing secret](/assets/supabase-signing-secret.webp) To find the Signing Secret for connector configuration, open your Supabase project and navigate to the project settings. Under the **JWT Keys** tab, reveal and copy the **JWT secret**, and then paste it into the **Signing Secret** input of the connector in Descope. ### Getting the Supabase URL and Service Role Key These fields are only required if **Create Supabase User** is enabled. 1. Enter the **Project URL** (for example, `https://.supabase.co`) into the **Supabase URL** field of the connector. 2. In your Supabase project, go to **Project Settings** > **API Keys** > **Legacy anon, service_role API keys**. 3. Copy the `service_role` secret key into the **Supabase Service Role Key** field of the connector. 4. Press the connector's **Test** button to confirm Descope can reach your Supabase project with these credentials. If a user with that email or phone number already exists in Supabase, Descope skips creating a duplicate and continues generating the token. ### Custom Claims Configuration The Supabase connector supports custom claims configuration, allowing you to map Descope user attributes to specific claims in the generated Supabase JWT. This enables you to include additional user information or metadata in the token that can be used by Supabase's Row Level Security (RLS) policies or your application logic. #### Configuring Custom Claims In the connector configuration, you can add custom claims by mapping Descope user attributes to JWT claim names: 1. In the **Custom Claims** section of the connector configuration, click **Add Claim** 2. Specify the **Claim Name** (the key that will appear in the JWT) 3. Select the **User Attribute** from Descope that should be mapped to this claim 4. Repeat for each custom claim you want to include Common use cases for custom claims include: - **User roles and permissions**: Map `user.customAttributes.role` to `role` claim for authorization - **Tenant/Organization ID**: Map `user.tenantIds` to `tenant_id` claim for multi-tenancy - **User metadata**: Map custom attributes to claims for application-specific logic - **Anonymous user flag**: Configure the `is_anonymous` claim to identify anonymous users #### Supported User Attributes You can map the following Descope user attributes to custom claims: - `user.userId` - The Descope user ID - `user.loginIds` - User's login IDs (email, phone, username) - `user.email` - User's email address - `user.phone` - User's phone number - `user.name` - User's display name - `user.tenantIds` - List of tenant IDs the user belongs to - `user.roleNames` - List of role names assigned to the user - `user.customAttributes.*` - Any custom attribute defined on the user #### Example: Configuring is_anonymous Claim Supabase uses the `is_anonymous` claim to distinguish between authenticated and anonymous users. To configure this in Descope: 1. Add a custom claim with name: `is_anonymous` 2. Map it to a boolean user attribute that indicates anonymous status 3. Use this in your Supabase RLS policies to control access for anonymous users When mapping user attributes that are arrays (like `tenantIds` or `roleNames`), the connector will serialize them appropriately in the JWT. For more information on Supabase custom access tokens, see [Supabase's Custom Access Token documentation](https://supabase.com/docs/guides/auth/auth-hooks/custom-access-token-hook). ## Enabling the Connector Now that the connector is configured, enable it under **External Token** in [Session Management](https://app.descope.com/settings/project/session). Learn more in the [External Token](/management/project-settings/external-token) guide. The Supabase token is included in the authentication response when the flow completes. Example: ```json { "cookieDomain": "", "cookieExpiration": 0, "cookieMaxAge": 0, "cookiePath": "/", "externalToken": "SUPABASE_TOKEN", "firstSeen": false, "idpResponse": null, "refreshJwt": "DESCOPE_REFRESH_TOKEN", "sessionExpiration": 1750879215, "sessionJwt": "DESCOPE_SESSION_TOKEN", "user": {} } ``` This external token contains standard Supabase claims along with any custom claims you've configured. Here's an example of a Supabase token without custom claims: ```json { "sub": "DESCOPE_USER_ID", "exp": 1752785069 } ``` And here's an example with custom claims configured: ```json { "sub": "DESCOPE_USER_ID", "email": "user@example.com", "role": "authenticated", "is_anonymous": false, "tenant_id": "tenant_123", "user_role": "admin", "exp": 1752785069 } ``` The custom claims (`tenant_id`, `user_role`, etc.) are populated from the Descope user attributes based on your connector configuration. ## Using the Token The token is accessible after authenticating with Descope using any method, and can be used when creating a Supabase client as a `Authorization Bearer` token: ```javascript createClient( SUPABASE_PROJECT_URL, SUPABASE_PROJECT_ANON_KEY, { global: { headers: { Authorization: `Bearer ${externalToken}` } } } ); ``` By default, this approach doesn't create a new user record in Supabase. It leverages Descope-managed user details to apply fine-grained control over user permissions. If you want a matching user to exist in Supabase too, turn on **Create Supabase User** in the connector configuration. Either way, you can use Descope to handle authentication while continuing to use Supabase features, such as database, storage, and real-time services, and enforce Supabase's authorization rules to manage user access. ### Using Custom Claims in Row Level Security (RLS) Custom claims configured in the connector are included in the JWT and can be used in Supabase Row Level Security policies. For example, if you’ve configured a `tenant_id` custom claim, you can create RLS policies that filter data based on the tenant: ```sql -- Example RLS policy using custom claim CREATE POLICY "Users can only see their tenant’s data" ON documents FOR SELECT USING (tenant_id = (auth.jwt() ->> ‘tenant_id’)::text); ``` This allows you to implement multi-tenancy, role-based access control, or any other authorization logic using the custom claims from Descope user attributes. # Backend SDKs (/mfa-and-step-up/mfa/mfa-with-sdks/backend-sdk) Add layered security to your app utilizing Multi-factor Authentication (MFA) via Descope Backend SDKs. # Implementing MFA Authentication with Descope Backend SDKs ## Install SDK ```sh title="Terminal" npm i --save @descope/node-sdk ``` ```sh title="Terminal" pip3 install descope ``` ```sh title="Terminal" go get github.com/descope/go-sdk ``` ```java // Include the following in your `pom.xml` (for Maven) java-sdk com.descope sdk-version // Check https://github.com/descope/descope-java/releases for the latest versions ``` ```sh title="Terminal" gem install descope ``` ## Import and initialize Management SDK ```javascript import DescopeClient from '@descope/node-sdk'; const managementKey = "xxxx" try{ // baseUrl="" // When initializing the Descope clientyou can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey }); } catch (error) { // handle the error console.log("failed to initialize: " + error) } ``` ```python from descope import ( REFRESH_SESSION_TOKEN_NAME, SESSION_TOKEN_NAME, AuthException, DeliveryMethod, DescopeClient, AssociatedTenant, RoleMapping, AttributeMapping ) management_key = "xxxx" try: # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com - this is useful when you utilize a custom domain within your Descope project." descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key) except Exception as error: # handle the error print ("failed to initialize. Error:") print (error) ``` ```go import "github.com/descope/go-sdk/descope" import "github.com/descope/go-sdk/descope/client" import "fmt" // Utilizing the context package allows for the transmission of context capabilities like cancellation // signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. import ( "context" ) managementKey = "xxxx" // DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com - this is useful when you utilize a custom domain within your Descope project. descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__", managementKey:managementKey}) if err != nil { // handle the error log.Println("failed to initialize: " + err.Error()) } ``` ```java import com.descope.client; // Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY) var descopeClient = new DescopeClient(); // ** Or directly ** var descopeClient = new DescopeClient(Config.builder() .projectId("__ProjectID__") .managementKey("management-key") .build()); ``` ```ruby require 'descope' descope_client = Descope::Client.new( { project_id: '__ProjectID__', management_key: 'management_key' } ) ``` ## Sign-Up, Sign-in, or Sign-Up-Or-In The next step after adding the Descope backend SDK within your application is to utilize one of the Sign-Up, Sign-in, or Sign-Up-Or-In functions for the supported authentication methods. Once you have successfully received a JWT from the authentication method, you should store it for the next step in the MFA process. ## MFA the user's authentication Now that you have a valid JWT for your authenticated user, you can utilize Sign-in or Sign-Up-Or-In for one of the supported authentication methods, adding the user Login Options. This example will focus on the `mfa` parameter of the Login Options; however, for further details on Login Options, navigate [here](/api/overview#user-login-options). The below example implements MFA authentication via [OTP Sign-In](/auth-methods/otp/with-sdks/backend#user-sign-in) after the user successfully signed up via [TOTP Sign-Up](/auth-methods/auth-apps/with-sdks/backend#user-sign-up). After a successful MFA sign-in, you will need to process the verification code via [OTP Verify](/auth-methods/otp/with-sdks/backend#user-verification). After verifying, the user will then have MFA authentication. ```javascript // Args: // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginId: email or phone - email or phone - the loginId for the user const loginId = "email@company.com" // loginOptions: login options for MFA, stepup, or custom claims. Ex: {stepup: true, mfa: false, customClaims: {}} const loginOptions = {mfa: true} // token: refresh token from the successful sign-in of the user const token = "xxxx" var resp = await descopeClient.otp.signIn[delivery_method](loginId, loginOptions, token); if (!resp.ok) { console.log("Failed to initialize mfa flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized mfa flow") console.log(resp.data) } ``` ```python # Args: # delivery_method: Delivery method to use to send OTP. Supported values include DeliveryMethod.SMS, DeliveryMethod.Voice, or DeliveryMethod.EMAIL delivery_method = DeliveryMethod.EMAIL # login_id: email or phone - email or phone - the loginId for the user login_id = "email@company.com" # login_options (LoginOptions): login options for MFA, stepup, or custom claims. Ex: LoginOptions(stepup: True, mfa: False, customClaims: {}) login_options = LoginOptions(mfa=True) # refresh_token: refresh token from the successful sign-in of the user refresh_token = "xxxxx" try: resp = descope_client.otp.sign_in(method=delivery_method, login_id=login_id, login_options=login_options, refresh_token=refresh_token) print ("Successfully initialized MFA flow") except AuthException as error: print ("Failed to initialize MFA flow") print ("Status Code: " + str(error.status_code)) print ("Error: " + str(error.error_message)) ``` ```go // Args: // ctx: context.Context - Application context for the transmission of context capabilities like // cancellation signals during the function call. In cases where context is absent, the context.Background() // function serves as a viable alternative. // Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher. ctx := context.Background() // deliveryMethod: Delivery method to use to send OTP. Supported values include descope.MethodEmail, descope.MethodVoice, or descope.MethodSMS deliveryMethod := descope.MethodEmail // loginID: email or phone - the loginId for the user loginID := "email@company.com" // r: HttpRequest for the update call. This request should contain refresh token for the authenticated user. // loginOptions: Optional login options for MFA, stepup, or custom claims. loginOptions := &descope.LoginOptions{mfa: true} err := descopeClient.Auth.OTP().SignIn(ctx, deliveryMethod, loginID, r, loginOptions) if (err != nil){ fmt.Println("Failed to initialize MFA flow: ", err) } else { fmt.Println("Successfully initialized MFA flow") } ``` ```java // Every user must have a loginID. All other user information is optional String loginId = "email@company.com"; User user = User.builder() .name("Joe Person") .phone("+15555555555") .email(loginId) .build(); var loginOptions = LoginOptions.builder() .mfa(true) .build(); OTPService otps = descopeClient.getAuthenticationServices().getOtpService(); try { String maskedAddress = otps.signIn(DeliveryMethod.EMAIL, loginId, loginOptions); } catch (DescopeException de) { // Handle the error } ``` # Client SDKs (/mfa-and-step-up/mfa/mfa-with-sdks/client-sdk) Add layered security to your app utilizing Multi-factor Authentication (MFA) via Descope Client SDKs. # Implementing MFA Authentication with Client SDKs ## Sign-Up, Sign-in, or Sign-Up-Or-In The next step after adding the Descope client SDK within your application is to utilize one of the Sign-Up, Sign-in, or Sign-Up-Or-In functions for the supported authentication methods. Once you have successfully received a JWT from the authentication method, you should store it for the next step in the MFA process. ## MFA the user's authentication Now that you have a valid JWT for your authenticated user, you can utilize Sign-in or Sign-Up-Or-In for one of the supported authentication methods, adding the user Login Options. This example will focus on the `mfa` parameter of the Login Options; however, for further details on Login Options, navigate [here](/api/overview#user-login-options). The below example implements MFA authentication via [OTP Sign-In](/auth-methods/otp/with-sdks/client#user-sign-in) after the user successfully signed up via [TOTP Sign-Up](/auth-methods/auth-apps/with-sdks/client#user-sign-up). After a successful MFA sign-in, you will need to process the verification code via [OTP Verify](/auth-methods/otp/with-sdks/client#user-verification). After verifying, the user will then have MFA authentication. ```javascript // Args: // loginId: email or phone - becomes the loginId for the user from here on and also used for delivery const loginId = "email@company.com" // deliveryMethod: Delivery method to use to send OTP. Supported values include "email", "voice, or "sms" const deliveryMethod = "email" // loginOptions: login options for MFA, stepup, or custom claims. Ex: {stepup: true, mfa: false, customClaims: {}} const loginOptions = {mfa: true} // token: refresh token from the successful sign-in of the user const token = "xxxxxx" const resp = await descopeClient.otp.signIn[deliveryMethod] (loginId, loginOptions, token); if (!resp.ok) { console.log("Failed to initialize MFA flow") console.log("Status Code: " + resp.code) console.log("Error Code: " + resp.error.errorCode) console.log("Error Description: " + resp.error.errorDescription) console.log("Error Message: " + resp.error.errorMessage) } else { console.log("Successfully initialized MFA flow") } ``` # OIDC (/identity-federation/applications/setup-guides/auth0/auth0-oidc) Implement OpenID Connect (OIDC) with Auth0 and Descope for secure SSO. Follow our guide for a smooth setup process. # Auth0 OIDC Integration Setup Guide In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) to implement authentication for applications that currently use Auth0. With Descope acting as an OpenID Connect Identity Provider (IdP), you replace Auth0 username and password authentication with Descope authentication, while retaining Auth0 as the primary user management solution. The flow diagram below shows this process: ![Descope OIDC guide diagram of Auth0 as auth provider](/assets/descope-oidc-auth0-flow.webp) Configuring Descope as an OIDC provider with Auth0 is super simple! All you will need is: - A Descope account (you can [sign up](https://www.descope.com/sign-up) for a “Free Forever” account) - Access to an Auth0 account that allows for [Enterprise External Connections](https://auth0.com/docs/authenticate/enterprise-connections). Once you have the above, simply follow along with this guide to learn how to add Descope Flows to your application. ## Setting up your Hosted Auth Page If you want to use Passkeys, you can download the oidc-flow JSON from our [sample app repository](https://github.com/descope-sample-apps/auth0-passkey-implementation), which you can import into your own project. It is important to use this Flow, as it is designed to make sure the user and their email is always verified when using passkeys as an authentication method for security reasons. ![Descope OIDC with Auth0 as auth provider flow configuration 1](/assets/descope-oidc-auth0-config-1.webp) Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). If you're using the `oidc-flow.json` provided above, edit the query parameter at the end of the Flow Hosting URL like so: `https://auth.descope.io/?flow=oidc-flow` ![Descope OIDC with Auth0 as auth provider flow configuration 2](/assets/descope-oidc-auth0-config-2.webp) You should keep this page open, as you're going to need this information for the next part of this blog. If you would like to edit the UI of the passkey login screen, you can do that in the Flow Editor. Once your flow is complete and your login redirect has been configured, you'll need to connect your flow to Auth0 by setting Descope up as an enterprise OIDC connection. ## Descope as an Enterprise Connection In order to set up Descope as an Enterprise connection with Auth0, you will need to add an Enterprise OpenID connection. You can do this under the Authentication -> Enterprise section of your [Auth0 admin dashboard](https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/oidc). When you create a new OpenID connection, you will be prompted to fill in the following information: - **Connection Name**: Call it **Descope** - **Issuer URL**: This is the Issuer URL which is found under your SSO Configuration settings - **Client ID**: Your Descope Project ID, which can be found under [Project Settings](https://app.descope.com/settings/project) in the Descope Console - **Client Secret**: Access key generated under [Access Keys](https://app.descope.com/m2m/accessKeys) in the Descope Console Once you've gathered all of this information, put it in the configuration panel as shown below: ![Descope OIDC with Auth0 as auth provider Auth0 configuration 1](/assets/descope-oidc-auth0-1.webp) The selection toggle at the bottom - **Sync user profile attributes at each login** - will need to be toggled on if you would like to use Descope as an alternative login method to your traditional Auth0 authentication methods. Otherwise, every time a new user logs in with Descope, an additional user will be created in the Auth0 User Portal, without all of the same permissions and roles that were previously defined for that user. ![Descope OIDC with Auth0 as auth provider Auth0 configuration 2](/assets/descope-oidc-auth0-2.webp) When using this feature, the user will then be prompted to verify their Auth0 account with their normal username and password before the user account details can be merged. That way, a malicious user cannot login with Descope for someone else and gain access to their account. Lastly, you'll need to make sure that the scopes for the OIDC connection are correct and that you have a way to initiate the OIDC flow and redirect back the hosted Descope Flow. To do this, click on the new connection you've just made, and make sure that the four scopes are defined in your connection configuration: ![Descope OIDC with Auth0 as auth provider Auth0 configuration 3](/assets/descope-oidc-auth0-3.webp) ![Descope OIDC with Auth0 as auth provider Auth0 configuration 4](/assets/descope-oidc-auth0-4.webp) If you're using a custom login page you've built with Auth0, you will need to add a button or some other way to navigate to where you've embedded the Descope Flow. However, it's likely that you are using the [Universal Login Experience](https://auth0.com/docs/authenticate/login/auth0-universal-login/new-experience) that comes with Auth0 -if that's the case, you can navigate to the **Login Experience** tab and select **Display connection as a button**. ![Descope OIDC with Auth0 as auth provider Auth0 configuration 5](/assets/descope-oidc-auth0-5.webp) This will allow you to redirect back to your Descope Flow and use Descope to perform the authentication rather than Auth0. Your main login screen will now look something like this: ![Descope OIDC with Auth0 testing the completed flow](/assets/descope-oidc-auth0-completed-flow.webp) Once the user clicks on **Continue with Descope Passkeys**, and logs in with their biometrics, the app will act as though they've logged in with Auth0. ## Sample App If you're interested in seeing how this is implemented in a sample React application, feel free to check out our [sample app](https://github.com/descope-sample-apps/auth0-passkey-implementation) on GitHub. If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # SAML (/identity-federation/applications/setup-guides/auth0/auth0-saml) Integrate SAML-based SSO with Auth0 and Descope. Follow our guide for a smooth setup process. # Auth0 SAML Integration Setup Guide In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) to implement authentication for applications that currently use Auth0. With Descope acting as an SAML Service Provider (SP), you replace Auth0 username and password authentication with Descope authentication, while retaining Auth0 as your identity provider (IdP) and primary user management solution. To do this, all you will need is: - A Descope account (you can [sign up](https://www.descope.com/sign-up) for a “Free Forever” account) - Access to an Auth0 account that allows for [Enterprise External Connections](https://auth0.com/docs/authenticate/enterprise-connections). Once you have the above, simply follow along with this guide to learn how to add Descope Flows to your application. ## Setting up your Hosted Auth Page If you want to use Passkeys, you can download the oidc-flow JSON from our [sample app repository](https://github.com/descope-sample-apps/auth0-passkey-implementation), which you can import into your own project. It is important to use this Flow, as it is designed to make sure the user and their email is always verified when using passkeys as an authentication method for security reasons. Your flows are automatically hosted with our [Descope Auth Hosting Application](https://github.com/descope/auth-hosting). To learn more about our hosted app, you can read about it in our Docs page [here](/identity-federation/auth-hosting). If you're using the `oidc-flow.json` provided above, edit the query parameter at the end of the Flow Hosting URL like so: `https://auth.descope.io/?flow=oidc-flow` You can find this `Flow Hosting URL` under your OIDC-based [Application](/identity-federation/applications/oidc-apps) configuration. If you're using the default OIDC application, the configuration is [here](https://app.descope.com/applications/descope-default-oidc) ![Descope with Auth0 as auth provider flow configuration 2](/assets/descope-oidc-auth0-auth-config-2.webp) You should keep this page open, as you're going to need this information for the next part of this blog. Once your flow is set up and your `Flow Hosting URL` has been configured, you'll need to connect your flow to Auth0 by setting Descope up as an enterprise OIDC connection. ## Descope as an Enterprise Connection In order to set up Descope as an Enterprise connection with Auth0, you will need to add an Enterprise SAML connection. You can do this under the Authentication -> Enterprise section of your [Auth0 admin dashboard](https://auth0.com/docs/authenticate/identity-providers/enterprise-identity-providers/saml). When you create a new SAML connection, you will be prompted to fill in the following information: - **Connection Name**: Call it **Descope** - **Sign In URL**: This will be the **SSO URL** that exists under `Identity Provider (IdP)` in your Application settings - **X509 Signing Certificate**: The Descope SAML Signing Certificate which you can download by clicking on **Download public certificate** under `Identity Provider (IdP)` in your Application settings - **User ID Attribute**: This Once you've gathered all of this information, put it in the configuration panel as shown below: ![Descope OIDC with Auth0 as auth provider Auth0 configuration 1](/assets/descope-auth0-saml-configuration-1.webp) You'll then need to find your SP Metadata URL, which can be constructed by following the steps in the Auth0 documentation [here](https://auth0.com/docs/authenticate/protocols/saml/saml-identity-provider-configuration-settings#metadata). ``` https://YOUR_AUTH0_DOMAIN/samlp/metadata?connection=YOUR_AUTH0_CONNECTION_NAME Example: https://dev-7ftdf3sadfsdaf.us.auth0.com/samlp/metadata?connection=Descope ``` - **yourDomain** - this is your Auth0 Domain - **yourConnectionName** - the name of the Connection you configured in the previous step (i.e. `Descope`) This URL will need to be placed in the Application configuration in the Descope Console, under SP Configuration: ![Descope OIDC with Auth0 as auth provider Auth0 configuration 1](/assets/descope-oidc-auth0-auth-config-1.webp) The selection toggle at the bottom - **Sync user profile attributes at each login** - will need to be toggled on if you would like to use Descope as an alternative login method to your traditional Auth0 authentication methods. Otherwise, every time a new user logs in with Descope, an additional user will be created in the Auth0 User Portal, without all of the same permissions and roles that were previously defined for that user. ![Descope OIDC with Auth0 as auth provider Auth0 configuration 2](/assets/descope-oidc-auth0-auth-config-2-2.webp) When using this feature, the user will then be prompted to verify their Auth0 account with their normal username and password before the user account details can be merged. That way, a malicious user cannot login with Descope for someone else and gain access to their account. If you're using a custom login page you've built with Auth0, you will need to add a button or some other way to navigate to where you've embedded the Descope Flow. However, it's likely that you are using the [Universal Login Experience](https://auth0.com/docs/authenticate/login/auth0-universal-login/universal-login-vs-classic-login/universal-experience) that comes with Auth0 -if that's the case, you can navigate to the **Login Experience** tab and select **Display connection as a button**. ![Descope OIDC with Auth0 as auth provider Auth0 configuration 5](/assets/descope-oidc-auth0-auth-config-5.webp) This will allow you to redirect back to your Descope Flow and use Descope to perform the authentication rather than Auth0. Your main login screen will now look something like this: ![Descope OIDC with Auth0 testing the completed flow](/assets/descope-oidc-auth0-auth-completed.webp) Once the user clicks on **Continue with Descope Passkeys**, and logs in, the app will act as though they've logged in with Auth0. ## Sample App If you're interested in seeing how this is implemented in a sample React application, feel free to check out our [sample app](https://github.com/descope-sample-apps/auth0-passkey-implementation) on GitHub. If you have any other questions about Descope or our flows, feel free to reach out to [us](/support)! # Overview (/identity-federation/applications/setup-guides/auth0) Learn how to set up Descope as a federated Identity Provider (IdP) to implement authentication for applications that currently use Auth0. # Auth0 Integration Guides In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) to implement authentication for applications that currently use Auth0. With Descope acting as an Identity Provider (IdP), you replace Auth0 username and password authentication with Descope authentication, while retaining Auth0 as the primary user management solution. Implementing this for both OpenID Connect (OIDC) and Security Assertion Markup Language (SAML) is covered in this guide. # Overview (/identity-federation/applications/setup-guides/keycloak) Learn how to integrate Keycloak with Descope for effective single sign-on (SSO) authentication. A comprehensive setup guide is available. # Keycloak Integration Setup Guides Keycloak is an open source software product to allow single sign-on with identity and access management aimed at modern applications and services. Descope allows you to configure Applications within the [Applications](https://app.descope.com/applications) page. Within this page, you can configure your OIDC and SAML applications. Using these applications, Single Sign-On (SSO) can be enabled for Keycloak so that Descope can be used to authenticate into any app using Keycloak. With Descope acting as an Identity Provider (IdP), you replace Keycloak username and password authentication with Descope authentication, while retaining Keycloak as the primary user management solution. In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) to implement SSO authentication for applications that currently use Keycloak. This can be done using OpenID Connect (OIDC) or Security Assertion Markup Language (SAML). Both options will be covered in this guide. # OIDC (/identity-federation/applications/setup-guides/keycloak/keycloak-oidc) Implement OpenID Connect (OIDC) with Keycloak and Descope for robust SSO. Follow our detailed setup instructions. # Keycloak OIDC Integration Setup Guide In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) using OpenID Connect (OIDC) to implement authentication for applications that currently use Keycloak. ## Configuring Keycloak SSO Descope will act as the OIDC IdP so that Descope can be used for authentication while Keycloak remains the primary user management solution. This will allow you to have the versatility and customizability of Descope Flows in the authentication process without having to migrate all users from Keycloak. After installing Keycloak and running it on your local machine, navigate to the Identity Providers section of the menu. ![Creating a new OIDC provider in Keycloak](/assets/keycloak-idp-1.webp) Select Keycloak OpenID Connect to create a new identity provider. This is where the Descope Application information will be entered. Now in the Descope Console, navigate to the Applications page and create a new application by pressing the create button in the top right. Make sure the new application that is being created is using OIDC. ![Creating a new OIDC Application in Descope](/assets/keycloak-idp-5.webp) After creating the Application in Descope, copy the Discovery URL from the Application settings and enter it as the Discovery endpoint in Keycloak. ![Configuring the IdP in Keycloak](/assets/keycloak-idp-7.webp) The Client ID and Client Secret must also be configured. The Client ID is the Project ID of the Descope project you are using. The Client Secret is an Access Key you must create in the Descope Console on the Access Key page. ![Creating the Access Key in Descope](/assets/keycloak-idp-6.webp) After creating the Access Key, make sure to copy it and save it somewhere as this is the only time you will be able to see it. After copying the access key, paste it into the Client Secret in Keycloak. SSO is now enabled for Keycloak. Be sure to grant new users roles and permissions in Keycloak so that they can utilize the Keycloak console. ![Log in screen with SSO](/assets/keycloak-idp-8.webp) Get started by going to the [Applications](https://app.descope.com/applications) page in your Descope Console! You can read more about Federated Applications [here](/identity-federation/applications). If you have any other questions about Descope or our flows, feel reach to reach out to [us](https://docs.descope.com/support/)! # SAML (/identity-federation/applications/setup-guides/keycloak/keycloak-saml) Integrate SAML-based SSO with Keycloak and Descope. Easy-to-follow setup guide included. # Keycloak SAML Integration Setup Guide In this guide, we will cover how to set up Descope as a federated Identity Provider (IdP) using Security Assertion Markup Language (SAML) to implement authentication for applications that currently use Keycloak. ## Configuring Keycloak SSO Descope will act as the SAML IdP so that Descope can be used for authentication while Keycloak remains the primary user management solution. This will allow you to have the versatility and customizability of Descope Flows in the authentication process without having to migrate all users from Keycloak. ### Configuring Descope as a SAML IdP on Keycloak After installing Keycloak and running it on your local machine, navigate to the Identity Providers section of the menu. ![Creating a new SAML provider in Keycloak](/assets/keycloak-idp-1.webp) Select SAML 2.0 to create a new identity provider. This is where the Descope Application information will be entered. Now in the Descope Console, navigate to the Applications page and create a new application by pressing the create button in the top right. Make sure the new application that is being created is using SAML. ![Creating a new SAML Application in Descope](/assets/keycloak-idp-2.webp) After creating the Application in Descope, copy the Descope Metadata (XML) from the Application settings and enter it as the SAML entity descriptor in Keycloak. Keycloak requires a single logout service URL which Descope does not provide so any generic URL can be used in this place, enable backchannel logout instead. Then set the NameID policy format to Email and press add. ![Setting up Descope as the IdP in Keycloak](/assets/keycloak-idp-3.webp) ### Configuring Keycloak as a SAML Application on Descope Now in the Descope Console, enter the information about the Service Provider. Entering the connection details manually: - ACS URL: http(s)://host:port/realms/realm-name/broker/IdP-name/endpoint - Entity ID: http(s)://host:port/realms/realm-name ![Setting up Keycloak as the SP in Descope](/assets/keycloak-idp-4.webp) Make sure the SAML Assertion Subject Type is also set to Email and Email is mapped to NameID. SSO is now enabled for Keycloak. Be sure to grant new users roles and permissions in Keycloak so that they can utilize the Keycloak console. ![Log in screen with SSO](/assets/keycloak-idp-8.webp) Get started by going to the [Applications](https://app.descope.com/applications) page in your Descope Console! You can read more about Federated Applications [here](/identity-federation/applications). If you have any other questions about Descope or our flows, feel reach to reach out to [us](https://docs.descope.com/support/)! # Authenticated SSO Admin Link (/api/management/tenants/admin-links/authenticated-tenant-admin-link-sso) ### Authenticated SSO admin link for a tenant, using a valid management key. This API endpoint handles authenticated SSO admin link requests for a tenant. # Generate Tenant Admin SSO Link (Deprecated) (/api/management/tenants/admin-links/generate-tenant-admin-link-sso-deprecated) Generate tenant admin SSO configuration link, using a valid management key. Deprecated: use the v2 endpoint at /v2/mgmt/tenant/adminlinks/sso/generate instead. # Generate SSO Admin Link (/api/management/tenants/admin-links/generate-tenant-admin-link-sso) ### Generate an SSO admin link for a tenant, using a valid management key. This API endpoint generates an SSO admin link that allows a tenant administrator to configure SSO settings. # Revoke SSO Admin Link (/api/management/tenants/admin-links/revoke-tenant-admin-link-sso) ### Revoke an SSO admin link for a tenant, using a valid management key. This API endpoint revokes an existing SSO admin link for a tenant. # Send SSO Admin Link (/api/management/tenants/admin-links/send-tenant-admin-link-sso) ### Send an SSO admin link to a tenant administrator, using a valid management key. This API endpoint sends an SSO admin link via email to the specified tenant administrator. # Group Management API Overview (/api/management/tenants/groups) Use the Descope API to manage your tenants' Group configurations with a management key. # Group Management ## Overview The Group Management APIs let you load details about external SSO groups associated with your tenants. ## Use Cases 1. [Load All External Groups for a Tenant](/api/management/tenants/groups/load-groups) 2. [Load All External Groups for Specific Members](/api/management/tenants/groups/load-member-groups) 3. [Load All Members of a Specific External Group](/api/management/tenants/groups/load-group-members) ## Examples ### Example - load all groups for a tenant Use the [Load All External Groups for a Tenant](/api/management/tenants/groups/load-groups) endpoint to retrieve the external groups associated with a tenant. This lets administrators verify group associations and make any necessary changes via SCIM or their SSO IdP. # Load All Members of a specific External Group (/api/management/tenants/groups/load-group-members) ### Load all members of a specific External group, using a valid management key. This API endpoint allows administrators to load all members of a specific external group that is associated to a tenant. The response contains an array of group objects including the group id, display name, and an array of associated members. ### Next Steps Administrators can review this information and make changes within their IdP or if necessary, [Create a SCIM Group](/api/management/tenants/scim/create-scim-group), [Update an Existing SCIM Group (adding new members)](/api/management/tenants/scim/update-scim-group), or [Delete an Existing SCIM Group](/api/management/tenants/scim/delete-scim-group) ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Load All External Groups for a Tenant (/api/management/tenants/groups/load-groups) ### Load all external groups for a tenant, using a valid management key. This API endpoint allows administrators to load all external groups that are associated to a tenant. The response contains an array of group objects including the group id, display name, and an array of associated members. ### Next Steps Administrators can review this information and make changes within their IdP or if necessary, [Create a SCIM Group](/api/management/tenants/scim/create-scim-group), [Update an Existing SCIM Group (adding new members)](/api/management/tenants/scim/update-scim-group), or [Delete an Existing SCIM Group](/api/management/tenants/scim/delete-scim-group) ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Load All External Groups for Specific Members (/api/management/tenants/groups/load-member-groups) ### Load all external group for specific members, using a valid management key. This API endpoint allows administrators to load all external groups for specific members associated with a specific tenant. The tenantId is required and the loginId or userId are optional for further filtering. The response contains an array of group objects including the group id, display name, and an array of associated members. ### Next Steps Administrators can review this information and make changes within their IdP or if necessary, [Create a SCIM Group](/api/management/tenants/scim/create-scim-group), [Update an Existing SCIM Group (adding new members)](/api/management/tenants/scim/update-scim-group), or [Delete an Existing SCIM Group](/api/management/tenants/scim/delete-scim-group) ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Update Tenant Password Settings (/api/management/tenants/passwords/configure-password-settings) ### Update password settings for a tenant This endpoint allows you to update the password settings of a given tenant. ### See Also - See [tenant password settings](/customize/tenant#passwords) for details about tenant password settings. # Get Tenant Password Settings (/api/management/tenants/passwords/get-password-settings) ### Get password settings for a tenant This endpoint allows you to get the password settings of a given tenant. ### See Also - See [tenant password settings](/customize/tenant#passwords) for details about tenant password settings. # Create SCIM Group (/api/management/tenants/scim/create-scim-group) ### Create a SCIM group, using a valid access key. This endpoint allows administrators to create new SCIM groups within their environement. When creating the group, you can configure the groupId, displayName, and it's members. The response includes the new group's group object which includes details about the groups including the members. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### Next Steps Once you have created the group, you can later add or remove users from the SCIM groups via [Update SCIM Group](/api/management/tenants/scim/update-scim-group). ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Delete SCIM Group (/api/management/tenants/scim/delete-scim-group) ### Delete an existing SCIM group, using a valid access key. This endpoint allows administrators to delete an existing SCIM group using using the SCIM groupId, which is a required field, and optionally the displayName. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Delete SCIM User (/api/management/tenants/scim/delete-scim-user) ### Delete an existing SCIM User, using a valid access key. This API endpoint allows administrators to delete an existing SCIM user from the Descope tenant. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # SCIM Management API Overview (/api/management/tenants/scim) Use the Descope API to manage your tenants' SCIM configurations with a management key. # SCIM Management API Overview ## Overview These APIs are specifically used by external IdPs, to perform actions relating to SCIM users. For User and Group management, refer to our [User Management](/api/management/users) or [Group Management](/api/management/tenants/groups) APIs. Descope provides inbound SCIM (System for Cross-domain Identity Management) support, allowing your Identity Provider (IdP) to push user and group data to Descope. Descopers can configure SCIM provisioning from their IdP, which will then be able to push user profile updates as well as groups. For a detailed audit trail of each SCIM call (request payload, results, and changes), see [SCIM Audit Events](/audit-trails-and-integrations/audit-events/scim-audit-events). The Descope API then opens endpoints to load, create, update, and delete SCIM-related configurations. The Descope SDK does not support the same SCIM endpoints as the API. ## Important Configuration Notes ### 1. Obtaining Project ID and Access Key In the Descope console, navigate to your project settings to retrieve the `ProjectId` and generate an `AccessKey` for SCIM configuration. Ensure that the access key has `tenant admin` privileges and is securely stored. ### 2. Authentication Header Format All SCIM API requests require a Bearer token in the following format: ``` Authorization: Bearer ProjectId:AccessKey ``` ### 3. Supported Attributes and Schema Each SCIM resource type has specific attributes that Descope supports, such as `emails`, `phoneNumbers`, and `displayName` for users. Review Descope’s schema documentation to ensure the IdP attributes align with Descope’s requirements. ## SCIM API Endpoints These are a list of the all of the endpoints you will find in provided by our management service. ### Base URL All SCIM requests to Descope should be directed to the following base URL, or your [custom domain](/how-to-deploy-to-production/custom-domain) if configured: ``` https://api.descope.com || ``` ### Group Management Endpoints #### **GET `/scim/v2/Groups`** **Purpose**: Search SCIM groups associated with the Descope application and tenant. **Description**: This endpoint allows administrators to view all SCIM groups within the Descope environment, including details of their members. **Use Case**: Use this endpoint to verify group memberships or synchronize group information. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **POST `/scim/v2/Groups`** **Purpose**: Create a new SCIM group. **Description**: This endpoint lets administrators create new groups and specify properties such as `groupId`, `displayName`, and members. **Use Case**: Use this to add groups in bulk or provision groups according to your organizational structure. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **GET `/scim/v2/Groups/{groupId}`** **Purpose**: Retrieve a specific SCIM group’s details using `groupId`. **Description**: Fetches details of a specified group, including `displayName` and members. **Use Case**: Retrieve precise information on a single group to manage group access or verify membership details. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **PUT `/scim/v2/Groups/{groupId}`** **Purpose**: Update an existing SCIM group’s details. **Description**: Modify properties such as `displayName` and members by specifying the `groupId`. **Use Case**: Adjust groups to ensure they reflect the latest structure or access needs. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **DELETE `/scim/v2/Groups/{groupId}`** **Purpose**: Delete a SCIM group using `groupId`. **Description**: Removes the specified group from the Descope tenant. **Use Case**: Clean up outdated or redundant groups to maintain an organized environment. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **PATCH `/scim/v2/Groups/{groupId}`** **Purpose**: Partially update a SCIM group’s details. **Description**: Use this to modify only specific attributes of a group, such as updating its members or `displayName`. **Use Case**: Quickly apply minor updates to a group without overwriting its full configuration. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- ### User Management Endpoints #### **GET `/scim/v2/Users/{userId}`** **Purpose**: Retrieve a specific SCIM user’s details using `userId`. **Description**: Access detailed profile information such as `email`, `phone`, `username`, and other attributes. **Use Case**: Check or validate user data against the IdP to ensure accurate records. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **PUT `/scim/v2/Users/{userId}`** **Purpose**: Update a SCIM user’s information. **Description**: Modify user attributes, including `displayName`, `phoneNumbers`, `emails`, and active status. **Use Case**: Keep user profiles up-to-date by synchronizing changes from the IdP. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **DELETE `/scim/v2/Users/{userId}`** **Purpose**: Delete a SCIM user. **Description**: Remove a user from Descope’s environment to reflect changes in the IdP. **Use Case**: Deprovision users who no longer require access. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- ### Metadata Endpoints #### **GET `/scim/v2/ResourceTypes`** **Purpose**: Retrieve available SCIM resource types. **Description**: Lists the types of SCIM resources supported within Descope. **Use Case**: Familiarize yourself with the resources that can be provisioned via SCIM. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. --- #### **GET `/scim/v2/ServiceProviderConfig`** **Purpose**: Retrieve SCIM service provider configuration. **Description**: Provides detailed information on supported configurations and schemas for SCIM provisioning. **Use Case**: Use this information to align your IdP’s configuration with Descope’s requirements. **Authorization**: Bearer token format: `ProjectId:AccessKey`, requiring `tenant admin` privileges. This guide provides the foundation for configuring SCIM provisioning between Descope and your IdP, helping maintain accurate user and group data across systems. For further assistance, consult the Descope API documentation or contact support. # Load SCIM Group (/api/management/tenants/scim/load-scim-group) ### Load an existing SCIM group, using a valid access key. This endpoint allows administrators to load an existing SCIM group using the SCIM groupId, which is a required field, and optionally the displayName. The response includes the group's object which includes details about the groups including the members. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### Next Steps Once you have this data, you can add or remove users from the SCIM groups via [Update SCIM Group](/api/management/tenants/scim/update-scim-group). ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Load SCIM Resource Types (/api/management/tenants/scim/load-scim-resource-types) ### Load SCIM resource types, using a valid access key. This API endpoint allows administrators to load the resource types available within the SCIM provisioning. The response includes an array of the available resource types. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Load SCIM Service Provider Config (/api/management/tenants/scim/load-scim-service-provider-config) ### Load the supported SCIM provisioning service provider configuration, using a valid access key. This API endpoint allows administrators to load the supported SCIM provisioning service provider configuration. The response includes detailed information on the applicable configurations and schemas within your IdP for SCIM provisioning. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Load SCIM User (/api/management/tenants/scim/load-scim-user) ### Load an existing SCIM user, using a valid access key. This endpoint allows administrators to load an existing SCIM user. The response includes the user's object, which includes details about the users including their email, phone, username, name, etc. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### Next Steps Once you have user data, you can utilize [Update SCIM Group](/api/management/tenants/scim/update-scim-group) to add or remove the user on groups. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Patch SCIM Group (/api/management/tenants/scim/patch-scim-group) Patch SCIM Group, using a valid access key. # Patch SCIM User (/api/management/tenants/scim/scim-patch-user) Patch SCIM User, using a valid access key. # Search SCIM Groups (/api/management/tenants/scim/search-scim-groups) ### Search SCIM groups, using a valid access key. This endpoint allows administrators to search SCIM groups. These groups have been created and associated to the Application and Descope tenant. The response includes an array of group objects within the Resources object. These group objects include details about the groups including the members. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and has the tenant admin role. ### Next Steps Once you have this data, you can [Update an Existing SCIM Group](/api/management/tenants/scim/update-scim-group) or [Delete an Existing SCIM Group](/api/management/tenants/scim/delete-scim-group). You can add or remove users from the SCIM groups via [Update SCIM Group](/api/management/tenants/scim/update-scim-group). ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Search SCIM Users (/api/management/tenants/scim/search-scim-users) ### Search SCIM users, using a valid access key. This endpoint allows administrators to search SCIM users. These users have been created and associated to the Application and Descope tenant. The response includes an array of user objects within the Resources object. These user objects include details about the users including their email, phone, username, name, etc. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### Next Steps Once you have user data, you can utilize [Update SCIM Group](/api/management/tenants/scim/update-scim-group) to add or remove the user on groups. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Update SCIM Group (/api/management/tenants/scim/update-scim-group) ### Update an existing SCIM group, using a valid access key. This endpoint allows administrators to update an existing SCIM group using the SCIM group ID, which is a required field. You can update the display name and members through this API endpoint. The response includes the group's object which includes details about the groups including the members. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Update SCIM User (/api/management/tenants/scim/update-scim-user) ### Update an existing SCIM user, using a valid access key. This endpoint allows administrators to update an existing SCIM user. Through this API endpoint, administrators can update the displayName, phoneNumbers, emails, and if the user is active. The response includes the user's object, which includes details about the users including their email, phone, username, name, etc. It is important to note the bearer token for SCIM API endpoints. The format is `ProjectId:AccessKey` the access key must be associated with the applicable tenant and associated with the tenant admin role. ### See also - See [SCIM Management](/scim) for further details on managing SCIM provisioning. # Update Tenant Session Settings (/api/management/tenants/session/configure-tenant-settings) ### Update session settings for a tenant This endpoint allows you to update the session settings of a given tenant. ### See Also - See [tenant session settings](/customize/tenant#session-management) for details about tenant session settings. # Get Tenant Session Settings (/api/management/tenants/session/get-tenant-settings) ### Get session settings for a tenant This endpoint allows you to get the session settings of a given tenant. ### See Also - See [tenant session settings](/customize/tenant#session-management) for details about tenant session settings. # Enable or disable an SSO configuration (/api/management/tenants/sso/configure-sso-auth-type) Set the authentication type of a tenant SSO configuration, using a valid management key. Setting it to none disables the configuration without deleting it, and setting it back to saml or oidc re-enables it with its stored settings. # Set Tenant's OIDC Settings (/api/management/tenants/sso/configure-sso-oidc-settings) ### Configure the OIDC settings of a tenant, using a valid management key. This API endpoint will configure the OIDC settings on a tenant utilizing a valid management key. This endpoint accepts the OIDC configuration settings as well as the attribute mapping you would like to be configured on the SAML settings. ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Configure SSO Redirect URL (/api/management/tenants/sso/configure-sso-redirect-url) Configure tenant SSO Redirect URL, using a valid management key. # Set Tenant's SAML Settings via Metadata URL (/api/management/tenants/sso/configure-sso-saml-settings-by-metadata) ### Configure the SAML Metadata URL, using a valid management key. This API endpoint will configure the SAML Metadata URL on a tenant utilizing a valid management key. This API endpoint accepts idpMetadataURL which will be applied to the tenant under SSO Configuration section and will select the option to "Retrieve the connection details dynamically using a metadata URL" This endpoint also accepts the attribute mapping you would like to be configured on the SAML settings. This Metadata URL can can be obtained from the admin console of the identity provider. Configuring SAML via Metadata URL allows administrators to configure SAML without applying these setting manually via [Configure SAML Settings](/api/management/tenants/sso/configure-sso-saml-settings) ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Set Tenant's SAML Settings (/api/management/tenants/sso/configure-sso-saml-settings) ### Configure the SAML Settings, using a valid management key. This API endpoint will configure the SAML settings on a tenant utilizing a valid management key. This API endpoint accepts idpURL, entityId, idpCert, and redirectURL which will be applied to the tenant under SSO Configuration section and will select the option to "Enter the connection details manually" This endpoint also accepts the attribute mapping you would like to be configured on the SAML settings. These configurations will need to be captured directly from your idp provider. The values for each field can be obtained from the admin console of the identity provider. Alternatively, administrators can configure SAML without applying these setting manually via [Configure SAML Metadata URL](/api/management/tenants/sso/configure-sso-saml-settings-by-metadata) ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Create New SSO Settings (/api/management/tenants/sso/create-sso-settings) ### Create new SSO settings for a tenant, using a valid management key. This API endpoint allows you to create a new SSO configuration for a tenant. The endpoint accepts the tenant ID, an optional SSO ID, and a display name for the SSO configuration. ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Delete Tenant's SAML/OIDC Settings (/api/management/tenants/sso/delete-sso-settings) ### Delete the current SAML/OIDC configuration settings of a tenant, using a valid management key. This API endpoint allows you to delete the current SAML/OIDC configuration settings of a tenant. Use this with caution as this endpoint deletes the configuration and is irreversible. ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Get SSO settings (/api/management/tenants/sso/get-sso-settings) Get the project's SSO settings, using a valid management key. # SSO Management API Overview (/api/management/tenants/sso) Use the Descope API to manage your tenants' SSO configurations with a management key. # SSO Management API Overview ## Overview The SSO Management APIs let you programmatically manage tenant SSO configurations using a management key. Management keys are generated from **Company > Management Keys**. Include the key in the `Authorization` header as a bearer token in the format `:`. ## Use Cases 1. [Get Tenant SAML/OIDC Settings](/api/management/tenants/sso/load-sso-settings) 2. [Configure SAML Settings](/api/management/tenants/sso/configure-sso-saml-settings) 3. [Configure SAML Metadata URL](/api/management/tenants/sso/configure-sso-saml-settings-by-metadata) 4. [Configure OIDC Settings](/api/management/tenants/sso/configure-sso-oidc-settings) 5. [Delete SSO Settings](/api/management/tenants/sso/delete-sso-settings) ## Examples ### Example - configure SSO via manual configuration 1. Call the [Configure SSO Settings](/api/management/tenants/sso/configure-sso-saml-settings) API endpoint to apply `idpURL`, `entityId`, `idpCert`, and `redirectURL` to a tenant. ### Example - configure SSO via metadata URL 1. Call the [Configure SSO Metadata URL](/api/management/tenants/sso/configure-sso-saml-settings-by-metadata) API endpoint to apply the `idpMetadataURL` to a tenant. # Load all SSO Settings for a tenant (/api/management/tenants/sso/load-all-sso-settings) Load all SSO Settings for a tenant, using a valid management key. # Get Tenant's SAML/OIDC Settings (/api/management/tenants/sso/load-sso-settings) ### Get the current SAML/OIDC configuration settings of a tenant, using a valid management key. This API endpoint allows you to get the current SAML/OIDC configuration settings of a tenant. ### See also - See [SSO Configuration](/sso) for further details on managing SSO Configurations on a tenant. # Recalculate SSO Mappings (/api/management/tenants/sso/recalculate-sso-mappings) Recalculate SSO group to role mappings for all users in a tenant, using a valid management key. # Set SSO settings (/api/management/tenants/sso/set-sso-settings) Set the project's SSO settings, using a valid management key. # Create a Custom Attributes (/api/management/users/custom-attributes/create-user-custom-attribute) ### Create a custom attributes to configure on users within a project, using a valid management key. This API endpoint will create a custom attribute within a project. ### See also - See [Custom Attributes](/manage/users#custom-user-attributes) for further details on custom user attributes - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Delete a Custom Attributes (/api/management/users/custom-attributes/delete-user-custom-attribute) ### Delete a custom attributes to configure on users within a project, using a valid management key. This API endpoint will delete a custom attribute within a project. ### See also - See [Custom Attributes](/manage/users#custom-user-attributes) for further details on custom user attributes - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Get Available Custom Attributes (/api/management/users/custom-attributes/user-custom-attributes) ### Get available custom attributes to configure on users within a project, using a valid management key. This API endpoint will return the available user custom attributes within a project. ### See also - See [Custom Attributes](/manage/users#custom-user-attributes) for further details on custom user attributes - See [Manage Users](/manage/users) for further details on managing users. - See [The User Object](/api/overview#the-user-object) for further details on the user object. # Delete All Test Users (/api/management/users/test-users/delete-all-test-users) ### Delete all test users This endpoint is used to delete all test users from a project. This action will delete these users forever and they will not be recoverable. ### See Also - See [Manage Test Users](/manage/testusers/) for more information on test users. # Generate Enchanted Link (/api/management/users/test-users/generate-enchanted-link) ### Generate a Enchanted Link for a test user. This endpoint is used to generate a Enchanted Link for a test user. You can define whether this is sent via email or sms. Once you generate the Enchanted Link Token must be verified via [verify token](/api/enchanted-link/verify-token) ### See Also - See [Enchanted link Authentication](/api/enchantedlink/) for details about implementing enchanted links. - See [Create a user](/api/management/users/create-user) with the `test` flag set to true to set it as a test user. - See [Manage Test Users](/manage/testusers/) for more information on test users. # Generate Magic Link (/api/management/users/test-users/generate-magic-link) ### Generate a Magic Link for a test user. This endpoint is used to generate a Magic Link for a test user. You can define whether this is sent via email or sms. Once you generate the Magic Link Token must be verified via [verify token](/api/magic-link/verification/verify-token) ### See Also - See [Magic link Authentication](/api/magiclink/) for details about implementing magic links. - See [Create a user](/api/management/users/create-user) with the `test` flag set to true to set it as a test user. - See [Manage Test Users](/manage/testusers/) for more information on test users. # Generate OTP (/api/management/users/test-users/generate-otp) ### Generate an OTP verification code for a test user. This endpoint is used to generate an OTP verification code for a test user. You can define whether this is sent via email or sms. Once you generate the OTP code, you must verify the OTP code via [verify OTP email](/api/otp/email/verify-otp) or [verify OTP sms](/api/otp/sms/verify-otp) ### See Also - See [OTP Authentication](/api/otp/) for details about implementing OTP. - See [Create a user](/api/management/users/create-user) with the `test` flag set to true to set it as a test user. - See [Manage Test Users](/manage/testusers/) for more information on test users. # Test User Management API Overview (/api/management/users/test-users) Use the Descope API to manage your application's test users with a management key. # Test User Management API Overview ## Overview Using the test user management APIs enables administrators to manage their test users utilizing a management key. This allows administrators to generate login details to test the user authentication flow. Management keys are generated from Company > Management Keys. These keys will be used within the bearer token. The format is `:`. ## Use Cases Test User Management: 1. Create test Users 2. Generate OTP (sms/email) for test users 3. Generate Magic Link (sms/email) for test users 4. Generate Enchanted Link (email) for test users 5. Delete Test Users ## Examples ### Example - Create Test User and Validate Authentication Functionality with OTP 1. [Create a user](/api/management/users/create-user) with the `test` flag set to true to set it as a test user. 2. [Generate an OTP code for the test user](/api/management/users/test-users/generate-otp), when you generate this, you can set it to sms or email for the delivery method. 3. Verify the OTP code via [verify OTP email](/api/otp/email/verify-otp) or [verify OTP sms](/api/otp/sms/verify-otp) 4. [Delete the test user](/api/management/users/delete-user) or [delete all test users](/api/management/users/test-users/delete-all-test-users) ### Example - Create Test User and Validate Authentication Functionality with Enchanted Link 1. [Create a user](/api/management/users/create-user) with the `test` flag set to true to set it as a test user. 2. [Generate an Enchanted Link for the test user](/api/management/users/test-users/generate-enchanted-link). 3. [Verify the token](/api/enchanted-link/verify-token) 4. [Delete the test user](/api/management/users/delete-user) or [delete all test users](/api/management/users/test-users/delete-all-test-users) # Apple (/auth-methods/oauth/providers/setting-up-your-own-apps/apple) This guide covers how to create a custom Apple login and integrate it within Descope. # Custom Social Login with Apple When integrating Descope flows in a website, often you'll want to allow Social Login with Apple as one of the authentication methods for your users. However, when you sign in with the flow, the Apple sign-in page will not be personalized to your website domain by default. This tutorial will cover creating your custom Apple OAuth-based login so that the login process is personalized to your domain. ![Apple login now shows custom provider name within Descope flow](/assets/apple-login-custom-flow.webp) ## Default Apple Login Configuration The default Descope login app looks like the below within the [Authentication Methods](https://app.descope.com/settings/authentication/social) page within the Descope console for the Apple provider. ![Descope custom social login with Apple, customize Apple OAuth](/assets/apple-custom-login-default.webp) However, if you use the Descope login app, the sign-in page will default to show `Create an account for Descope using your Apple ID "example@company.com".` ![Descope custom social login with Apple, Descope signin example](/assets/apple-login-default.webp) ## Customizing the Apple Login ### Configuring the CNAME You will need to create to enable a custom domain within your environment. If you still need to configure the custom domain, review the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide for a step-by-step guide for configuring the custom domain and managing sessions within cookies. ### Apple Configuration Once you have a project with a configured custom domain, you can create the necessary items within Apple's developer portal. #### Create Application ID Once logged into your Apple developer account, go to [Certificates, Identifiers & Profiles](https://developer.apple.com/account/resources/identifiers/list/bundleId). Here, you will start by creating our Application ID. Click the `+` next to `Identifiers` and select `App IDs`. ![Descope - create Apple application ID for custom social login](/assets/create-apple-app-id.webp) After clicking continue to the next page, select the type as `App` and click continue. ![Descope - create Apple application ID for custom social login 2](/assets/create-apple-app-id-2.webp) Now configure the `Description` and the `Bundle Id`. If your domain is `mycompany.com`, the `Bundle ID` should be configured as `com.mycompany`. ![Descope - create Apple application ID for custom social login 3](/assets/create-apple-app-id-3.webp) While still on the same page, scroll down and check the `Sign In with Apple` box. You can click the `Edit` button depending on your configuration to select whether it is grouped to an existing primary App ID. Within this example, it is configured to `Enable as a primary App ID`. ![Descope - create Apple application ID for custom social login 4](/assets/create-apple-app-id-4.webp) Now click continue and then register to complete the application ID registration. #### Create Service ID Now go to [Service Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) and ensure from the top right drop-down you are under `Services IDs`. We will then click `+` next to `Identifiers` and select `Services IDs`. ![Descope - create Apple service ID for custom social login](/assets/create-apple-service-id.webp) After clicking continue, you must configure your `Description` (which will be what the user sees during the Apple login process) and the `Identifier`. The identifier should be the CNAME configured for your application when stepping through the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide. Since our domain for this example is `mycompany.com`, our CNAME would likely be `auth.mycompany.com`, so our `Identifier` will be `com.mycompany.auth`. After configuring, click continue and register to complete the service ID registration. ![Descope - create Apple service ID for custom social login 2](/assets/create-apple-service-id-2.webp) #### Create Key Go to [Keys](https://developer.apple.com/account/resources/authkeys/list) and click the `+` next to `Keys`. Here you will give the key a name and check the box for `Sign in with Apple`. ![Descope - create Apple key for custom social login](/assets/create-apple-key.webp) Then click the edit icon to the right of `Sign in with Apple`. Then select your primary application ID, which you created [here](/auth-methods/oauth/providers/setting-up-your-own-apps/apple#create-application-id). Then click save, continue, and then register. ![Descope - create Apple key for custom social login 2](/assets/create-apple-key-2.webp) On the next screen, take note of Key ID and download the key. ![Descope - create Apple key for custom social login 3](/assets/create-apple-key-3.webp) This key for your Apple app has a validity of only 6 months. Please make sure to regenerate a new key after its expiry. This limit is non configurable which means it cannot be extended beyond 6 months. To avoid any service interruptions, we recommend setting a reminder well before the key expires to regenerate it on time. #### Update the Service ID Now go to [Service Identifiers](https://developer.apple.com/account/resources/identifiers/list/serviceId) and select the key you made above, then check the box for `Sign in with Apple` and click configure. Here, you will add any domains and subdomains associated with your application. For this example, the domains and subdomains would include minimally `mycompany.com` and `auth.mycompany.com`. Then you will also configure the callback URLs, which would also minimally be `https://auth.mycompany.com/v1/auth/oauth/callback` and `https://auth.mycompany.com/v1/oauth/callback`. ![Descope - update service ID with domains, subdomains, and callback URLs](/assets/update-apple-service-id.webp) Then click done, continue, etc, until you have saved your Service ID. This completes the configuration within Apple. #### Register domain for private relay In cases where the user signs up with an option to hide their email, if correct domains are not setup, the email through relay service tends to not be sent to users' inboxes. There needs to be a way to handle apple's private email relay service. For this to be handled, go to [this](https://developer.apple.com/account/resources/services/list) link and configure the applicable domains: `auth.custom_domaim_cname.com` & `custom_domaim_cname.com`. For more information, refer to [this](https://developer.apple.com/help/account/configure-app-capabilities/configure-private-email-relay-service)link. ![Apple relay service](/assets/apple-relay.webp) ### Configure Descope Once you have successfully created your application within your Apple Developer account, it's time to configure your application within Descope in the [Authentication Methods](https://app.descope.com/settings/authentication/social) page. #### Automatic JWT rotation (recommended) **Automatic JWT rotation** uses your Apple credentials (for example, Team ID, Key ID, and private key) so Descope can **automatically** rotate the client secret JWT for you. This is the recommended way to use Apple as a social provider with your own Apple developer account, and you do not need to run scripts or manually regenerate the client secret before it expires. When configuring your Apple OAuth default provider, select the `Automatic JWT rotation` option, as opposed to `Manual JWT rotation`, and provide the credentials requested (aligned with the Application ID, Service ID, and key you created above). ![Apple automatic JWT rotation](/assets/apple-jwt-rotation.webp) #### Manual JWT rotation Alternatively, you can generate the client secret yourself with a script. You will need to decrypt the key to get the client secret. Once you have the decrypted client secret, continue with the configuration within the [Authentication Methods](https://app.descope.com/settings/authentication/social) page. **Decrypt the key** Below, you can find example scripts for Node.js and Python, allowing you to decrypt the key to get the client secret. Install dependencies ```sh title="Terminal" npm install jsonwebtoken ``` ```sh title="Terminal" pip install python-jose pyopenssl ``` **Decryption Scripts** Update the placeholders within the script with your `KeyID` which was generated when you [created the key](#create-key), your `ClientID` which is the `Identifier` of the [created service ID](#create-service-id), and your `TeamID` which can be found at the top right of your Apple Developer console. ```javascript const jwt = require('jsonwebtoken'); const fs = require('fs'); // Replace the placeholders with your actual values const keyFilePath = './AuthKey_.p8'; const teamId = ''; const clientId = ''; const keyId = ''; // Read the ECDSA key const ecdsaKey = fs.readFileSync(keyFilePath, 'utf8'); // Define the headers and claims const headers = { 'kid': keyId }; const currentTime = Math.floor(Date.now() / 1000); const claims = { 'iss': teamId, 'iat': currentTime, 'exp': currentTime + (180 * 24 * 60 * 60), // 180 days in seconds 'aud': 'https://appleid.apple.com', 'sub': clientId }; // Encode the JWT const token = jwt.sign(claims, ecdsaKey, { algorithm: 'ES256', header: headers }); // Print the client ID and token console.log(`client_id: ${clientId}`); console.log(`client_secret: ${token}`); ``` ```python from jose import jwt from datetime import datetime, timedelta import OpenSSL # Replace the placeholders with your actual values key_file_path = "./AuthKey_.p8" team_id = "" client_id = "" key_id = "" # Read the ECDSA key with open(key_file_path, 'r') as key_file: ecdsa_key = key_file.read() # Define the headers and claims headers = { 'kid': key_id } claims = { 'iss': team_id, 'iat': int(datetime.now().timestamp()), 'exp': int((datetime.now() + timedelta(days=180)).timestamp()), 'aud': 'https://appleid.apple.com', 'sub': client_id, } # Encode the JWT token = jwt.encode(claims, ecdsa_key, algorithm='ES256', headers=headers) # Print the client ID and token print(f"client_id: {client_id}") print(f"client_secret: {token}") ``` #### Managing Tokens from Provider You can choose to **Manage tokens from provider** if you want Descope to store the provider's access token in an encrypted state for later use. ![Apple provider configured within the Descope Console](/assets/apple-custom-login-configured.webp) # Facebook (/auth-methods/oauth/providers/setting-up-your-own-apps/facebook) Learn to integrate Descope flows by adding a Facebook Social Login. This guide covers creating and configuring a custom app to display your website's domain. # Custom Social Login with Facebook When integrating Descope flows in a website, often you'll want to allow Social Login with Facebook as one of the authentication methods for your users. However, when you actually sign in with the flow, the Facebook sign-in page will not be personalized to your website domain by default. In order to get that to work, you'll need to make a few changes and create a Facebook Social Login app. This tutorial will explain all the steps on how to do that. This guide is specifically for Facebook Social Login ## Current Default Login Page If you use the default Descope login app, the sign-in consent screen will default to show [Descope](http://descope.com/) as the app name, instead of showing your application name. ![Descope custom social login with Facebook, Descope signin example](/assets/facebook-login-default.webp) Instead of displaying Descope, this guide will show you how to personalize your Facebook Sign-In Page to display your own company domain and name instead. ## Solution To make this happen, you'll need to configure a few things on the Facebook side and the Descope side, as well as configure a new CNAME in your domain's DNS records. ## Configuring the CNAME You will need to create a [custom domain](/how-to-deploy-to-production/custom-domain) that points to `cname.descope.com` (US) / `CNAME.euc1.descope.com` (EU), so that if the redirect URI is `auth.example.com` for instance, the Facebook page will show Sign in to continue to with `example.com` instead of `descope.com`. See the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide for a step-by-step guide for managing sessions within cookies. Once that is completed, you will need to create a new public Facebook app. ## Creating a new Facebook App and Configuring Descope 1. You will need to follow the steps on the [Developer Facebook Docs](https://developers.facebook.com/docs/development/register), to create an OAuth Facebook login app with your personalized settings and logo. 2. When going through all of the steps, you'll need to add scopes. This tells Facebook what information the app can access about the user that is signing in. If you want to learn more about the specific scopes, you can do so on the [Developer Facebook Docs](https://developers.facebook.com/docs/permissions) You MUST check *public_profile*, and we highly recommend using the email permission as well (otherwise Descope will not have access to the user's email and will not be able to create a login ID from it). ![Descope custom social login with Facebook, creating Facebook application](/assets/facebook-login-create-app.webp) Only apps that are verified by Facebook (after going through the verification process) can display a custom logo and use a custom app name. Otherwise, the login page will display the redirect URL domain. More information can be found on the [Developer Facebook Docs](https://developers.facebook.com/docs/resp-plat-initiatives/app-review). Once everything has been approved, you should be able to switch the app to Live Mode, and your app should be published in production for anyone to see. If you got confused with any of the steps, there is an [FAQ](https://developers.facebook.com/docs/development/support) that should be able to help answer any questions you may have had while creating the app. 4. To get the App ID and App Secret for your new app, click on **App Settings** in the left-hand sidebar and select **Basic**. Then, copy the **App ID** and **App Secret** from the screen. ![Descope custom social login with Facebook, Facebook application App ID and Secret](/assets/facebook-login-app-id-secret.webp) 5. Go to the [Descope Console](https://app.descope.com/settings/authentication/social), click on **Facebook**, and then select **Use my own account** underneath Authentication account. From Facebook, copy over the **App ID** and **App Secret** (from the Facebook Developer Console) ![Descope custom social login with Facebook, configuring within Descope](/assets/facebook-login-descope-config.webp) Make sure to leave that Descope Console window open, as you'll need it again in the final steps. 6. Next, you'll need to set up the **OAuth Redirect URI** in Facebook. While in the Descope Console, scroll to the bottom of the Facebook social app configuration page and copy the **OAuth Callback URL** to the clipboard. ![Descope custom social login with Facebook, creating Facebook application redirect URI](/assets/facebook-login-redirect-uri.webp) Then in the Facebook Developer Console, click on **Add Product**, under **Products**, in the left-hand side bar. Add the **Facebook Login** option and select **Settings**. Under **Valid OAuth Redirect URIs**, enter the **redirect URI** you obtained from the Descope Console. Your Facebook settings page should look something like this: ![Descope custom social login with Facebook, creating Facebook application redirect URI](/assets/facebook-login-redirect-uri-facebook.webp) Now that you've created a Facebook App and have it published and approved by Facebook, you should see your own personalized app page with your custom application name. If you have any questions, please reach out to us [here](/support)! # GitHub (/auth-methods/oauth/providers/setting-up-your-own-apps/github) Learn how to integrate GitHub as a custom OAuth provider in Descope, with specific guides for both OAuth Apps and GitHub Apps. # Custom Social Login with GitHub When you sign in with GitHub with Descope by default, the GitHub sign-in page will not be personalized to your website domain by default. In order to get that to work, you'll need to configure GitHub as a social login provider. This guide provides a detailed walkthrough for setting up GitHub as a social login provider, covering both GitHub Apps and OAuth Apps. ## Differences Between GitHub Apps and OAuth Apps GitHub offers two types of applications for integrations: GitHub Apps and OAuth Apps. The primary difference lies in the permission granularity and installation flow. GitHub Apps offer more granular permissions and are installed on a specific part of the user's GitHub account, whereas OAuth Apps are simpler and are authorized by the user for specific scopes. For Descope to function properly with GitHub Apps, **permissions to read the user's email are required**. This guide will cover the setup process for both application types. ## Creating the GitHub Application ### For OAuth Apps 1. **Create a New OAuth App**: Go to your GitHub settings, navigate to the Developer settings, and create a new OAuth application. 2. **Configure Callback Url**: Set `https://api.descope.com/v1/oauth/callback` as the redirect URI. If you're using a Custom Domain, then you can find this in the GitHub configuration page in the [Descope Console](https://app.descope.com/settings/authentication/social), under `Github` -> `Use my own account`: ![Custom Domain](/assets/github-custom-domain.webp) 3. **Generate Client Secret**: Select `Generate a new client secret`, and copy the secret value for use in the Descope Console. 4. **Note Client ID**: After creation, note the client ID for use in the Descope Console. ![Github Client ID and secret](/assets/github-client-id-secret.webp) ### For GitHub Apps 1. **Create a New GitHub App**: In your GitHub settings, under Developer settings, create a new GitHub App. 2. **Set Permissions**: Ensure you set the read permission for the user's email. ![Github permissions scopes](/assets/github-app-permissions.webp) 3. **Configure Callback Url**: Similar to OAuth Apps, use `https://api.descope.com/v1/oauth/callback`. If you're using a Custom Domain, then you can find this in the GitHub configuration page in the [Descope Console](https://app.descope.com/settings/authentication/social), under `Github` -> `Use my own account`: ![Custom Domain](/assets/github-custom-domain-2.webp) 4. **Generate Client Secret**: Select `Generate a new client secret`, and copy the secret value for use in the Descope Console. 5. **Note Client ID**: After creation, note the client ID for use in the Descope Console. ![Github Client ID and secret](/assets/github-client-id-secret-2.webp) ## Configuring GitHub as a Social Login Provider in Descope To configure GitHub apps or OAuth in Descope, the process is the same. Simply head to [Social Login (OAuth / OIDC)](https://app.descope.com/settings/authentication/social), under Authentication Methods, and then select `GitHub` -> `Use my own account`. ![Github as OAuth method Descope setup](/assets/github-descope-setup.webp) Then under the configuration page, do the following: ### Authentication Account - Set `Provider Name` to `GitHub OAuth`. - Enter the `Client ID` and `Client Secret` from the GitHub OAuth App settings. - Configure scopes as needed (e.g., `user:email` for email access). More information about scopes supported can be found on GitHub's [documentation page](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps). ### Advanced Settings Configure additional options such as token management, and redirect URLs as per your application's requirements. # Google (/auth-methods/oauth/providers/setting-up-your-own-apps/google) When integrating Descope flows in a website, often you'll want to allow Social Login with Google as one of the authentication methods for your users. # Custom Social Login with Google When integrating Descope flows in a website, often you'll want to allow Social Login with Google as one of the authentication methods for your users. However, when you actually sign in with the flow, the Google sign-in page will not be personalized to your website domain by default. In order to get that to work, you'll need to make a few changes and create a Google Social Login app. This tutorial will explain all the steps on how to do that. This guide is specifically for Google Social Login ## Current Default Login Page The default Descope login app looks like [this](https://app.descope.com/settings/authentication/social) (Click on Google): ![Descope custom social login with google, customize Google OAuth](/assets/google-custom-login-default.webp) However, if you use the Descope login app, the sign in page will default to show ***Sign in to continue to [Descope](http://descope.com/)***. ![Descope custom social login with google, Descope signin example](/assets/google-custom-login-example.webp) Instead of displaying **Descope** and our logo, this guide will show you how to personalize your Google Sign-In Page to display your own company logo and domain instead. ## Solution To make this happen, you'll need to configure a few things on the Google side and the Descope side, as well as configure a new CNAME in your domain's DNS records. ## Configuring the CNAME You will need to create a [custom domain](/how-to-deploy-to-production/custom-domain) that points to `cname.descope.com` (US) / `CNAME.euc1.descope.com` (EU), so that if the redirect URI is `auth.example.com` for instance, the Google page will show **Sign in to continue to with example.com** instead of **descope.com**

See the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide for a step by step guide for managing sessions within cookies. Once that is completed, you will need to create a new public Google app. ## Creating a new Google App 1. You will need to follow the steps [here](https://support.google.com/cloud/answer/6158849?hl=en), to create an OAuth Google login app with your personalized settings and logo. 2. When going through all of the steps, you'll need to add scopes. This tells Google what information the app can access about the user that is signing in. If you want to learn more about the specific scopes, you can do so [here](https://support.google.com/cloud/answer/13463073) You **MUST** check ***openid***, and we highly recommend using the ***userInfo.email*** and ***userInfo.profile*** scopes as well: ![Descope custom social login with google, creating google application 1](/assets/google-custom-login-create-app-1.webp) 3. Once the app has been created, you should head back to your dashboard to see this: ![Descope custom social login with google, creating google application 2](/assets/google-custom-login-create-app-2.webp) Now click on **Publish App**, and your app should be published in production for anyone to see. Only apps that are verified by Google (after going through the verification process) can display a custom logo and use a custom app name. Otherwise, the login page will display the redirect URL domain. More information can be found [here](https://support.google.com/cloud/answer/10311615?hl=en). If you got confused with any of the steps, there is an FAQ [here](https://support.google.com/cloud/answer/13463073) that should be able to help answer any questions you may have had while creating the app. 4. To get the client ID and secret for your new app, click on **Credentials** in the left-hand sidebar and select **+ Create Credentials** ![Descope custom social login with google, google application client and secret](/assets/google-custom-login-create-app-3.webp) 5. Select **OAuth Client ID**, and then select the application type you're using Descope with. Then, fill in the rest of the information below, and click **Create**: ```text 1. Name - add your own name 2. Authorized JavaScript - add your website url - Example: http://www.example.com 3. Authorized redirect URIs - ``` It's important to make sure that the authorized redirect URI is configured here properly, as this needs to match what is in the Descope console for the OAuth callback. You can get URI by heading to the Descope console [here](https://app.descope.com/settings/authentication/social), and clicking on **Google** -> **Use my own account** (under Authentication account). Don't worry about the rest of the fields for now, just enter the CNAME you created in the first step, under **OAuth Callback CNAME**, and you should see the **URL to register in your social app** change. That will be your authorized redirect URI. Copy that URL over, and paste it in the Google developer console. Also, make sure to leave that Descope Console window open, as you'll need it again in the final steps. Your page should look something like this: ![Descope custom social login with google, creating google application client for web application](/assets/google-custom-login-create-app-4.webp) 6. Copy your client ID and the secret to the clipboard, and we should be done with Google! ![Descope custom social login with google, creating google application client for web application done](/assets/google-custom-login-create-app-5.webp) Now that you've created a Google App and have it published and approved by Google, you can wrap this up by changing a few simple things in the Descope Console. ## Descope Configuration 1. Go to the [Descope Console](https://app.descope.com/settings/authentication/social), click on **Google**, and then select **Use my own account** underneath Authentication account: ![Descope custom social login with google, configuring within Descope 1](/assets/google-custom-login-config-1.webp) 2. From Google, copy over the **Client ID and Client Secret** (from the Google Developer Console), and **OAuth CNAME** you've configured in your DNS and paste them into the respective fields. 3. Make sure that your CNAME is present in the **OAuth Callback CNAME** field. This should have been done in step 5, of setting up the Google App. 4. Finally, as if you were using the Descope app instead of your own custom app, put the **redirect URL** you wish to redirect your users to after they have successfully logged in. If you have already set a redirect URL with a Descope SDK, this field is not required to be set. ![Descope custom social login with google, configuring within Descope 2](/assets/google-custom-login-config-2.webp) And you're done! Now when you sign in, you should see your own personalized app page with your custom domain name. If you have any questions, please reach out to us [here](/support)! # Microsoft (/auth-methods/oauth/providers/setting-up-your-own-apps/microsoft) When integrating Descope flows, often you'll want to allow Social Login with Microsoft as one of the authentication methods for your users. # Custom Social Login with Microsoft Having issues with making Microsoft social authentication work? Please refer to this guide on [social providers with unverified emails](/auth-methods/oauth/customize/handle-oauth-provider-unverified-emails). When integrating Descope flows in a website, often you'll want to allow Social Login with Microsoft as one of the authentication methods for your users. You'll need to create a Microsoft enterprise application and integrate it with Descope. This doc will walk you through all the steps to do that. This guide is specifically for Microsoft Social Login ## Current Default Login Page The default Descope login app looks like [this](https://app.descope.com/settings/authentication/social) (Click on Microsoft): ![Descope custom social login with Microsoft, customize Microsoft OAuth](/assets/microsoft-custom-login-customize.webp) However, if you use the Descope login app, the sign in page will default to show ***Sign in to continue to [Descope](http://descope.com/)***. ![Descope custom social login with microsoft, Descope signin example](/assets/microsoft-custom-login-example.webp) Instead of displaying **Descope** and our logo, this guide will show you how to personalize your Microsoft Sign-In Page to display your own company logo and domain instead. ## Solution To make this happen, you'll need to configure a few things on the Microsoft side and the Descope side, as well as configure a new CNAME in your domain's DNS records. ### Configuring the CNAME You will need to create a [custom domain](/how-to-deploy-to-production/custom-domain) that points to `cname.descope.com` (US) / `CNAME.euc1.descope.com` (EU), so that if the redirect URI is `auth.example.com` for instance, the Microsoft page will show **Sign in to continue to with example.com** instead of **descope.com** See the [Custom Domain](/how-to-deploy-to-production/custom-domain) guide for a step by step guide for managing sessions within cookies. Once that is completed, you will need to create a new enterprise application in Azure. ### Creating a new Enterpise Application 1. Navigate to your Azure portal. 2. Search For `Enterprise Applications`, and click on `+New Application` to start the process: ![Descope custom social login with microsoft, creating microsoft enterprise application 1](/assets/microsoft-custom-login-enterprise.webp) 3. Create Your Own Application: ![Descope custom social login with microsoft, creating microsoft enterprise application 2](/assets/microsoft-custom-login-enterprise-1.webp) 4. Choose a name, keep the `Integrate any other application you don't find in the gallery (Non-gallery)` option selected and click on Create: ![Descope custom social login with microsoft, creating microsoft enterprise application 3](/assets/microsoft-custom-login-enterprise-2.webp) 5. Once you create the application, make sure to copy the application ID, and set it aside: ![Descope custom social login with microsoft, creating Microsoftf application 3](/assets/microsoft-custom-login-enterprise-3.webp) 6. Search for `Microsoft Entra ID`, and navigate to `App Registrations` under the manage tab: ![Descope custom social login with Microsoft, creating microsoft application 4](/assets/microsoft-custom-login-enterprise-4.webp) 7. Search for your application by name in the `App Registrations` tab, after finding it, click it. Then navigate to `Certificates & Secrets`: ![Descope custom social login with Microsoft, creating microsoft application 5](/assets/microsoft-custom-login-enterprise-5.webp) 8. Create a new secret, copy the value and set it aside: Exiting the page will hide the secret forever. ![Descope custom social login with Microsoft, creating microsoft application 6](/assets/microsoft-custom-login-enterprise-6.webp) 9. Navigate to `Authentication`, add a new web platform, and set the callback URI to `https://api.descope.com/v1/oauth/callback`: ![Descope custom social login with Microsoft, creating microsoft application 7](/assets/microsoft-custom-login-enterprise-7.webp) 10. Scroll down inside the `Authentication` page, and select the multi-tenant option to allow any user to use your app, click save: ![Descope custom social login with Microsoft, creating microsoft application 8](/assets/microsoft-custom-login-enterprise-8.webp) 11. Navigate to `API Permissions`, and create a new permission: ![Descope custom social login with Microsoft, creating microsoft application 9](/assets/microsoft-custom-login-enterprise-9.webp) ![Descope custom social login with Microsoft, creating microsoft application 10](/assets/microsoft-custom-login-enterprise-10.webp) ![Descope custom social login with Microsoft, creating microsoft application 11](/assets/microsoft-custom-login-enterprise-11.webp) You should ask your administrators to approve the permission granted to the app. 12. Now on Descope, navigate to the custom Microsoft OAuth settings * Click on `Use my own account`. * Put the Application ID you put aside in the `Client ID` section. * Put the Client secret you created in the `Client Secret` section. * You can put the `Prompt` as `Consent` for testing. * Click save. ![Descope custom social login with Microsoft, creating microsoft application 12](/assets/microsoft-custom-login-enterprise-12.webp) ## Testing Authentication Now we are all set to test if our application works, here is an example flow that uses Microsoft OAuth: ![Descope custom social login with Microsoft, creating microsoft application 13](/assets/microsoft-custom-login-enterprise-13.webp) Now testing the flow, we can see that we get the consent screen with our custom app: ![Descope custom social login with Microsoft, creating microsoft application 14](/assets/microsoft-custom-login-enterprise-14.webp) ## Enterprise Application Authentication For your app to appear as verified by Microsoft, you need to pass [verification](https://learn.microsoft.com/en-us/answers/questions/1054464/how-to-verify-azure-app). And you're done! Now when you sign in, you should see your own personalized app page with your custom domain name. If you have any questions, please reach out to us [here](/support)! ## Merging identities from Microsoft When using Microsoft for social login, Azure does not certainly verify the user's email. We at Descope understand that this could be a security flaw when a user tries to authenticate and we cover this subject in this [blog](https://www.descope.com/blog/post/noauth). By default, we do not merge email addresses to an existing user login ID to prevent this security flaw from actually happening. Inside Descope flows, we provide the option to triage based on email verification status using `authInfo.user.verifiedEmail`. If you want to merge the user based on the user's email address, follow these steps: * Triage based on condition: ![Descope custom social login with Microsoft, triage](/assets/microsoft-custom-login-triage.webp) * Merge the identity after verifing the email: ![Descope custom social login with Microsoft, merge identity inside the flow](/assets/microsoft-custom-login-flow.webp) ## Implementing Email Verification with Microsoft claims: Microsoft provides the `xms_edov` (Email Domain Owner Verified) claim that indicates whether an email is domain-verified. Using this claim can bypass verification issues entirely. There is an alternative claim `primary_verified_email` that can also be used for email verification. To implement this in your flow, follow these steps: * Go to the OAuth application registration in Microsoft Azure portal, navigate to `Token configuration`, and add either one of the claims as optional claims for ID token: ![Descope custom social login with Microsoft, add optional claims in microsoft app registration](/assets/microsoft-oauth-optional-claims.webp) * Go to the OAuth step inside your flow, and mark "expose raw OAuth response": ![Descope custom social login with Microsoft, expose raw OAuth response](/assets/microsoft-oauth-expose-raw-response.webp) * After the OAuth step, add a Scriptlet to set the email verification status based on the claims: ![Descope custom social login with Microsoft, scriptlet to set email verification](/assets/microsoft-oauth-scriptlet-set-email-verification.webp) Here is an example scriptlet code to set the email verification status: ```javascript // id token extraction const idToken = oauthResponse.idToken; //email address extraction, depending on the IdP const email = idToken.email || idToken.sub; //xms_edov extraction - boolean const xms_edov = idToken.xms_edov; //xms_edov extraction - array const verified_primary_email = idToken.verified_primary_email; return { verified: xms_edov || verified_primary_email.contains(email) }; ``` * Mark the email as verified if either of the claims indicate verification: ![Descope custom social login with Microsoft, update user email verification based on claims](/assets/microsoft-oauth-action-email-verified.webp) # Ethereum Wallet (/auth-methods/oauth/providers/custom-providers/ethereum-wallet) This guide covers the implementation of adding a custom Ethereum Wallet (social login) provider within Descope. # Ethereum Wallet Descope allows you to create custom Social Login (OAuth) providers within the [Authentication Methods page](https://app.descope.com/settings/authentication/social). This guide covers the step by step configuration of a custom Ethereum Wallet OAuth provider. ## Obtain Ethereum Client ID and Secret Before creating the custom OAuth provider within the Descope console, you must obtain a Ethereum Client ID and Secret. To obtain a Client ID and Secret, run the curl command below. You can find your project's base url in our [Multi-Region Support Guide](/management/project-settings/multi-regional). ```sh title="Terminal" curl -X POST https://oidc.login.xyz/register -d '{"redirect_uris":["https:///v1/oauth/callback"]}' ``` The response will look something like this which includes the Client ID, secret, access token, and Client URI. ```json { "client_id": "xxxxx", "client_secret": "xxxxx", "registration_access_token": "xxxxx", "registration_client_uri": "https://oidc.login.xyz/client/xxxxx", "redirect_uris": [ "__BaseURL__/v1/oauth/callback" ] } ``` ## Creating Descope Custom Provider You can configure a custom provider in the Descope Console. Under [Authentication Methods -> Social Login](https://app.descope.com/settings/authentication/social), select `+ Add custom provider` in the top right corner. You can then set the name (in this case, "Ethereum"), logo, and description for your custom provider. ![Create custom provider](/assets/custom_providers.webp) ### Configure Account Settings Within the account settings section of your provider, you will configure the following items: - `Client ID`: This is the Client ID from the response in the above curl. - `Client Secret`: This is the Client Secret from the response in the above curl. - `Scopes`: These are the configured scopes granted to Descope for the user within Ethereum. Scopes provides Ethereum users using third-party apps the confidence that only the information they choose to share will be shared. The minimum needed scopes for Descope to integrate with Ethereum are `openid` and `profile`. - `Authorized Grant Type`: Authorization Code grant type uses the default configured response method, while implicit is set to use the Form Post response method with "id_token" response type only. ![Custom Ethereum OAuth provider account settings configured within Descope](/assets/custom-ethereum-provider-descope.webp) ### Configure Connection Settings Within the account settings section of your provider, you will configure the following items: - `Authorization Endpoint`: The endpoint to request authorization from the user. For Ethereum, this endpoint is `https://oidc.login.xyz/authorize` - `Token Endpoint`: The endpoint to exchange the authorization code for an access token. For Ethereum this endpoint is `https://oidc.login.xyz/token` - `User Info Endpoint`: The endpoint to get user details for attribute mapping. For Ethereum, this endpoint is `https://oidc.login.xyz/userinfo` Note, these items can be found by running the below curl against the well known OpenID configuration URL. ```sh title="Terminal" curl https://oidc.login.xyz/.well-known/openid-configuration ``` The response will look something like this which includes well known OpenID configuration items. ```json { "issuer": "https://oidc.login.xyz/", "authorization_endpoint": "https://oidc.login.xyz/authorize", "token_endpoint": "https://oidc.login.xyz/token", "userinfo_endpoint": "https://oidc.login.xyz/userinfo", "jwks_uri": "https://oidc.login.xyz/jwk", "registration_endpoint": "https://oidc.login.xyz/register", "scopes_supported": [ "openid", "profile" ], "response_types_supported": [ "code", "id_token", "token id_token" ], "subject_types_supported": [ "pairwise" ], "id_token_signing_alg_values_supported": [ "RS256" ], "userinfo_signing_alg_values_supported": [ "RS256" ], "token_endpoint_auth_methods_supported": [ "client_secret_basic", "client_secret_post", "private_key_jwt" ], "claims_supported": [ "sub", "aud", "exp", "iat", "iss", "preferred_username", "picture" ], "op_policy_uri": "https://oidc.login.xyz/legal/privacy-policy.pdf", "op_tos_uri": "https://oidc.login.xyz/legal/terms-of-use.pdf" } ``` Once configured within the Descope console, your Connection Settings will look like the below. ![Custom Ethereum OAuth provider connection settings configured within Descope](/assets/custom-ethereum-provider-descope-config.webp) ### Configure User Attribute Mapping These are the available claims supported by Ethereum: ```json "claims_supported": [ "sub", "aud", "exp", "iat", "iss", "preferred_username", "picture" ] ``` You can map `sub`, `preferred_username`, and `picture` to Descope attributes. See below for an example of the configured user attribute mapping. ![Custom Ethereum OAuth provider user attribute mapping configured within Descope](/assets/custom-ethereum-provider-descope-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](/auth-methods/oauth/settings). To add Ethereum Social Login to your flow, check out our [Social Login In Flows Guide](/auth-methods/oauth#social-login-oauth-with-flows). # ID.me (/auth-methods/oauth/providers/custom-providers/id-me) This guide outlines the process for configuring a connection ID.me as a custom OAuth provider within Descope. # ID.me OAuth Provider Descope provides the flexibility to add custom Social Login (OAuth) providers, including [ID.me](https://www.id.me/) as an identity provider. This guide will help you configure a custom [ID.me OAuth/OIDC provider](https://docs.id.me/guides/oidc/overview) within the Descope platform. ## Creating the ID.me Application Before integrating the custom OAuth provider in Descope, you must first set up your application on ID.me. ### Access ID.me Developer Portal Go to the [ID.me developer portal](https://developer.id.me/) and select **Create application +**. ![ID.me create application](/assets/id-me-application-create.webp) ### Configuring Application Details Add your application's details: 1. **Application Name**: Specify app name. 2. **Display name**: Specify a user-friendly name to display during the sign-in flow. 3. **Redirect URI**: This is where users will be redirected after completing authentication. Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base url in our [Multi-Region Support Guide](/management/project-settings/multi-regional). Below is an example of an application configuration in ID.me: ![ID.me create application configuration](/assets/id-me-create-app-settings.webp) ## Creating Descope Custom Provider In Descope, navigate to the [Customize Authentication Methods page](https://app.descope.com/settings/authentication/social) and add a new custom provider by selecting **+ Provider** -> **Custom**. For this example, we will name the provider `ID.me`. ### Configure Account Settings The values needed to configure Descope can be found in your ID.me application under the **Integration** tab. In your Descope provider, i the account settings: - `Client ID`: Use the **Client ID** from the ID.me application. - `Client Secret`: Use the **Client secret** from the ID.me application. - `Scopes`: The minimum required scopes for using ID.me for login are `openid` and `login`. If you need to request other scopes based on the authentication scenario, you can add them here. A full list of supported scopes can be found [here](https://docs.id.me/integrations/configurations/configuration-standards). Some scopes must also be enabled in your ID.me integration before Descope can request them. ![Custom ID.me OAuth provider account settings configured within Descope](/assets/id-me-provider-descope-config.webp) ### Configure Connection Settings Depending on whether you are using the `developers.id.me` Production environment, `developers.id.me` Sandbox environment, or the `developers.idmelabs.com` Sandbox environment, the base URL for the OAuth endpoints will differ. | Environment | OAuth domain | |---------------------|-----------------------------------------------| | `developers.id.me` Production | api.id.me | | `developers.id.me` Sandbox | api.id.me | | `developers.idmelabs.com` Sandbox | api.idmelabs.com | You will need to input the OIDC endpoints that come from the ID.me well known configuration. Replace `BASE_DOMAIN` with the OAuth domain for your environment from the table above. The values you will need to input in the Console are listed below. | Setting | Value | |---------------------|-----------------------------------------------| | Discovery URL | `https:///oidc/.well-known/openid-configuration` | | Issuer | `https:///oidc` | | Authorization Endpoint | `https:///oauth/authorize` | | Token Endpoint | `https:///oauth/token` | | User Info Endpoint | `https:///api/public/v3/userinfo` | | JWKS Endpoint | `https:///oidc/.well-known/jwks` | Below is an example of the production well-known configuration values: ![Custom ID.me OAuth provider connection settings configured within Descope](/assets/id-me-provider-connection-settings.webp) You can optionally enable PKCE in the Descope configuration for an additional layer of security. ### Configure User Attribute Mapping Map the necessary user attributes based on the information provided by the ID.me user info endpoint. For example, email and name. ![Custom ID.me OAuth provider user attribute mapping configured within Descope](/assets/custom-provider-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](/auth-methods/oauth/settings). To add ID.me Social Login to your flow, check out our [Social Login In Flows Guide](/auth-methods/oauth#social-login-oauth-with-flows). # Custom Providers (/auth-methods/oauth/providers/custom-providers) Overview for all the custom social login pages that we support # Custom Providers When using **Social Login (OAuth)** as authentication method, you may need to use a provider that we don't provide out of the box. For this, Descope allows you to create your own custom OAuth provider for your Descope Project. For some common applications, we have dedicated guides below for you to follow. Otherwise, [continue reading](#configuring-a-custom-provider) to learn how to create a custom OAuth provider. } href="/auth-methods/oauth/custom-providers/ethereum-wallet" title="Ethereum Wallet" description="Custom Social Login with Ethereum Wallet" /> } href="/auth-methods/oauth/custom-providers/line" title="LINE" description="Custom Social Login with LINE" /> } href="/auth-methods/oauth/custom-providers/logingov" title="Login.gov" description="Custom Social Login with Login.gov" /> } href="/auth-methods/oauth/custom-providers/id-me" title="ID.me" description="Custom Social Login with ID.me" /> } href="/auth-methods/oauth/custom-providers/spotify" title="Spotify" description="Custom Social Login with Spotify" /> } href="/auth-methods/oauth/custom-providers/tiktok" title="TikTok" description="Custom Social Login with TikTok" /> } href="/auth-methods/oauth/custom-providers/kakaotalk" title="KakaoTalk" description="Custom Social Login with KakaoTalk" /> ## Configuring a Custom Provider In addition to the above providers, you can set up Social Login with any OAuth provider by configuring a custom provider in the Descope Console. Under [Authentication Methods → Social Login](https://app.descope.com/settings/authentication/social), select **+ Add custom provider** in the top right corner. You can then set the name, logo, and description for your custom provider. ![Create custom provider](/assets/custom_providers.webp) ## Trigger Method Control how this provider can be invoked using the **Trigger method** setting: * **Enable All**: Allows authentication via flows, APIs, and SDKs * **Block API/SDK**: Restricts to flows and management calls only, blocking direct API/SDK access This setting allows you to override the [global OAuth trigger method](/auth-methods/oauth/settings#enable-method-in-api-and-sdk) for this specific provider. ## Configure Account Settings Configure the following account settings: * **Client ID**: The public identifier for your OAuth application, obtained directly from your OAuth provider (e.g., Google, Microsoft, GitHub). This value is required to authenticate your application during the OAuth flow. * **Client Authentication**: Choose how Descope authenticates to your OAuth provider. Select **Client Secret** to use a confidential key provided by your provider, or **Private Key** to use a signed JWT instead. See [Private Key JWT](#private-key-jwt) below. * **Scopes**: Defines which user permissions your application requests. These scopes are configured within your OAuth provider and determine what user data can be accessed. Examples include: * `openid`, `email`, `profile` (for OpenID Connect flows) * Provider-specific scopes like `user:email`, `read:user`, `identify`, `public_profile`, etc. If additional scopes are added, ensure **Manage tokens from provider** is enabled to allow Descope to store and manage access and refresh tokens. * **Grant Type**: Choose between: * **Authorization Code (recommended):** Uses the standard authorization code exchange for better security. * **Implicit:** Uses `form_post` with an `id_token` response type (required for certain flows like [Google One Tap](/auth-methods/oauth/google-one-tap)). ![Custom provider account settings](/assets/custom-provider-account-settings-new.webp) ### Private Key JWT Descope will automatically generate a private key for your project. You cannot use your own private key with this method. Select **Private Key** under **Client Authentication** to use a signed JWT instead of a client secret, per [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523). With this method, Descope creates a signed JWT using your project's private key during token exchange. The OAuth provider validates this JWT instead of a client secret. Provide your project's JWK endpoint to the OAuth provider: __BaseURL__/__ProjectID__/.well-known/jwks.json Replace `api.descope.com` with your [custom domain](/how-to-deploy-to-production/custom-domain) if applicable. #### Example Token Request When exchanging the authorization code for tokens, Descope sends a request like this to the OAuth provider's token endpoint with a signed JWT as the `client_assertion`: ```http POST /token HTTP/1.1 Host: provider.example.com Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTHORIZATION_CODE &redirect_uri=__BaseURL__/v1/oauth/callback &client_id=YOUR_CLIENT_ID &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer &client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... ``` ## Configure Connection Settings Within the connection settings section of your provider, you will configure the following items: * **Discovery URL**: If your provider supports OpenID Connect Discovery, you can enter the `.well-known/openid-configuration` URL here. Descope will automatically populate the following fields below. * **Issuer** (optional): Typically matches the `iss` claim returned from your provider's OpenID configuration. * **Authorization Endpoint**: The provider's URL where users are redirected to authenticate. Usually auto-filled if a Discovery URL is provided. * **Token Endpoint**: The endpoint used to exchange authorization codes for access and ID tokens. Usually auto-filled from the Discovery URL. * **User Info Endpoint**: The endpoint that returns user claims (like email, name, or subject ID). Usually auto-filled from the Discovery URL. * **JWKs Endpoint (optional)**: Used to fetch public keys for validating JWTs. If not provided, Descope will rely on the User Info endpoint for user data, rather than validating the `id_token` with a public key from a JWKs endpoint. You can add custom request parameters to the **Authorization Endpoint** and **Token Endpoint**. These parameters will be sent with every request to the respective endpoint. This is useful for providers that require specific parameters like `access_type=offline` or `prompt=consent`. ![Custom provider connection settings](/assets/custom-provider-connection-settings-new.webp) ### PKCE Support Enable PKCE when your external OAuth provider requires or supports it (e.g., Login.gov), when their documentation specifies it, or if you receive errors about missing code_challenge parameters. In OAuth 2.1, PKCE is now mandatory in some cases, and recommended for use by all clients. Generally speaking, when Descope acts as the service provider (SP) for default or custom OAuth providers, we are treated as a confidential client. However, in cases where the OAuth provider expects all clients (confidential or public) to use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) during the authorization code exchange, Descope supports PKCE (Proof Key for Code Exchange). To enable **PKCE Support**, toggle the **Use PKCE** toggle button under the **Connection Settings** section of your OAuth provider configuration. ![PKCE Support](/assets/pkce-support-for-oauth-providers.webp) ## Configure the Prompt Prompts can also be overridden at the Flow level, allowing different user experiences for the same provider configuration. The `prompt` parameter defines how and when users are prompted during authentication. It can improve user experience or enhance security by controlling reauthentication and consent behavior. You can find this under **Prompt** in the Descope console: ![Custom Spotify OAuth provider select the prompt setting within Descope](/assets/spotify-custom-provider-prompt.webp) ### 1. The `login` prompt The `login` prompt forces the user to re-enter their credentials, guaranteeing the current user is explicitly authenticated. This is ideal for sensitive operations or re-verification scenarios where you need to ensure the user is actively present. ### 2. The `consent` prompt The `consent` prompt always asks the user to reapprove requested permissions, ensuring transparency and renewed consent from users. This is particularly useful when handling sensitive data or when the scopes/permissions your application requests have changed. ### 3. The `select_account` prompt The `select_account` prompt lets the user choose from multiple available accounts, enhancing usability for users managing multiple identities. This is ideal for multi-account environments where users might have both personal and work accounts. ## Configure User Attribute Mapping Map attributes returned from your OAuth provider to Descope's user profile. At minimum, you must map at least one attribute to the **Descope Login ID** (typically `sub` by default). You can map to built-in Descope fields such as `email`, `name`, or `phone`, as well as [custom attributes](/management/user-management#custom-user-attributes) you've defined for your project. Use the **Update user information upon successful login** toggle to control whether user attributes are refreshed on every login. When disabled, attribute mapping only applies during initial user creation, preserving any subsequent changes made to the user's profile. ![Custom provider user attribute mapping](/assets/custom-provider-user-attribute-mapping.webp) ## Configure Advanced Settings Additional optional settings enhance customization and security: * **Manage Tokens from Provider**: Enable this to let Descope securely store and manage provider-issued access and refresh tokens. These tokens can be used later via Descope APIs or SDKs to perform actions on behalf of the user. * **Callback Domain**: The domain that handles the OAuth callback. Defaults to your Descope project domain, but can be overridden. The callback URL will be: `https://api.descope.com/v1/oauth/callback` Or, if using a custom domain: `https://auth.mycompany.com/v1/oauth/callback` * **Redirect URL**: The URL users are redirected to after successful authentication when using SDKs or APIs. Typically, setting this isn't needed when using Flows. This can be overridden dynamically within SDK/API calls. * **Email Address Handling**: Decide if the email address returned from the provider should be treated as a login ID. You can promote the email to a login ID only if verified, promote regardless of verification status, or keep the email as a user attribute without allowing it to be a login ID. When emails are promoted to login IDs, users will be able to sign in with just their email address without using OAuth. * **Merge User Accounts Based on Email**: When enabled, Descope automatically merges accounts sharing the same verified email address. See [User Merging](/management/user-management#user-merging) for more. ![Advanced Custom Provider Settings](/assets/advanced-custom-providers-settings.webp) # KakaoTalk (/auth-methods/oauth/providers/custom-providers/kakaotalk) This guide covers the implementation of adding a custom KakaoTalk OAuth (social login) provider within Descope. # KakaoTalk Descope allows you to create custom Social Login (OAuth) providers within the [Authentication Methods page](https://app.descope.com/settings/authentication/social). This allows you to add any Social OAuth authentication within your application. This guide specifically covers the step-by-step configuration of a custom [KakaoTalk OAuth provider](https://developers.kakao.com/docs/latest/en/kakaologin/common). ## Creating the KakaoTalk Application Before creating the custom OAuth provider within the Descope console, you must create and configure an application within your Kakao developer account. You can follow the [Kakao App Setup Guide](https://developers.kakao.com/docs/latest/en/app-setting/app) for details on creating your application. When you create your app within the [Kakao Developers Console](https://developers.kakao.com/console/app), you must configure the redirect URI. Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base URL in our [Multi-Region Support Guide](/management/project-settings/multi-regional). Additionally, make sure **Kakao Login** is activated for your app under **Product Settings -> Kakao Login**. ![KakaoTalk app setup](/assets/kakao-app-setup.webp) ## Creating Descope Custom Provider You can configure a custom provider in the Descope Console. Under [Authentication Methods -> Social Login](https://app.descope.com/settings/authentication/social), select `+ Add custom provider` in the top right corner. You can then set the name (in this case, "KakaoTalk"), logo, and description for your custom provider. ![KakaoTalk app setup](/assets/custom_providers.webp) ### Configure Account Settings Within the account settings section of your provider, you will configure the following items: - `Client ID`: This is the **REST API Key** of the application created within the Kakao Developers Console. You can find it under **App -> Platform Keys**. - `Client Secret`: This is the Client Secret generated for your Kakao application. You can enable and retrieve it under **App -> Platform Keys**. - `Scopes`: These are the configured scopes granted to Descope for the user within Kakao. Scopes provides users using third-party apps the confidence that only the information they choose to share will be shared. - `Authorized Grant Type`: Use **Authorization Code** (recommended). This uses the standard authorization code exchange for better security. ![KakaoTalk account settings](/assets/kakao-account-settings.webp) ### Configure Connection Settings Within the **Connection Settings** section of your provider, you will configure the following items: - **Authorization Endpoint**: The endpoint to request authorization from the user. For KakaoTalk, this endpoint is `https://kauth.kakao.com/oauth/authorize`. - **Token Endpoint**: The endpoint to exchange the authorization code for an access token. For KakaoTalk, this endpoint is `https://kauth.kakao.com/oauth/token`. - **User Info Endpoint**: The endpoint to retrieve user details for attribute mapping. For KakaoTalk, this endpoint is `https://kapi.kakao.com/v2/user/me`. ![KakaoTalk connection settings](/assets/kakao-connection-settings.webp) ### Configure User Attribute Mapping Given the `account_email` and `profile_nickname` scopes, Descope can capture any items from the user info endpoint response. Per the below example, we have mapped email, display name, and picture. Map these fields in the **User Attribute Mapping** section of the Descope Console accordingly. ![KakaoTalk user attribute mapping](/assets/kakao-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](https://docs.descope.com/auth-methods/oauth/settings). To add KakaoTalk Social Login to your flow, check out our [Social Login In Flows Guide](https://docs.descope.com/auth-methods/oauth#social-login-oauth-with-flows). # LINE (/auth-methods/oauth/providers/custom-providers/line) This guide outlines the process for configuring a connection to LINE as a custom OAuth provider within Descope. # LINE OAuth Provider Descope allows you to create custom Social Login (OAuth) providers within the [Authentication Methods page](https://app.descope.com/settings/authentication/social). This allows you to add any Social OAuth authentication within your application. This guide specifically covers the step by step configuration of a custom [Line OAuth provider](https://developers.line.biz/en/docs/line-login/integrate-line-login/). ## Creating the LINE Application Before creating the custom OAuth provider within the Descope console, you must create and configure an application within your LINE developer account. You can follow the [Line Login Integration Guide](https://developers.line.biz/en/docs/line-login/integrate-line-login/) for details on creating your application. When you create your app within Line, you must configure the redirect URI. Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base url in our [Multi-Region Support Guide](https://docs.descope.com/management/project-settings/multi-regional). ## Creating Descope Custom Provider You can configure a custom provider in the Descope Console. Under [Authentication Methods \-\> Social Login](https://app.descope.com/settings/authentication/social), select `+ Add custom provider` in the top right corner. You can then set the name (in this case, "Line"), logo, and description for your custom provider. ![Create custom provider](/assets/custom_providers.webp) ### Configure Account Settings Within the account settings section of your provider, you will configure the following items: * `Client ID`: This is the Channel ID of the application created within Line * `Client Secret`: This is the Channel Secret of the application created within Line * `Scopes`: These are the configured scopes granted to Descope for the user within Line. Scopes provides Line users using third-party apps the confidence that only the information they choose to share will be shared. Line’s Docs specify the available scopes that can be requested * `Authorized Grant Type`: * Authorization Code: Recommended. Uses standard query/form response modes. * Implicit: Only if required; typically not recommended for new integrations. ![Custom Line OAuth provider account settings configured within Descope](/assets/line-custom-provider-account-settings.webp) ### Configure Connection Settings Within the connection settings section of your provider, you will configure the following items: - `Authorization Endpoint`: The endpoint to request authorization from the user. For LINE, this endpoint is `https://access.line.me/oauth2/v2.1/authorize`. - `Token Endpoint`: The endpoint to exchange the authorization code for an access token. For LINE this endpoint is `https://api.line.me/oauth2/v2.1/token`. - `User Info Endpoint`: The endpoint to get user details for attribute mapping. For LINE, this endpoint is `https://api.line.me/v2/profile`. ![Custom Line OAuth provider user attribute mapping configured within Descope](/assets/line-custom-provider-connection-settings.webp) ### Configure User Attribute Mapping Given the `openid` and `profile` scopes, Descope can capture any items from the [user info endpoint](https://developers.line.biz/en/reference/line-login/#userinfo) response. However, capturing additional profile information (name, gender, birthday, phone number, address) that users have registered with LINE Profile+, would require you to undergo an application process with Line, see [LINE Profile+](https://developers.line.biz/en/docs/partner-docs/line-profile-plus/) for more details. ![Custom Line OAuth provider user attribute mapping configured within Descope](/assets/line-custom-provider-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](https://docs.descope.com/auth-methods/oauth/settings). To add Line Social Login to your flow, check out our [Social Login In Flows Guide](https://docs.descope.com/auth-methods/oauth#social-login-oauth-with-flows). # Login.gov (/auth-methods/oauth/providers/custom-providers/logingov) This guide outlines the process for configuring a connection to Login.gov as a custom OAuth provider within Descope. # Login.gov OAuth Provider Descope provides the flexibility to add custom Social Login (OAuth) providers, including providers like [Login.gov](https://login.gov/). This guide will help you configure a custom [Login.gov OAuth provider](https://developers.login.gov/oidc/getting-started/) within the Descope platform. ## Creating the Login.gov Application Before integrating the custom OAuth provider in Descope, you must first set up your application on Login.gov. Login.gov is used for government agencies. You will need to go through Login.gov's [integration developer approval process](https://developers.login.gov/) to obtain a test account and get your application cleared for production. You can contact their [Partner Support](https://developers.login.gov/support/#contacting-partner-support) to get started, or if you have any Login.gov related questions. ### Access Login.gov Developer Sandbox Go to the [Login.gov developer sandbox](https://developers.login.gov) and select `Create a new test app`. Make sure to select **PKCE** as the authentication protocol and configure the necessary settings such as Level of Service and Attribute bundles. ![Login.gov create test app](/assets/create-test-app.webp) ### Configuring Application Details Add your application's details: 1. **App Name**: Specify app name. 2. **Friendly name**: Specify a name to display during the sign-in flow. 3. **Team**: Select the previously configured team to test the integration. 4. **Authentication protocol**: Select OpenID Connect PKCE 5. **Level of service**: Select the level of service as per your need. (Authentication only is IAL1 standard) 6. **Issuer**: A string in the following format. Fill in `app_name` and `ageny_name` with your own values: `urn:gov:gsa:openidconnect.profiles:sp:sso:agency_name:app_name`. 7. **Logo**: Optionally upload a logo for your application ![Login.gov create test app configuration](/assets/create-test-app-config.webp) A Client Secret should also be generated when you create this test app integration. You'll need this when you configure Login.gov as a custom provider in Descope. ### Setting Redirect URIs Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base url in our [Multi-Region Support Guide](/management/project-settings/multi-regional). ## Creating Descope Custom Provider In Descope, navigate to the [Customize Authentication Methods page](https://app.descope.com/settings/authentication/social) and add a new custom provider. For this example, we will name the provider `Login.gov`. ### Configure Account Settings In the account settings: - `Client ID`: Use the Issuer from the Login.gov setup. - `Client Secret`: Generated in Login.gov App Setup. - `Scopes`: Configure scopes as needed for your application's access requirements. A full list of support scopes can be found [here](https://developers.login.gov/attributes/) ![Custom Login.gov OAuth provider account settings configured within Descope](/assets/login-gov-provider-descope-config.webp) ### Configure Connection Settings You can get the well known configuration URLs, for both sandbox and production [here](https://developers.login.gov/oidc/getting-started/#auto-discovery). You'll need to input the OIDC endpoints that come from the Login.gov well known configuration. The values you'll need to input in the Console are listed below. #### Identity Sandbox Well Known Configuration Values ``` bash https://idp.int.identitysandbox.gov/openid_connect/authorize https://idp.int.identitysandbox.gov/api/openid_connect/token https://idp.int.identitysandbox.gov/api/openid_connect/userinfo https://idp.int.identitysandbox.gov/api/openid_connect/certs ``` #### Production Well-Known Configuration Values ``` bash https://secure.login.gov/openid_connect/authorize https://secure.login.gov/api/openid_connect/token https://secure.login.gov/api/openid_connect/userinfo https://secure.login.gov/api/openid_connect/certs ``` Below is an example of the production well-known configuration values: ![Custom Login.gov OAuth provider connection settings configured within Descope](/assets/custom-provider-connection-settings.webp) Login.gov requires PKCE to be enabled. See [PKCE Support](/auth-methods/oauth/providers/custom-providers#pkce-support) for more details on configuring PKCE for OAuth providers. ### Configure User Attribute Mapping Map the necessary user attributes based on the information provided by the Login.gov user info endpoint. For example, email and name. A full list of Login.gov supported attributes you can use to map can be found [here](https://developers.login.gov/attributes/) ![Custom Login.gov OAuth provider user attribute mapping configured within Descope](/assets/custom-provider-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](/auth-methods/oauth/settings). To add Login.gov Social Login to your flow, check out our [Social Login In Flows Guide](/auth-methods/oauth#social-login-oauth-with-flows). # Spotify (/auth-methods/oauth/providers/custom-providers/spotify) This guide covers the implementation of adding a custom Spotify OAuth (social login) provider within Descope. # Spotify OAuth Provider Descope allows you to create custom Social Login (OAuth) providers within the [Authentication Methods page](https://app.descope.com/settings/authentication/social). This allows you to add any Social OAuth authentication within your application. This guide specifically covers the step by step configuration of a custom [Spotify OAuth provider](https://developer.spotify.com/documentation/web-api/concepts/authorization). ## Creating the Spotify Application Before creating the custom OAuth provider within the Descope console, you must create and configure an application within your Spotify developer account. You can follow the [Spotify App Guide](https://developer.spotify.com/documentation/web-api/concepts/apps) for details on creating your application. When you create your app within Spotify, you must configure the redirect URI. Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base url in our [Multi-Region Support Guide](/management/project-settings/multi-regional). ## Creating Descope Custom Provider You can configure a custom provider in the Descope Console. Under [Authentication Methods -> Social Login](https://app.descope.com/settings/authentication/social), select `+ Add custom provider` in the top right corner. You can then set the name (in this case, "Spotify"), logo, and description for your custom provider. ![Create custom provider](/assets/custom_providers.webp) ### Configure Account Settings Within the account settings section of your provider, you will configure the following items: - `Client ID`: This is the Client ID of the application created within Spotify - `Client Secret`: This is the Client Secret of the application created within Spotify - `Scopes`: These are the configured scopes granted to Descope for the user within Spotify. Scopes provides Spotify users using third-party apps the confidence that only the information they choose to share will be shared. The minimum needed [Spotify scope](https://developer.spotify.com/documentation/web-api/concepts/scopes) for Descope to be able to capture the user's email and basic account details from Spotify is `user-read-email`. - `Authorized Grant Type`: Authorization Code grant type uses the default configured response method, while implicit is set to use the Form Post response method with "id_token" response type only. ![Custom Spotify OAuth provider account settings configured within Descope](/assets/spotify-account-settings.webp) ### Configure Connection Settings Within the account settings section of your provider, you will configure the following items: - `Authorization Endpoint`: The endpoint to request authorization from the user. For Spotify, this endpoint is `https://accounts.spotify.com/authorize`. - `Token Endpoint`: The endpoint to exchange the authorization code for an access token. For Spotify this endpoint is `https://accounts.spotify.com/api/token`. - `User Info Endpoint`: The endpoint to get user details for attribute mapping. For Spotify, this endpoint is `https://api.spotify.com/v1/me`. ![Custom Spotify OAuth provider connection settings configured within Descope](/assets/spotify-custom-provider-connection-settings.webp) ### Configure User Attribute Mapping Given the `user-read-email` and `user-read-private` scopes, Descope can capture any items from the [user info endpoint](https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile) response. Per the below example, we have mapped email and display name. ![Custom Spotify OAuth provider user attribute mapping configured within Descope](/assets/spotify-custom-provider-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](/auth-methods/oauth/settings). To add Spotify Social Login to your flow, check out our [Social Login In Flows Guide](/auth-methods/oauth#social-login-oauth-with-flows). # TikTok (/auth-methods/oauth/providers/custom-providers/tiktok) This guide covers the implementation of adding a custom TikTok OAuth (social login) provider within Descope. # TikTok OAuth Provider Descope allows you to create custom Social Login (OAuth) providers within the [Authentication Methods page](https://app.descope.com/settings/authentication/social). This allows you to add any Social OAuth authentication within your application. This guide specifically covers the step by step configuration of a custom [TikTok OAuth provider](https://developers.tiktok.com/doc/login-kit-overview). ## Creating the TikTok Application Before creating the custom OAuth provider within the Descope console, you must create and configure an application within your TikTok developer account. You can follow the [TikTok App Guide](https://developers.tiktok.com/doc/login-kit-web) for details on creating your application. When you create your app within TikTok, you must configure the redirect URI. Set the redirect URI to be `https:///v1/oauth/callback`. You can find your project's base url in our [Multi-Region Support Guide](/management/project-settings/multi-regional). ## Creating Descope Custom Provider You can configure a custom provider in the Descope Console. Under [Authentication Methods -> Social Login](https://app.descope.com/settings/authentication/social), select `+ Add custom provider` in the top right corner. You can then set the name (in this case, "TikTok"), logo, and description for your custom provider. ![Create custom provider](/assets/custom_providers.webp) ### Configure Account Settings Within the account settings section of your provider, you will configure the following items: - `Client ID`: This is the `Client Key` of the application created within TikTok. TikTok uses a non standard format, so will need to use custom parameter keys in the connection settings. - `Client Secret`: This is the `Client Secret` of the application created within TikTok - `Scopes`: These are the configured scopes granted to Descope for the user within TikTok. Scopes provides users using third-party apps the confidence that only the information they choose to share will be shared. The minimum needed [scope](https://developers.tiktok.com/doc/scopes-overview) for Descope to be able to capture the user's email and basic account details from TikTok is `user.info.basic`. ![Custom TikTok OAuth provider account settings configured within Descope](/assets/tiktok-custom-provider-account-settings.webp) ### Configure Connection Settings Within the account settings section of your provider, you will configure the following items: - `Authorization Endpoint`: The endpoint to request authorization from the user. For TikTok, this endpoint is `https://www.tiktok.com/v2/auth/authorize/`. - Add an authorization parameter key `client_key` and paste the value from the account setting's `Client Key`. - `Token Endpoint`: The endpoint to exchange the authorization code for an access token. For TikTok this endpoint is `https://open.tiktokapis.com/v2/oauth/token/`. - Add a token parameter key `client_key` and paste the value from the account setting's `Client Key`. - `User Info Endpoint`: The endpoint to get user details for attribute mapping. For TikTok, this endpoint is `https://open.tiktokapis.com/v2/user/info/?fields=union_id,display_name,avatar_url`. To query more fields, append the field name to this url. [Read more about scopes and fields](https://developers.tiktok.com/doc/tiktok-api-scopes). ![Custom TikTok OAuth provider connection settings configured within Descope](/assets/tiktok-custom-provider-connection-settings.webp) ### Configure User Attribute Mapping Given the `user.info.basic` scope, Descope can capture any items from the [user info endpoint](https://developers.tiktok.com/doc/tiktok-api-v1-user-info) response. Per the below example, we have mapped the following: * data.user.union_id -> Login ID (mandatory) * data.user.display_name -> Display Name (optional) * data.user.avatar_url -> Picture (optional) ![Custom TikTok OAuth provider user attribute mapping configured within Descope](/assets/tiktok-custom-provider-user-attribute-mapping.webp) For more OAuth provider settings information, check out the [OAuth Settings Guide](/auth-methods/oauth/settings). To add TikTok Social Login to your flow, check out our [Social Login In Flows Guide](/auth-methods/oauth#social-login-oauth-with-flows).