> ## Documentation Index
> Fetch the complete documentation index at: https://cubed3-mikhail-cub-3599-rebuild-driver-on-config-change.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up per-user OAuth

> Configure Cube to authenticate each user with their own OAuth token, falling back to a service account for liveness checks.

<Note>
  This feature is in beta. Reach out to your account manager to have it
  enabled for your Cube Cloud deployment.
</Note>

## Use case

You want each user's queries to run under their own database identity
using OAuth tokens managed by Cube Cloud. When a user's token is
unavailable or expired, Cube falls back to a service account so that
connectivity checks and background operations still work.

This pattern applies to any data source that supports OAuth, including
[Databricks][ref-databricks-jdbc] and [Snowflake][ref-snowflake]. The
examples below use Databricks; switch the `userCredentials` key and
driver options for any other OAuth-capable data source.

Because every user connects with different credentials, you also need
per-user query orchestrator state. Without this, one user's cached
connection could leak to another.

<Warning>
  Cube caches one database connection per
  [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] for the
  lifetime of the process. **Every input your `driver_factory` reads must
  therefore also appear in the orchestrator ID.** OAuth access tokens rotate
  (typically hourly), so an ID built from the username alone leaves the
  cached connection pinned to the token it was first built with — new
  database sessions then fail to authenticate until the next deploy. The
  configuration below derives both from one helper so they cannot drift.
</Warning>

## Prerequisites

* A [Cube Cloud][ref-cube-cloud] deployment connected to an
  OAuth-capable data source
* OAuth configured in your data source so that Cube Cloud can
  obtain per-user tokens (via the **User Credentials** feature)
* A service account credential (token or password) stored as an
  environment variable for fallback connectivity

<Warning>
  The service account credential is used only as a fallback for Cube's
  internal liveness checks and background operations. Grant it the minimum
  permissions necessary — ideally read-only access to the required schemas —
  to limit exposure if the credential is compromised.
</Warning>

## Set up the OAuth app

Before configuring Cube to use per-user OAuth, register your data
source as an OAuth app in Cube Cloud:

<Steps>
  <Step title="Open the OAuth apps settings">
    In Cube Cloud, go to **Admin → Integrations → OAuth apps** and click
    **Add**.

    <Frame>
      <img src="https://mintcdn.com/cubed3-mikhail-cub-3599-rebuild-driver-on-config-change/BzN_5Iznrv0i06GI/images/admin/connect-to-data/oauth-add-app.png?fit=max&auto=format&n=BzN_5Iznrv0i06GI&q=85&s=aace94fb1e279dc01fde9b7c09c7dce2" alt="Admin Integrations page showing the OAuth apps section with the Add button" width="2730" height="2112" data-path="images/admin/connect-to-data/oauth-add-app.png" />
    </Frame>
  </Step>

  <Step title="Fill out the OAuth app details">
    Provide the OAuth app metadata from your data source: **Name**,
    **Auth URL**, **Token URL**, **Client ID**, **Client Secret**, and any
    required **Scopes**. Copy the **Redirect URI** shown in this form and
    register it with your data source's OAuth provider, then click
    **Create**.

    <Frame>
      <img src="https://mintcdn.com/cubed3-mikhail-cub-3599-rebuild-driver-on-config-change/BzN_5Iznrv0i06GI/images/admin/connect-to-data/oauth-fill-fields.png?fit=max&auto=format&n=BzN_5Iznrv0i06GI&q=85&s=6de6e567e4586bbd691ede2e9e2e195e" alt="New OAuth app form with fields for Name, Auth URL, Token URL, Client ID, Client Secret, Scopes, and Redirect URI" width="2772" height="2114" data-path="images/admin/connect-to-data/oauth-fill-fields.png" />
    </Frame>
  </Step>

  <Step title="Authorize the app">
    Open the sidebar and go to **Connected apps**. Find your OAuth app
    and click **Authorize** to generate an access token.

    You'll need to repeat this step whenever the token expires.

    <Frame>
      <img src="https://mintcdn.com/cubed3-mikhail-cub-3599-rebuild-driver-on-config-change/BzN_5Iznrv0i06GI/images/admin/connect-to-data/oauth-authorize.png?fit=max&auto=format&n=BzN_5Iznrv0i06GI&q=85&s=69f9b92f4cfdac7d1e1a19887a502197" alt="Connected apps page showing the OAuth integration with an Authorize action" width="3012" height="1596" data-path="images/admin/connect-to-data/oauth-authorize.png" />
    </Frame>
  </Step>
</Steps>

## Configuration

The configuration uses two options from the
[configuration file reference][ref-config]:

* [`driver_factory`][ref-driver-factory] — dynamically selects the
  authentication credential per request
* [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] — gives
  each user their own query orchestrator instance (database connections,
  execution queues, pre-aggregation table caches)

### Environment variables

Set the environment variables for your data source. The examples below
show Databricks and Snowflake; adapt them to your specific setup.

<Tabs>
  <Tab title="Databricks">
    ```dotenv theme={null}
    CUBEJS_DB_TYPE=databricks-jdbc
    CUBEJS_DB_DATABRICKS_URL=jdbc:databricks://dbc-XXXXXXX-XXXX.cloud.databricks.com:443/default;transportMode=http;ssl=1;httpPath=sql/protocolv1/o/XXXXX/XXXXX;AuthMech=3;UID=token
    CUBEJS_DB_DATABRICKS_TOKEN=dapi_service_account_token
    CUBEJS_DB_DATABRICKS_ACCEPT_POLICY=true
    # Optional: specify a catalog
    CUBEJS_DB_DATABRICKS_CATALOG=my_catalog
    ```
  </Tab>

  <Tab title="Snowflake">
    ```dotenv theme={null}
    CUBEJS_DB_TYPE=snowflake
    CUBEJS_DB_SNOWFLAKE_ACCOUNT=XXXXXXXXX.us-east-1
    CUBEJS_DB_SNOWFLAKE_WAREHOUSE=MY_SNOWFLAKE_WAREHOUSE
    CUBEJS_DB_NAME=my_snowflake_database
    CUBEJS_DB_USER=service_account_user
    CUBEJS_DB_PASS=service_account_password
    CUBEJS_DB_SNOWFLAKE_ROLE=MY_ROLE
    ```
  </Tab>
</Tabs>

### Configuration file

The examples below use Databricks. To target a different data source,
swap `userCredentials.databricks` for the matching key (for example,
`userCredentials.snowflake`) and update the `driver_factory` return
value with the correct `type` and driver-specific options. See the
[data sources reference][ref-data-sources] for available drivers.

<Tabs>
  <Tab title="Python">
    ```python cube.py theme={null}
    from cube import config
    from datetime import datetime, timezone
    import os
    import time

    # Don't hand the driver a token that is about to expire. Drivers cache their
    # connection settings, and the connection pool opens new sessions long after
    # the driver was built, so "valid right now" is not enough.
    EXPIRY_SKEW_SECONDS = 120


    def _parse_expiry(value):
        if not value:
            return None
        try:
            parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        except ValueError:
            return None
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        return parsed.timestamp()


    def _credential(ctx: dict):
        """Resolve the credential for this request, plus the key identifying it.

        driver_factory and context_to_orchestrator_id both call this, so the cached
        connection can never drift from the token it was built with.
        """
        # For other data sources, swap "databricks" for "snowflake", etc.
        cube_cloud = (ctx.get("securityContext") or {}).get("cubeCloud") or {}
        creds = (cube_cloud.get("userCredentials") or {}).get("databricks") or {}

        access_token = creds.get("accessToken")
        expires_at = _parse_expiry(creds.get("accessTokenExpiresAt"))

        # Gate on the expiry rather than on `status`: a failed background refresh
        # can flag the record while the token already in hand is still valid, and
        # treating that as fatal drops the user onto the service account for no
        # reason.
        if access_token and expires_at and expires_at > time.time() + EXPIRY_SKEW_SECONDS:
            # Key on the expiry, never on the token itself — the orchestrator ID is
            # used as a cache prefix and appears in logs.
            return access_token, f"u{int(expires_at)}"

        return os.environ["CUBEJS_DB_DATABRICKS_TOKEN"], "service-account"


    @config("driver_factory")
    def driver_factory(ctx: dict) -> dict:
        token, _ = _credential(ctx)

        return {
            "type": "databricks-jdbc",
            "url": os.environ["CUBEJS_DB_DATABRICKS_URL"],
            "token": token,
            "acceptPolicy": True,
            "catalog": os.environ.get("CUBEJS_DB_DATABRICKS_CATALOG"),
        }


    @config("context_to_orchestrator_id")
    def context_to_orchestrator_id(ctx: dict) -> str:
        # One orchestrator per (user, credential): separate DB connections,
        # execution queues and pre-aggregation caches, and a cache key that changes
        # when the token rotates.
        cube_cloud = (ctx.get("securityContext") or {}).get("cubeCloud") or {}
        username = cube_cloud.get("username") or "default"
        _, cache_key = _credential(ctx)

        return f"CUBE_APP_{username}_{cache_key}"
    ```
  </Tab>

  <Tab title="JavaScript">
    ```javascript cube.js theme={null}
    // Don't hand the driver a token that is about to expire. Drivers cache their
    // connection settings, and the connection pool opens new sessions long after
    // the driver was built, so "valid right now" is not enough.
    const EXPIRY_SKEW_MS = 120 * 1000;

    /**
     * Resolve the credential for this request, plus the key identifying it.
     *
     * driverFactory and contextToOrchestratorId both call this, so the cached
     * connection can never drift from the token it was built with.
     */
    function resolveCredential(securityContext) {
      // For other data sources, swap `databricks` for `snowflake`, etc.
      const creds = securityContext?.cubeCloud?.userCredentials?.databricks ?? {};
      const expiresAt = Date.parse(creds.accessTokenExpiresAt ?? "");

      // Gate on the expiry rather than on `status`: a failed background refresh can
      // flag the record while the token already in hand is still valid, and
      // treating that as fatal drops the user onto the service account for no
      // reason.
      if (creds.accessToken && expiresAt > Date.now() + EXPIRY_SKEW_MS) {
        // Key on the expiry, never on the token itself — the orchestrator ID is
        // used as a cache prefix and appears in logs.
        return { token: creds.accessToken, cacheKey: `u${expiresAt}` };
      }

      return {
        token: process.env.CUBEJS_DB_DATABRICKS_TOKEN,
        cacheKey: "service-account",
      };
    }

    module.exports = {
      driverFactory: ({ securityContext }) => ({
        type: "databricks-jdbc",
        url: process.env.CUBEJS_DB_DATABRICKS_URL,
        token: resolveCredential(securityContext).token,
        acceptPolicy: true,
        catalog: process.env.CUBEJS_DB_DATABRICKS_CATALOG,
      }),

      // One orchestrator per (user, credential): separate DB connections, execution
      // queues and pre-aggregation caches, and a cache key that changes when the
      // token rotates.
      contextToOrchestratorId: ({ securityContext }) => {
        const username = securityContext?.cubeCloud?.username ?? "default";
        const { cacheKey } = resolveCredential(securityContext);

        return `CUBE_APP_${username}_${cacheKey}`;
      },
    };
    ```
  </Tab>
</Tabs>

## How it works

1. **User makes a request** — Cube Cloud attaches the user's OAuth
   credentials to `securityContext.cubeCloud.userCredentials.<data_source>`
   (for example, `.databricks` or `.snowflake`).

2. **`driver_factory` resolves the credential** — If the user has a token
   that has not expired, it is used. Otherwise, Cube falls back to the
   service account credential stored in environment variables.

3. **Per-user, per-credential orchestrator** —
   [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] returns a
   key derived from both the username and the credential in use, so each
   user gets their own database connection pool, execution queues, and
   pre-aggregation table cache — and a rotated token produces a fresh
   connection instead of reusing one built from the previous token. Keying
   on the username alone would share a single cached connection across
   token rotations; omitting the username would share one across users.

## Operational notes

* **Expect one orchestrator per token rotation.** Each distinct
  orchestrator ID holds its own connection pool, queues and
  pre-aggregation table cache, and tokens typically rotate hourly. Watch
  memory on deployments with many concurrent users, and note that the
  first query after a rotation runs against a cold pre-aggregation cache.
* **Don't make [`context_to_app_id`][ref-context-to-appid] per-user.** The
  data model is identical for every user — only the connection differs —
  so a per-user app ID forces a full data-model recompile per user on
  every replica for no benefit. Leave it unset, or return a constant if
  your deployment already sets one.
* **Give the service account the minimum it needs to pass a connection
  check.** If it has no access at all, liveness checks and any query that
  falls back to it fail with an opaque authorization error from the driver
  rather than something diagnosable.

[ref-config]: /reference/configuration/config

[ref-driver-factory]: /reference/configuration/config#driver_factory

[ref-context-to-orchestrator-id]: /reference/configuration/config#context_to_orchestrator_id

[ref-context-to-appid]: /reference/configuration/config#context_to_app_id

[ref-databricks-jdbc]: /admin/connect-to-data/data-sources/databricks-jdbc

[ref-snowflake]: /admin/connect-to-data/data-sources/snowflake

[ref-data-sources]: /admin/connect-to-data/data-sources

[ref-cube-cloud]: /docs/introduction
