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

# OAuth Authorization

> Authenticate Max AI users in your standalone app using OAuth 2.0 Authorization Code flow

If your app runs **outside** the Max AI dashboard (a standalone web app, mobile app, or portal), you need a way to identify which Max AI user is logged in. The OAuth 2.0 Authorization Code flow lets users sign in with their Max AI account and share their identity with your app.

<Note>
  **Embedded apps** (running inside the Max AI iframe) don't need OAuth — they receive user context automatically via the [App Bridge](/guides/architecture#app-bridge). OAuth is only for **standalone/external apps**.
</Note>

## Prerequisites

Before setting up OAuth, you need:

1. **A developer account** — [Get started](/guides/getting-started) if you haven't already
2. **A marketplace app** — Create one in the developer console or via the [CLI](/guides/cli)
3. **An API key** — Generate one from the developer console under your app's **API Keys** tab (see [Authentication](/guides/authentication))

Your app's **slug** (used as `client_id` in OAuth) is visible in the developer console on your app's settings page.

## How It Works

<Steps>
  <Step title="Redirect to Max AI" icon="arrow-right">
    Your app sends the user to `/oauth/authorize` with your `client_id`, `redirect_uri`, and a random `state` nonce for CSRF protection.
  </Step>

  <Step title="User authenticates and consents" icon="user-check">
    Max AI handles login (via Clerk), then shows a consent screen where the user reviews your app's permissions and selects which organizations to authorize.
  </Step>

  <Step title="Redirect back with code" icon="arrow-left">
    Max AI redirects the user to your `redirect_uri` with a short-lived authorization `code` and the original `state` parameter.
  </Step>

  <Step title="Exchange code for user info" icon="server">
    Your **backend** calls `POST /v3/oauth/token` with the authorization code and your API key. This is a server-to-server call — the code is never exposed to the browser.
  </Step>

  <Step title="Receive identity" icon="circle-check">
    Max AI returns an OIDC `id_token` (ES256 JWT), user profile, and the list of authorized organizations with their facilities.
  </Step>
</Steps>

```mermaid theme={null}
sequenceDiagram
    participant App
    participant Browser
    participant MaxAI as Max AI

    App->>Browser: Redirect to /oauth/authorize
    Browser->>MaxAI: Authorize request
    MaxAI->>Browser: Login + Consent screen
    Browser->>MaxAI: User approves
    MaxAI->>Browser: Redirect with code
    Browser->>App: code + state
    App->>MaxAI: POST /v3/oauth/token
    MaxAI->>App: id_token + user + orgs
```

## Setup

### 1. Register Redirect URIs

Add your callback URLs to the `[oauth]` section of your app manifest (`max-ai.app.toml`):

```toml theme={null}
[oauth]
redirect_uris = [
  "https://myapp.com/auth/callback",
  "http://localhost:3000/auth/callback"
]
```

Or configure them in the developer console under your app version's **OAuth** tab.

<Warning>
  Redirect URIs must **exactly match** — including scheme, host, port, and path. No wildcards. `localhost` is allowed for development.
</Warning>

### 2. Redirect to Authorize

When a user wants to sign in with Max AI, redirect them to the authorize endpoint:

```
https://app.maxcare.ai/oauth/authorize
  ?client_id=your-app-slug
  &redirect_uri=https://myapp.com/auth/callback
  &state=random-csrf-nonce
  &response_type=code
```

| Parameter       | Required | Description                                                                         |
| --------------- | -------- | ----------------------------------------------------------------------------------- |
| `client_id`     | Yes      | Your app's slug (from the developer console)                                        |
| `redirect_uri`  | Yes      | Must exactly match a registered redirect URI                                        |
| `state`         | Yes      | Random string for CSRF protection — you must verify it matches on callback          |
| `response_type` | No       | Must be `code` (default)                                                            |
| `prompt`        | No       | Set to `consent` to force the consent screen even if the user previously authorized |

The user will see a consent screen showing your app's name, the permissions it requires, and which organizations to authorize.

### 3. Handle the Callback

After the user approves (or denies), Max AI redirects to your `redirect_uri`:

**On approve:**

```
https://myapp.com/auth/callback?code=abc123def456&state=random-csrf-nonce
```

**On deny:**

```
https://myapp.com/auth/callback?error=access_denied&state=random-csrf-nonce
```

<Warning>
  Always verify that the returned `state` matches the one you sent. This prevents CSRF attacks.
</Warning>

<Info>
  Authorization codes expire after **10 minutes** and can only be used **once**. Exchange them immediately.
</Info>

### 4. Exchange the Code

Your **backend** exchanges the authorization code for user info by calling the token endpoint. Authenticate with your app's API key:

```bash theme={null}
curl -X POST "https://api.maxcare.ai/v3/oauth/token" \
  -H "Authorization: Bearer max_prd_ak_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "abc123def456",
    "redirect_uri": "https://myapp.com/auth/callback"
  }'
```

<Note>
  The token endpoint does **not** require the `X-Organization-Id` header. The authorization code already encodes which organizations the user authorized.
</Note>

### 5. Receive User Info

The token endpoint returns the user's identity, authorized organizations, and an OIDC-compliant `id_token`:

```json theme={null}
{
  "id_token": "eyJhbGciOiJFUzI1NiIs...",
  "user": {
    "id": "usr_1231b6f32b4f4b8f8eeb4f7806bc45b0",
    "email": "jane@clinic.com",
    "firstName": "Jane",
    "lastName": "Doe",
    "imageUrl": "https://img.clerk.com/..."
  },
  "authorizedOrganizations": [
    {
      "id": "org_7e2c8cfeb7a94deb986de7012589e72b",
      "name": "Dermatology Clinic",
      "role": "admin",
      "facilities": [
        {
          "id": "fac_a1b2c3d4...",
          "name": "Main Office",
          "address": "123 Main St, Austin, TX 78701"
        }
      ]
    }
  ]
}
```

| Field                     | Description                                                                         |
| ------------------------- | ----------------------------------------------------------------------------------- |
| `id_token`                | OIDC-compliant JWT (ES256-signed). Verify via the [JWKS endpoint](#openid-connect). |
| `user`                    | The authenticated Max AI user                                                       |
| `authorizedOrganizations` | Organizations the user selected, with their role and facilities                     |

### 6. Make API Calls

Now that you know the user and their organizations, use the organization IDs as the `X-Organization-Id` header when calling the [Public API](/guides/authentication):

```bash theme={null}
curl -X GET "https://api.maxcare.ai/v3/patients" \
  -H "Authorization: Bearer max_prd_ak_YOUR_API_KEY" \
  -H "X-Organization-Id: 7e2c8cfe-b7a9-4deb-986d-e7012589e72b"
```

The `X-Organization-Id` must be one of the organization IDs returned in `authorizedOrganizations`. Your API key provides data access; the OAuth flow provides **user identity**.

## Code Example

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const crypto = require("crypto");

  // Step 1: Redirect to Max AI
  app.get("/auth/login", (req, res) => {
    const state = crypto.randomBytes(16).toString("hex");
    req.session.oauthState = state;

    const params = new URLSearchParams({
      client_id: "your-app-slug",
      redirect_uri: "https://myapp.com/auth/callback",
      state,
      response_type: "code",
    });

    res.redirect(`https://app.maxcare.ai/oauth/authorize?${params}`);
  });

  // Step 2: Handle callback
  app.get("/auth/callback", async (req, res) => {
    const { code, state, error } = req.query;

    if (error) return res.redirect(`/login?error=${error}`);
    if (state !== req.session.oauthState) return res.status(403).send("Invalid state");

    // Step 3: Exchange code for user info
    const response = await fetch("https://api.maxcare.ai/v3/oauth/token", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.MAXAI_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        grant_type: "authorization_code",
        code,
        redirect_uri: "https://myapp.com/auth/callback",
      }),
    });

    const data = await response.json();

    if (data.error) return res.redirect(`/login?error=${data.error}`);

    // Step 4: Create your app's session
    req.session.user = data.user;
    req.session.organizations = data.authorizedOrganizations;
    res.redirect("/dashboard");
  });
  ```

  ```python Python (Flask) theme={null}
  import os, secrets, requests
  from urllib.parse import urlencode
  from flask import Flask, redirect, request, session

  app = Flask(__name__)

  @app.route("/auth/login")
  def login():
      state = secrets.token_hex(16)
      session["oauth_state"] = state

      params = {
          "client_id": "your-app-slug",
          "redirect_uri": "https://myapp.com/auth/callback",
          "state": state,
          "response_type": "code",
      }
      return redirect(f"https://app.maxcare.ai/oauth/authorize?{urlencode(params)}")

  @app.route("/auth/callback")
  def callback():
      if request.args.get("error"):
          return redirect(f"/login?error={request.args['error']}")

      if request.args.get("state") != session.get("oauth_state"):
          return "Invalid state", 403

      resp = requests.post(
          "https://api.maxcare.ai/v3/oauth/token",
          headers={
              "Authorization": f"Bearer {os.environ['MAXAI_API_KEY']}",
              "Content-Type": "application/json",
          },
          json={
              "grant_type": "authorization_code",
              "code": request.args["code"],
              "redirect_uri": "https://myapp.com/auth/callback",
          },
      )

      data = resp.json()
      if "error" in data:
          return redirect(f"/login?error={data['error']}")

      session["user"] = data["user"]
      session["organizations"] = data["authorizedOrganizations"]
      return redirect("/dashboard")
  ```
</CodeGroup>

## Token Exchange Errors

The token endpoint returns OAuth 2.0 spec-compliant errors:

```json theme={null}
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}
```

| `error`                  | HTTP Status | Description                                                             |
| ------------------------ | ----------- | ----------------------------------------------------------------------- |
| `invalid_request`        | 400         | Missing required parameter (`grant_type`, `code`, or `redirect_uri`)    |
| `unsupported_grant_type` | 400         | Only `authorization_code` is supported                                  |
| `invalid_grant`          | 400         | Code is invalid, expired, already used, or `redirect_uri` doesn't match |
| `invalid_client`         | 401         | API key is missing, invalid, expired, or revoked                        |
| `server_error`           | 500         | OIDC signing is not configured on the server                            |

## Repeat Authorization

When a user authorizes your app, the grant is remembered. On subsequent OAuth flows:

* **Same permissions** — the user is redirected back instantly (no consent screen)
* **Permissions changed** — the consent screen is shown again
* **Force re-consent** — add `prompt=consent` to the authorize URL to let users change which organizations they share

## OpenID Connect

The `id_token` is a standard OIDC JWT signed with ES256. You can verify it using the public key from the JWKS endpoint:

| Endpoint       | URL                                     |
| -------------- | --------------------------------------- |
| OIDC Discovery | `GET /.well-known/openid-configuration` |
| JWKS           | `GET /.well-known/jwks.json`            |

The `id_token` contains standard OIDC claims:

| Claim         | Description                                 |
| ------------- | ------------------------------------------- |
| `iss`         | Issuer URL (e.g., `https://app.maxcare.ai`) |
| `sub`         | User ID                                     |
| `aud`         | Your app's slug                             |
| `iat` / `exp` | Issued at / expires at (1 hour)             |
| `email`       | User's email                                |
| `given_name`  | First name                                  |
| `family_name` | Last name                                   |
| `picture`     | Profile image URL                           |

## Embedded vs External Apps

|                  | Embedded (iframe)                  | External (standalone)                |
| ---------------- | ---------------------------------- | ------------------------------------ |
| **Runs inside**  | Max AI dashboard                   | Your own domain                      |
| **User context** | App Bridge SDK                     | OAuth flow                           |
| **Auth method**  | Session token (automatic)          | API key + OAuth code exchange        |
| **Setup**        | Set `appUrl` in manifest           | Set `redirect_uris` in manifest      |
| **Use when**     | App is part of the clinic workflow | App has its own portal or mobile app |

You can support **both** — use the App Bridge when embedded, and OAuth when accessed standalone.
