|
| 1 | +--- |
| 2 | +title: Set up per-user OAuth |
| 3 | +description: Configure Cube to authenticate each user with their own OAuth token, falling back to a service account for liveness checks. |
| 4 | +--- |
| 5 | + |
| 6 | +<Note> |
| 7 | + |
| 8 | +This feature is in beta. Reach out to your account manager to have it |
| 9 | +enabled for your Cube Cloud deployment. |
| 10 | + |
| 11 | +</Note> |
| 12 | + |
| 13 | +## Use case |
| 14 | + |
| 15 | +You want each user's queries to run under their own database identity |
| 16 | +using OAuth tokens managed by Cube Cloud. When a user's token is |
| 17 | +unavailable or expired, Cube falls back to a service account so that |
| 18 | +connectivity checks and background operations still work. |
| 19 | + |
| 20 | +This pattern applies to any data source that supports OAuth, including |
| 21 | +[Databricks][ref-databricks-jdbc] and [Snowflake][ref-snowflake]. The |
| 22 | +examples below use Databricks; switch the `userCredentials` key and |
| 23 | +driver options for any other OAuth-capable data source. |
| 24 | + |
| 25 | +Because every user connects with different credentials, you also need |
| 26 | +per-user query orchestrator state. Without this, one user's cached |
| 27 | +connection could leak to another. |
| 28 | + |
| 29 | +## Prerequisites |
| 30 | + |
| 31 | +- A [Cube Cloud][ref-cube-cloud] deployment connected to an |
| 32 | + OAuth-capable data source |
| 33 | +- OAuth configured in your data source so that Cube Cloud can |
| 34 | + obtain per-user tokens (via the **User Credentials** feature) |
| 35 | +- A service account credential (token or password) stored as an |
| 36 | + environment variable for fallback connectivity |
| 37 | + |
| 38 | +<Warning> |
| 39 | + |
| 40 | +The service account credential is used only as a fallback for Cube's |
| 41 | +internal liveness checks and background operations. Grant it the minimum |
| 42 | +permissions necessary β ideally read-only access to the required schemas β |
| 43 | +to limit exposure if the credential is compromised. |
| 44 | + |
| 45 | +</Warning> |
| 46 | + |
| 47 | +## Set up the OAuth app |
| 48 | + |
| 49 | +Before configuring Cube to use per-user OAuth, register your data |
| 50 | +source as an OAuth app in Cube Cloud: |
| 51 | + |
| 52 | +<Steps> |
| 53 | + |
| 54 | +<Step title="Open the OAuth apps settings"> |
| 55 | + |
| 56 | +In Cube Cloud, go to **Admin β Integrations β OAuth apps** and click |
| 57 | +**Add**. |
| 58 | + |
| 59 | +<Frame> |
| 60 | + <img src="/images/admin/connect-to-data/oauth-add-app.png" alt="Admin Integrations page showing the OAuth apps section with the Add button" /> |
| 61 | +</Frame> |
| 62 | + |
| 63 | +</Step> |
| 64 | + |
| 65 | +<Step title="Fill out the OAuth app details"> |
| 66 | + |
| 67 | +Provide the OAuth app metadata from your data source: **Name**, |
| 68 | +**Auth URL**, **Token URL**, **Client ID**, **Client Secret**, and any |
| 69 | +required **Scopes**. Copy the **Redirect URI** shown in this form and |
| 70 | +register it with your data source's OAuth provider, then click |
| 71 | +**Create**. |
| 72 | + |
| 73 | +<Frame> |
| 74 | + <img src="/images/admin/connect-to-data/oauth-fill-fields.png" alt="New OAuth app form with fields for Name, Auth URL, Token URL, Client ID, Client Secret, Scopes, and Redirect URI" /> |
| 75 | +</Frame> |
| 76 | + |
| 77 | +</Step> |
| 78 | + |
| 79 | +<Step title="Authorize the app"> |
| 80 | + |
| 81 | +Open the user menu and go to **Connected apps**. Find your OAuth app |
| 82 | +and click **Authorize** to generate an access token. |
| 83 | + |
| 84 | +You'll need to repeat this step whenever the token expires. |
| 85 | + |
| 86 | +<Frame> |
| 87 | + <img src="/images/admin/connect-to-data/oauth-authorize.png" alt="Connected apps page showing the OAuth integration with an Authorize action" /> |
| 88 | +</Frame> |
| 89 | + |
| 90 | +</Step> |
| 91 | + |
| 92 | +</Steps> |
| 93 | + |
| 94 | +## Configuration |
| 95 | + |
| 96 | +The configuration uses two options from the |
| 97 | +[configuration file reference][ref-config]: |
| 98 | + |
| 99 | +- [`driver_factory`][ref-driver-factory] β dynamically selects the |
| 100 | + authentication credential per request |
| 101 | +- [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] β gives |
| 102 | + each user their own query orchestrator instance (database connections, |
| 103 | + execution queues, pre-aggregation table caches) |
| 104 | + |
| 105 | +### Environment variables |
| 106 | + |
| 107 | +Set the environment variables for your data source. The examples below |
| 108 | +show Databricks and Snowflake; adapt them to your specific setup. |
| 109 | + |
| 110 | +<Tabs> |
| 111 | + |
| 112 | +<Tab title="Databricks"> |
| 113 | + |
| 114 | +```dotenv |
| 115 | +CUBEJS_DB_TYPE=databricks-jdbc |
| 116 | +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 |
| 117 | +CUBEJS_DB_DATABRICKS_TOKEN=dapi_service_account_token |
| 118 | +CUBEJS_DB_DATABRICKS_ACCEPT_POLICY=true |
| 119 | +# Optional: specify a catalog |
| 120 | +CUBEJS_DB_DATABRICKS_CATALOG=my_catalog |
| 121 | +``` |
| 122 | + |
| 123 | +</Tab> |
| 124 | + |
| 125 | +<Tab title="Snowflake"> |
| 126 | + |
| 127 | +```dotenv |
| 128 | +CUBEJS_DB_TYPE=snowflake |
| 129 | +CUBEJS_DB_SNOWFLAKE_ACCOUNT=XXXXXXXXX.us-east-1 |
| 130 | +CUBEJS_DB_SNOWFLAKE_WAREHOUSE=MY_SNOWFLAKE_WAREHOUSE |
| 131 | +CUBEJS_DB_NAME=my_snowflake_database |
| 132 | +CUBEJS_DB_USER=service_account_user |
| 133 | +CUBEJS_DB_PASS=service_account_password |
| 134 | +CUBEJS_DB_SNOWFLAKE_ROLE=MY_ROLE |
| 135 | +``` |
| 136 | + |
| 137 | +</Tab> |
| 138 | + |
| 139 | +</Tabs> |
| 140 | + |
| 141 | +### Configuration file |
| 142 | + |
| 143 | +The examples below use Databricks. To target a different data source, |
| 144 | +swap `userCredentials.databricks` for the matching key (for example, |
| 145 | +`userCredentials.snowflake`) and update the `driver_factory` return |
| 146 | +value with the correct `type` and driver-specific options. See the |
| 147 | +[data sources reference][ref-data-sources] for available drivers. |
| 148 | + |
| 149 | +<Tabs> |
| 150 | + |
| 151 | +<Tab title="Python"> |
| 152 | + |
| 153 | +```python cube.py |
| 154 | +from cube import config |
| 155 | +import os |
| 156 | + |
| 157 | + |
| 158 | +@config("driver_factory") |
| 159 | +def driver_factory(ctx: dict) -> dict: |
| 160 | + # Extract the Cube Cloud security context, which contains |
| 161 | + # per-user OAuth credentials when available. |
| 162 | + # For other data sources, swap "databricks" for "snowflake", etc. |
| 163 | + databricks_creds = ( |
| 164 | + ctx |
| 165 | + .get("securityContext", {}) |
| 166 | + .get("cubeCloud", {}) |
| 167 | + .get("userCredentials", {}) |
| 168 | + .get("databricks", {}) |
| 169 | + ) |
| 170 | + |
| 171 | + # Only use the OAuth token when the credential status is "active". |
| 172 | + # An expired or revoked token falls back to the service account. |
| 173 | + oauth_token = ( |
| 174 | + databricks_creds.get("accessToken") |
| 175 | + if databricks_creds.get("status") == "active" |
| 176 | + else None |
| 177 | + ) |
| 178 | + |
| 179 | + return { |
| 180 | + "type": "databricks-jdbc", |
| 181 | + "url": os.environ["CUBEJS_DB_DATABRICKS_URL"], |
| 182 | + # Prefer the user's OAuth token; fall back to the service account token |
| 183 | + "token": oauth_token or os.environ["CUBEJS_DB_DATABRICKS_TOKEN"], |
| 184 | + "acceptPolicy": True, |
| 185 | + "catalog": os.environ.get("CUBEJS_DB_DATABRICKS_CATALOG"), |
| 186 | + } |
| 187 | + |
| 188 | + |
| 189 | +@config("context_to_orchestrator_id") |
| 190 | +def context_to_orchestrator_id(ctx: dict) -> str: |
| 191 | + # Give each user a separate orchestrator instance (DB connections, |
| 192 | + # execution queues, pre-aggregation caches) |
| 193 | + username = ( |
| 194 | + ctx |
| 195 | + .get("securityContext", {}) |
| 196 | + .get("cubeCloud", {}) |
| 197 | + .get("username", "default") |
| 198 | + ) |
| 199 | + return f"CUBE_APP_{username}" |
| 200 | +``` |
| 201 | + |
| 202 | +</Tab> |
| 203 | + |
| 204 | +<Tab title="JavaScript"> |
| 205 | + |
| 206 | +```javascript cube.js |
| 207 | +module.exports = { |
| 208 | + driverFactory: ({ securityContext }) => { |
| 209 | + // Extract the Cube Cloud security context, which contains |
| 210 | + // per-user OAuth credentials when available. |
| 211 | + // For other data sources, swap `databricks` for `snowflake`, etc. |
| 212 | + const databricksCreds = |
| 213 | + securityContext?.cubeCloud?.userCredentials?.databricks ?? {}; |
| 214 | + |
| 215 | + // Only use the OAuth token when the credential status is "active". |
| 216 | + // An expired or revoked token falls back to the service account. |
| 217 | + const oauthToken = |
| 218 | + databricksCreds.status === "active" |
| 219 | + ? databricksCreds.accessToken |
| 220 | + : null; |
| 221 | + |
| 222 | + return { |
| 223 | + type: "databricks-jdbc", |
| 224 | + url: process.env.CUBEJS_DB_DATABRICKS_URL, |
| 225 | + // Prefer the user's OAuth token; fall back to the service account token |
| 226 | + token: oauthToken || process.env.CUBEJS_DB_DATABRICKS_TOKEN, |
| 227 | + acceptPolicy: true, |
| 228 | + catalog: process.env.CUBEJS_DB_DATABRICKS_CATALOG, |
| 229 | + }; |
| 230 | + }, |
| 231 | + |
| 232 | + // Give each user a separate orchestrator instance (DB connections, |
| 233 | + // execution queues, pre-aggregation caches) |
| 234 | + contextToOrchestratorId: ({ securityContext }) => { |
| 235 | + const username = securityContext?.cubeCloud?.username ?? "default"; |
| 236 | + return `CUBE_APP_${username}`; |
| 237 | + }, |
| 238 | +}; |
| 239 | +``` |
| 240 | +
|
| 241 | +</Tab> |
| 242 | +
|
| 243 | +</Tabs> |
| 244 | +
|
| 245 | +## How it works |
| 246 | +
|
| 247 | +1. **User makes a request** β Cube Cloud attaches the user's OAuth |
| 248 | + credentials to `securityContext.cubeCloud.userCredentials.<data_source>` |
| 249 | + (for example, `.databricks` or `.snowflake`). |
| 250 | +
|
| 251 | +2. **`driver_factory` resolves the credential** β If the credential status |
| 252 | + is `active`, the user's OAuth token is used. Otherwise, Cube falls back |
| 253 | + to the service account credential stored in environment variables. |
| 254 | +
|
| 255 | +3. **Per-user orchestrator** β |
| 256 | + [`context_to_orchestrator_id`][ref-context-to-orchestrator-id] returns |
| 257 | + a unique key per username, so each user gets their own database |
| 258 | + connection pool, execution queues, and pre-aggregation table cache. |
| 259 | + Without this, Cube would share a single cached connection across all |
| 260 | + users, causing one user's credentials to be reused for another user's |
| 261 | + queries. |
| 262 | +
|
| 263 | +[ref-config]: /reference/configuration/config |
| 264 | +[ref-driver-factory]: /reference/configuration/config#driver_factory |
| 265 | +[ref-context-to-orchestrator-id]: /reference/configuration/config#context_to_orchestrator_id |
| 266 | +[ref-databricks-jdbc]: /admin/connect-to-data/data-sources/databricks-jdbc |
| 267 | +[ref-snowflake]: /admin/connect-to-data/data-sources/snowflake |
| 268 | +[ref-data-sources]: /admin/connect-to-data/data-sources |
| 269 | +[ref-cube-cloud]: /docs/introduction |
0 commit comments