> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication for Embedded Users

> Choosing the right OAuth2 flow for Corti Embedded integrations

## Background

OAuth (Open Authorization) is an open-standard framework for access delegation, allowing applications to securely access a user's protected resources without exposing their login credentials. By keeping passwords private and limiting access to sensitive information, OAuth improves both security and access management across web, mobile, and desktop applications. OAuth 2.0, the current and most widely adopted version, expands upon the original protocol to support APIs, mobile apps, and connected devices, offering multiple authorization flows tailored to different application types.

<Warning>
  Corti Assistant requires user-based authentication. Client credentials flows and other machine-to-machine authentication methods are NOT supported for embedded Corti Assistant integrations. You must authenticate as an end user, not as an application.
</Warning>

When embedding Corti Assistant in your application (via iFrame/WebView), **it's important to use the right OAuth2 grant type** that supports user-based authentication.

This guide explains the OAuth flows that work with embedded Corti Assistant, focusing on user-based authentication methods that are suitable for interactive scenarios.

***

## Supported OAuth grant types for Embedded Corti Assistant

The following OAuth flows support user-based authentication and can be used with embedded Corti Assistant. **Client credentials grant is NOT supported** as it does not provide user context.

### 1. Authorization code flow with PKCE (recommended)

**Best for:** Native apps, single-page apps, or any browser-based integration where a user is present.

**Why:** This flow is secure, interactive, and doesn't require a client secret (ideal for public clients). Proof Key for Code Exchange (PKCE) protects against code interception attacks.

**How it works:**

1. Your app redirects the user to Corti's OAuth2 authorization server.
2. The user logs in and grants permission.
3. Corti redirects back with an authorization code.
4. Your app exchanges the code (with the PKCE verifier) for an access token.

**Key advantages:**

* Secure and suitable for embedded web apps.
* No client secret is required.
* Enforces user interaction.

<Accordion title="Code sample:">
  <Tabs>
    <Tab title="Using Corti JS SDK">
      <Tip>
        This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
      </Tip>

      ```javascript Step 1: Generate Authorization URL (Frontend) [expandable] theme={null}
      import { CortiAuth, CortiEnvironment } from "@corti/sdk";

      const auth = new CortiAuth({
          environment: CortiEnvironment.Eu,
          tenantName: "YOUR_TENANT_NAME",
      });

      // SDK automatically generates code verifier, stores it in localStorage and makes redirect
      await auth.authorizePkceUrl({
          clientId: "YOUR_CLIENT_ID",
          redirectUri: "https://your-app.com/callback"
      });
      ```

      ```javascript Step 2: Handle the Callback and Exchange Code for Tokens [expandable] theme={null}
      // Extract the authorization code from URL parameters
      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get('code');
      const error = urlParams.get('error');

      if (error) {
          console.error('Authorization failed:', error);
          return;
      }

      if (code) {
          // Exchange the authorization code for tokens using SDK
          const tokenResponse = await auth.getPkceFlowToken({
              clientId: "YOUR_CLIENT_ID",
              redirectUri: "https://your-app.com/callback",
              code: code,
          });

          const { accessToken, refreshToken } = tokenResponse;
      }
      ```
    </Tab>

    <Tab title="Using Fetch">
      This flow requires two stages: generating the code verifier/challenge and handling the token exchange after the redirect.

      ```bash Step 1: Redirect User to Authorize [expandable] theme={null}
      // Generate code verifier + challenge
      const code_verifier = crypto.randomUUID().replace(/-/g, '');
      const code_challenge = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
      const base64url = btoa(String.fromCharCode(...new Uint8Array(code_challenge)))
      .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

      // Store verifier for use after redirect
      localStorage.setItem('pkce_verifier', code_verifier);

      // Redirect to authorization server
      window.location.href = `https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/auth?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&code_challenge=${base64url}&code_challenge_method=S256&scope=openid`;
      ```

      ```bash Step 2: Exchange Code for Token (after redirect) [expandable] theme={null}
      const code = new URLSearchParams(window.location.search).get('code');
      const code_verifier = localStorage.getItem('pkce_verifier');

      const response = await fetch('https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
          grant_type: 'authorization_code',
          client_id: 'YOUR_CLIENT_ID',
          redirect_uri: 'https://yourapp.com/callback',
          code,
          code_verifier
      })
      });

      const data = await response.json();
      console.log('Access Token:', data.access_token);
      ```
    </Tab>

    <Tab title="C#">
      <Tip>
        This example uses standard .NET libraries for OAuth2 PKCE flow
      </Tip>

      ```csharp Step 1: Generate Authorization URL (Frontend) [expandable] theme={null}
      using System;
      using System.Security.Cryptography;
      using System.Text;
      using System.Web;

      public class PkceAuth
      {
          private const string ClientId = "YOUR_CLIENT_ID";
          private const string RedirectUri = "https://your-app.com/callback";
          private const string AuthBaseUrl = "https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect";

          public static (string authUrl, string codeVerifier) GenerateAuthorizationUrl()
          {
              // Generate code verifier (random string)
              var codeVerifier = GenerateCodeVerifier();

              // Generate code challenge (SHA256 hash, base64url encoded)
              var codeChallenge = GenerateCodeChallenge(codeVerifier);

              // Store code verifier (e.g., in session or secure storage)
              // In a real app, store this securely for use after redirect
              HttpContext.Current.Session["pkce_verifier"] = codeVerifier;

              // Build authorization URL
              var authUrl = $"{AuthBaseUrl}/auth?" +
                  $"response_type=code&" +
                  $"client_id={HttpUtility.UrlEncode(ClientId)}&" +
                  $"redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&" +
                  $"code_challenge={codeChallenge}&" +
                  $"code_challenge_method=S256&" +
                  $"scope=openid";

              return (authUrl, codeVerifier);
          }

          private static string GenerateCodeVerifier()
          {
              var bytes = new byte[32];
              using (var rng = RandomNumberGenerator.Create())
              {
                  rng.GetBytes(bytes);
              }
              return Convert.ToBase64String(bytes)
                  .TrimEnd('=')
                  .Replace('+', '-')
                  .Replace('/', '_');
          }

          private static string GenerateCodeChallenge(string codeVerifier)
          {
              using (var sha256 = SHA256.Create())
              {
                  var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
                  return Convert.ToBase64String(challengeBytes)
                      .TrimEnd('=')
                      .Replace('+', '-')
                      .Replace('/', '_');
              }
          }
      }
      ```

      ```csharp Step 2: Exchange Code for Token (after redirect) [expandable] theme={null}
      using System;
      using System.Collections.Generic;
      using System.Net.Http;
      using System.Text.Json;
      using System.Threading.Tasks;
      using System.Web;

      public class TokenExchange
      {
          private const string ClientId = "YOUR_CLIENT_ID";
          private const string RedirectUri = "https://your-app.com/callback";
          private const string TokenUrl = "https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token";

          public static async Task<TokenResponse> ExchangeCodeForTokenAsync(string code, string codeVerifier)
          {
              using var httpClient = new HttpClient();

              var formData = new List<KeyValuePair<string, string>>
              {
                  new KeyValuePair<string, string>("grant_type", "authorization_code"),
                  new KeyValuePair<string, string>("client_id", ClientId),
                  new KeyValuePair<string, string>("redirect_uri", RedirectUri),
                  new KeyValuePair<string, string>("code", code),
                  new KeyValuePair<string, string>("code_verifier", codeVerifier)
              };

              var content = new FormUrlEncodedContent(formData);
              var response = await httpClient.PostAsync(TokenUrl, content);

              response.EnsureSuccessStatusCode();

              var responseBody = await response.Content.ReadAsStringAsync();
              var tokenData = JsonSerializer.Deserialize<JsonElement>(responseBody);

              return new TokenResponse
              {
                  AccessToken = tokenData.GetProperty("access_token").GetString(),
                  RefreshToken = tokenData.GetProperty("refresh_token").GetString(),
                  ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
              };
          }
      }

      public class TokenResponse
      {
          public string AccessToken { get; set; }
          public string RefreshToken { get; set; }
          public int ExpiresIn { get; set; }
      }

      // Usage in callback handler (e.g., ASP.NET MVC Controller)
      public class AuthController : Controller
      {
          public async Task<ActionResult> Callback(string code)
          {
              if (string.IsNullOrEmpty(code))
              {
                  return View("Error");
              }

              // Retrieve stored code verifier
              var codeVerifier = Session["pkce_verifier"] as string;

              if (string.IsNullOrEmpty(codeVerifier))
              {
                  return View("Error");
              }

              var tokenResponse = await TokenExchange.ExchangeCodeForTokenAsync(code, codeVerifier);

              // Store tokens securely (e.g., in session or secure cookie)
              Session["access_token"] = tokenResponse.AccessToken;

              return RedirectToAction("Index", "Home");
          }
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

<Tip>
  Use this gold standard for Corti Assistant embedded use cases.
</Tip>

***

### 2. Authorization code flow (without PKCE)

**Best for:** Server-side web applications embedding Corti Assistant where the client secret can be safely stored on the backend.

**Why:** Similar to PKCE, but requires storing a client secret — which is *not* safe in public or browser-based clients.

**Key Concerns:**

* Unsafe for apps where the frontend or iFrame can be inspected.
* Only acceptable in secure backend environments where the client secret can be protected.

<Accordion title="Code sample:">
  <Tabs>
    <Tab title="Using Corti JS SDK">
      <Tip>
        This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
      </Tip>

      ```javascript Step 1: Create Authorization URL [expandable] theme={null}
      import { CortiAuth, CortiEnvironment } from "@corti/sdk";

      const auth = new CortiAuth({
          environment: CortiEnvironment.Eu,
          tenantName: "YOUR_TENANT_NAME",
      });

      // Generate authorization URL
      await auth.authorizeURL({
          clientId: "YOUR_CLIENT_ID",
          redirectUri: "https://your-app.com/callback",
      });
      ```

      ```javascript Step 2: Handle the Callback and Exchange Code for Tokens [expandable] theme={null}
      // Extract the authorization code from URL parameters
      const urlParams = new URLSearchParams(window.location.search);
      const code = urlParams.get('code');
      const error = urlParams.get('error');

      if (error) {
          console.error('Authorization failed:', error);
          return;
      }

      if (code) {
          // Exchange the authorization code for tokens using SDK (client-side)
          const response = await fetch('http://localhost:3000/callback', {
              method: 'POST',
              headers: {
                  'Content-Type': 'application/json',
              },
              body: JSON.stringify({ code }),
          });

          if (!response.ok) {
              throw new Error('Authentication failed');
          }

          const tokens = await response.json();
      }
      ```

      ```javascript Step 3: Exchange Code for Access Token (Backend) [expandable] theme={null}
      // server.js or routes/callback.js
      import express from "express";
      import { CortiAuth } from "@corti/sdk";

      const app = express();

      const CLIENT_ID     = "YOUR_CLIENT_ID";
      const CLIENT_SECRET = "YOUR_CLIENT_SECRET";
      const TENANT_NAME   = "YOUR_TENANT_NAME";
      const ENVIRONMENT   = "YOUR_ENVIRONMENT";
      const REDIRECT_URI  = "https://yourapp.com/callback"; // must match OAuth settings

      app.get("/callback", async (req, res) => {
          const authCode = req.query.code;

          if (!authCode) {
              return res.status(400).send("Missing authorization code");
          }

          try {
              // Initialize CortiAuth SDK client
              const auth = new CortiAuth({
                  environment: ENVIRONMENT,
                  tenantName: TENANT_NAME,
              });

              // Exchange code for token
              const tokens = await auth.getCodeFlowToken({
                  clientId: CLIENT_ID,
                  clientSecret: CLIENT_SECRET,
                  redirectUri: REDIRECT_URI,
                  code: authCode,
              });

              // Example "do something": simply log the access token
              console.log("Access Token:", tokens.accessToken);

              // Redirect back to app with token in query param (example)
              res.json(tokens);
          } catch (err) {
              console.error("OAuth error:", err);
              return res.status(500).send("Failed to exchange authorization code");
          }
      });

      app.listen(3000, () => {
          console.log("Server running at http://localhost:3000");
      });
      ```
    </Tab>

    <Tab title="Using Fetch">
      This version assumes:

      * Your app has a **frontend (e.g., React)** that initiates the login.
      * Your app has a **backend (e.g., Node.js + Express)** that securely stores the `client_secret` and handles the token exchange.

      <Warning>
        Use this only if your backend can securely store the client secret (e.g. not in a browser or mobile app). Ideal for server-rendered or hybrid web apps.
      </Warning>

      ```bash Step 1: Frontend – Redirect the User to Log In [expandable] theme={null}
      // login.tsx or similar
      const clientId = 'YOUR_CLIENT_ID';
      const redirectUri = 'https://yourapp.com/callback'; // Must match what's registered in Corti OAuth

      const login = () => {
      const authUrl = new URL('https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/auth');
      authUrl.searchParams.set('response_type', 'code');
      authUrl.searchParams.set('client_id', clientId);
      authUrl.searchParams.set('redirect_uri', redirectUri);
      authUrl.searchParams.set('scope', 'openid profile');

      window.location.href = authUrl.toString(); // Send user to Corti login page
      };
      ```

      ```bash Step 2: Corti Redirects Back with a Code theme={null}
      https://yourapp.com/callback?code=abc123xyz
      ```

      ```bash Step 3: Backend – Exchange Code for Access Token [expandable] theme={null}
      // server.js or routes/callback.js
      const express = require('express');
      const fetch = require('node-fetch');
      const app = express();

      const CLIENT_ID = 'YOUR_CLIENT_ID';
      const CLIENT_SECRET = 'YOUR_CLIENT_SECRET';
      const REDIRECT_URI = 'https://yourapp.com/callback'; // Must match Step 1

      app.get('/callback', async (req, res) => {
      const authCode = req.query.code;

      if (!authCode) {
          return res.status(400).send('Missing authorization code');
      }

      // Exchange code for access token
      const tokenResponse = await fetch('https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token', {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
          grant_type: 'authorization_code',
          code: authCode,
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET,
          redirect_uri: REDIRECT_URI
          })
      });

      const tokenData = await tokenResponse.json();

      if (tokenData.error) {
          return res.status(500).send(`Token error: ${tokenData.error_description}`);
      }

      // Do something useful with the token, like create a session
      console.log('Access Token:', tokenData.access_token);

      // Optional: send token data to frontend or store in session
      res.redirect(`/app?token=${tokenData.access_token}`);
      });

      app.listen(3000, () => console.log('App listening on http://localhost:3000'));
      ```
    </Tab>

    <Tab title="C#">
      <Tip>
        This example uses standard .NET libraries for OAuth2 authorization code flow
      </Tip>

      ```csharp Step 1: Frontend – Redirect the User to Log In [expandable] theme={null}
      using System;
      using System.Web;

      public class AuthHelper
      {
          private const string ClientId = "YOUR_CLIENT_ID";
          private const string RedirectUri = "https://yourapp.com/callback";
          private const string AuthBaseUrl = "https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect";

          public static string GetAuthorizationUrl()
          {
              var authUrl = $"{AuthBaseUrl}/auth?" +
                  $"response_type=code&" +
                  $"client_id={HttpUtility.UrlEncode(ClientId)}&" +
                  $"redirect_uri={HttpUtility.UrlEncode(RedirectUri)}&" +
                  $"scope=openid profile";

              return authUrl;
          }
      }

      // Usage in Razor view or controller
      // Response.Redirect(AuthHelper.GetAuthorizationUrl());
      ```

      ```csharp Step 2: Corti Redirects Back with a Code theme={null}
      https://yourapp.com/callback?code=abc123xyz
      ```

      ```csharp Step 3: Backend – Exchange Code for Access Token [expandable] theme={null}
      using System;
      using System.Collections.Generic;
      using System.Net.Http;
      using System.Text.Json;
      using System.Threading.Tasks;
      using System.Web.Mvc;

      public class TokenExchange
      {
          private const string ClientId = "YOUR_CLIENT_ID";
          private const string ClientSecret = "YOUR_CLIENT_SECRET";
          private const string RedirectUri = "https://yourapp.com/callback";
          private const string TokenUrl = "https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token";

          public static async Task<TokenResponse> ExchangeCodeForTokenAsync(string code)
          {
              using var httpClient = new HttpClient();

              var formData = new List<KeyValuePair<string, string>>
              {
                  new KeyValuePair<string, string>("grant_type", "authorization_code"),
                  new KeyValuePair<string, string>("code", code),
                  new KeyValuePair<string, string>("client_id", ClientId),
                  new KeyValuePair<string, string>("client_secret", ClientSecret),
                  new KeyValuePair<string, string>("redirect_uri", RedirectUri)
              };

              var content = new FormUrlEncodedContent(formData);
              var response = await httpClient.PostAsync(TokenUrl, content);

              if (!response.IsSuccessStatusCode)
              {
                  var errorBody = await response.Content.ReadAsStringAsync();
                  throw new Exception($"Token exchange failed: {errorBody}");
              }

              var responseBody = await response.Content.ReadAsStringAsync();
              var tokenData = JsonSerializer.Deserialize<JsonElement>(responseBody);

              if (tokenData.TryGetProperty("error", out var error))
              {
                  var errorDescription = tokenData.TryGetProperty("error_description", out var desc)
                      ? desc.GetString()
                      : "Unknown error";
                  throw new Exception($"Token error: {errorDescription}");
              }

              return new TokenResponse
              {
                  AccessToken = tokenData.GetProperty("access_token").GetString(),
                  RefreshToken = tokenData.TryGetProperty("refresh_token", out var refresh)
                      ? refresh.GetString()
                      : null,
                  ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
              };
          }
      }

      public class TokenResponse
      {
          public string AccessToken { get; set; }
          public string RefreshToken { get; set; }
          public int ExpiresIn { get; set; }
      }

      // Usage in ASP.NET MVC Controller
      public class AuthController : Controller
      {
          public async Task<ActionResult> Callback(string code)
          {
              if (string.IsNullOrEmpty(code))
              {
                  return new HttpStatusCodeResult(400, "Missing authorization code");
              }

              try
              {
                  var tokenResponse = await TokenExchange.ExchangeCodeForTokenAsync(code);

                  // Store tokens securely (e.g., in session or secure cookie)
                  Session["access_token"] = tokenResponse.AccessToken;

                  // Redirect to app with token
                  return RedirectToAction("Index", "Home");
              }
              catch (Exception ex)
              {
                  return new HttpStatusCodeResult(500, $"Token error: {ex.Message}");
              }
          }
      }
      ```
    </Tab>
  </Tabs>

  Summary of the flow:

  | Step | Component | Description                                                                                         |
  | :--- | :-------- | :-------------------------------------------------------------------------------------------------- |
  | 1    | Frontend  | Redirects user to Corti login                                                                       |
  | 2    | Corti     | Redirects back to your app with `code`                                                              |
  | 3    | Backend   | Exchanges code + client secret for tokens <br /> Responds with session/token, redirects to frontend |

  Requirements:

  * Must use **HTTPS** for `redirect_uri`
  * Your `client_secret` **must not** be exposed to the frontend
  * The code returned is **valid for one use and short-lived**
</Accordion>

<Warning>
  Use only if the OAuth2 flow is entirely server-to-server.
</Warning>

***

### 3. Resource Owner Password Credentials (ROPC) grant (use with caution)

**Best for:** Controlled environments embedding Corti Assistant with trusted clients (e.g., internal tools).

**Why:** Allows username/password login directly in the app — but **bypasses the authorization server UI**.

**Risks:**

* Trains users to enter passwords into third-party apps.
* Easy to misuse, violates best practices.
* Only viable where UI constraints prevent redirecting (e.g., native kiosk apps without browsers).

<Accordion title="Code sample:">
  <Tabs>
    <Tab title="Using Corti JS SDK">
      <Tip>
        This example uses the [Corti JavaScript SDK](/sdk/js/overview) (`@corti/sdk`)
      </Tip>

      ```javascript [expandable] theme={null}
      import { CortiAuth, CortiClient, CortiEnvironment } from "@corti/sdk";

      const CLIENT_ID = "YOUR_CLIENT_ID";
      const USERNAME = "user@example.com";
      const PASSWORD = "your-password";

      // Step 1: Exchange credentials for tokens using ROPC flow
      const auth = new CortiAuth({
          environment: CortiEnvironment.Eu,
          tenantName: "YOUR_TENANT_NAME",
      });

      const tokenResponse = await auth.getRopcFlowToken({
          clientId: CLIENT_ID,
          username: USERNAME,
          password: PASSWORD,
      });

      const { accessToken, refreshToken } = tokenResponse;
      ```
    </Tab>

    <Tab title="Using Fetch">
      <Warning>
        Use only in trusted/internal scenarios.
      </Warning>

      Here's a simple fetch example:

      ```bash [expandable] theme={null}
      const response = await fetch('https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
          grant_type: 'password',
          client_id: 'YOUR_CLIENT_ID',
          username: 'user@example.com',
          password: 'yourpassword'
      })
      });

      const data = await response.json();
      console.log('Access Token:', data.access_token);
      ```
    </Tab>

    <Tab title="C#">
      <Tip>
        This example uses standard .NET libraries for OAuth2 ROPC flow
      </Tip>

      <Warning>
        Use only in trusted/internal scenarios.
      </Warning>

      ```csharp [expandable] theme={null}
      using System;
      using System.Collections.Generic;
      using System.Net.Http;
      using System.Text.Json;
      using System.Threading.Tasks;

      public class RopcAuth
      {
          private const string ClientId = "YOUR_CLIENT_ID";
          private const string TokenUrl = "https://auth.us.corti.app/realms/<realm-name>/protocol/openid-connect/token";

          public static async Task<TokenResponse> GetTokenAsync(string username, string password)
          {
              using var httpClient = new HttpClient();

              var formData = new List<KeyValuePair<string, string>>
              {
                  new KeyValuePair<string, string>("grant_type", "password"),
                  new KeyValuePair<string, string>("client_id", ClientId),
                  new KeyValuePair<string, string>("username", username),
                  new KeyValuePair<string, string>("password", password)
              };

              var content = new FormUrlEncodedContent(formData);
              var response = await httpClient.PostAsync(TokenUrl, content);

              response.EnsureSuccessStatusCode();

              var responseBody = await response.Content.ReadAsStringAsync();
              var tokenData = JsonSerializer.Deserialize<JsonElement>(responseBody);

              if (tokenData.TryGetProperty("error", out var error))
              {
                  var errorDescription = tokenData.TryGetProperty("error_description", out var desc)
                      ? desc.GetString()
                      : "Unknown error";
                  throw new Exception($"Authentication failed: {errorDescription}");
              }

              return new TokenResponse
              {
                  AccessToken = tokenData.GetProperty("access_token").GetString(),
                  RefreshToken = tokenData.TryGetProperty("refresh_token", out var refresh)
                      ? refresh.GetString()
                      : null,
                  ExpiresIn = tokenData.GetProperty("expires_in").GetInt32()
              };
          }
      }

      public class TokenResponse
      {
          public string AccessToken { get; set; }
          public string RefreshToken { get; set; }
          public int ExpiresIn { get; set; }
      }

      // Example usage
      class Program
      {
          static async Task Main()
          {
              try
              {
                  var tokenResponse = await RopcAuth.GetTokenAsync(
                      "user@example.com",
                      "yourpassword"
                  );

                  Console.WriteLine($"Access Token: {tokenResponse.AccessToken}");
                  Console.WriteLine($"Expires in: {tokenResponse.ExpiresIn} seconds");
              }
              catch (Exception ex)
              {
                  Console.WriteLine($"Error: {ex.Message}");
              }
          }
      }
      ```
    </Tab>
  </Tabs>
</Accordion>

<Warning>
  This authentication method is not recommended, but sometimes necessary.
</Warning>

***

## Final guidance

<Warning>
  **Important:** Embedded Corti Assistant requires user-based authentication. **Client credentials grant is NOT supported** as it does not provide user context.
</Warning>

Pick the flow that matches your interaction model as offered above. See further details and contact us for [support here](https://help.corti.app/en/articles/11156327-choosing-the-right-oauth2-flow-for-corti-integrations).

**What to use when:**

* **For embedded Corti Assistant** (e.g., in an iFrame/WebView): \
  **Use Authorization Code Flow with PKCE** to authenticate the end user securely. This is the recommended flow for most embedded integrations.
* **For server-side integrations** where you can securely store a client secret: \
  **Authorization Code Flow (without PKCE)** may be used, but ensure the client secret is never exposed to the frontend.
* **For constrained environments** without redirect capabilities: \
  **ROPC** may be used with caution, but only in trusted internal environments.
* **Client Credentials is NOT supported** — it does not provide user context and will not work with embedded Corti Assistant.

**Additional reference**:

* [OAuth 2.0 Authorization Framework (RFC 6749)](https://datatracker.ietf.org/doc/html/rfc6749)
* [IBM](https://www.ibm.com/think/topics/oauth)
