ociforge

← How-to guides

Setting up Ocient OAuth/OIDC SSO with Auth0

A step-by-step guide for a system administrator to put an Ocient System behind OAuth 2.0 / OpenID Connect single sign-on, covering the things that are easy to get wrong:

  1. An SSO-only connectivity pool (no password logins on the public endpoint).
  2. Multiple databases — configure SSO once on system and have every other database (sandbox, etc.) inherit it.
  3. Separate identity-provider applications for the two very different login styles: interactive device-grant (humans, headless CLIs) and machine-to-machine (scripts, services) — Ocient and OAuth force this split.

Scope. Concrete steps are written for Auth0 as the identity provider (IdP), because the app-type rules and quirks below are Auth0-specific. But the Ocient-side mechanics — issuer formatting, the azp/client_id rule, how a token is matched to an integration, multi-database fallback, the connectivity pool — apply to any OIDC provider (Okta, Entra ID, Keycloak, Google). Where a point is Auth0-only it is flagged.

This guide is IdP + DDL only. It does not cover provisioning the IdP as code (Terraform) or any one organization's specific automation.


0. Concepts that will bite you if you skip them

Read these once; every troubleshooting symptom below traces back to one of them.


1. Prerequisites

Throughout: replace auth.example.com (IdP), db.example.com (advertised SQL endpoint), 10.0.0.20 (SQL Node private IP), and the database/integration/group names with your own.


2. Part A — Identity provider (Auth0)

You will create two applications plus one API.

2a. Interactive application (humans + headless CLIs)

This drives the browser Authorization Code flow and the Device Authorization flow that pyocient/JDBC use on machines without a browser.

Trade-off (Auth0): public/native clients are treated as untrusted, so Auth0 re-displays the consent screen on every login and never remembers the grant. If you do not need device-grant (every user has a browser), use a Regular Web (confidential) app with Authorization Code instead — it skips consent. You lose headless device-grant but gain no-consent logins. You cannot have both on one app.

2b. Machine-to-machine application + API (scripts/services)

Client Credentials needs a confidential app and an API (resource server) to issue tokens for.

2c. (Optional) group/role claims for finer access

Interactive logins can carry a custom claim that Ocient maps to a database group (see groups_claim_ids in §3). In Auth0 a post-login Action can stamp it:

exports.onExecutePostLogin = async (event, api) => {
  api.idToken.setCustomClaim("https://example.com/groups", event.authorization?.roles ?? []);
  api.accessToken.setCustomClaim("https://example.com/groups", event.authorization?.roles ?? []);
};

Important: post-login Actions run for interactive logins only. The client-credentials (M2M) flow has no user and runs no post-login Action, so M2M tokens carry no group claim. Give M2M access via the integration's default_group instead (§3) — do not rely on a claim for machine tokens.

2d. Mint M2M tokens from the custom domain

So the token's iss is https://auth.example.com/ (matching your integration), always request from the custom domain token endpoint:

curl -s https://auth.example.com/oauth/token \
  -H 'content-type: application/json' \
  -d '{"grant_type":"client_credentials",
       "client_id":"<M2M_CLIENT_ID>",
       "client_secret":"<M2M_CLIENT_SECRET>",
       "audience":"https://db.example.com"}'
# -> { "access_token": "<JWT>", "expires_in": 86400, ... }

The JWT will have iss=https://auth.example.com/, aud=https://db.example.com, and azp=<M2M_CLIENT_ID>. Those three facts are what Ocient checks.


3. Part B — Ocient: groups, grants, and the two integrations

Connect as an admin to the system database (password handshake, not SSO):

pyocient "ocient://admin@system:<password>@10.0.0.20:4050/system?tls=unverified"

3a. Groups + cross-database grants

SSO users are placed in a group; the group carries the privileges. Groups are qualified to the integration's database (system here), and you grant them privileges on the user databases (sandbox, …) with cross-database grants.

CREATE GROUP readonly;            -- read-only (the default for SSO users)
CREATE GROUP readwrite;           -- elevated, optional

-- Cross-database grants: a system group gets privileges on another database.
GRANT USE    ON DATABASE "sandbox" TO GROUP "readonly";
GRANT SELECT ON DATABASE "sandbox" TO GROUP "readonly";
-- VIEW is what makes objects visible in the catalog (SHOW TABLES, schema
-- browsing). Without it a read-only user can SELECT but sees no tables.
GRANT VIEW TABLE  ON DATABASE "sandbox" TO GROUP "readonly";
GRANT VIEW SCHEMA ON DATABASE "sandbox" TO GROUP "readonly";
GRANT VIEW VIEW   ON DATABASE "sandbox" TO GROUP "readonly";

GRANT USE, SELECT, CREATE TABLE, CREATE VIEW, CREATE SCHEMA
  ON DATABASE "sandbox" TO GROUP "readwrite";

3b. The interactive integration (device-grant / auth-code)

CREATE SSO INTEGRATION sso_interactive PROTOCOL oidc
  issuer        = "https://auth.example.com",          -- NO trailing slash
  client_id     = "<INTERACTIVE_CLIENT_ID>",
  client_secret = "<INTERACTIVE_CLIENT_SECRET>",
  default_group = "readonly",                          -- everyone who logs in
  additional_scopes    = ["email", "profile", "offline_access"],
  additional_audiences = ["https://auth.example.com/"],-- the userinfo audience
  groups_claim_ids      = ["https://example.com/groups"],
  groups_claim_mappings = { "readwrite-role" = "readwrite" };

3c. The machine-to-machine integration

A second integration whose client_id is the M2M app (because azp must match — §0):

CREATE SSO INTEGRATION sso_m2m PROTOCOL oidc
  issuer        = "https://auth.example.com",
  client_id     = "<M2M_CLIENT_ID>",
  client_secret = "<M2M_CLIENT_SECRET>",
  default_group = "readonly",
  -- Required for token-only (no-browser) authentication:
  enable_id_token_authentication = true,
  -- Accept the M2M token's audience (the API identifier from §2b):
  additional_audiences = ["https://db.example.com", "https://auth.example.com/"],
  groups_claim_ids      = ["https://example.com/groups"],
  groups_claim_mappings = { "readwrite-role" = "readwrite" };

3d. Make the interactive integration the database default

SET designates the primary integration for a database. Make the interactive one primary on system so ordinary resolution (and device-grant) works everywhere by default. The M2M one stays a non-primary integration on system, selected explicitly at connect time (§6).

ALTER DATABASE system SET SSO INTEGRATION sso_interactive;

Both integrations now exist on system; sso_interactive is primary. Do not make sso_m2m the primary — doing so makes every default login (including the web UI and device-grant) try to authenticate with the confidential M2M app, which has no device_code grant, and interactive login breaks.

3e. To change an integration later

Use the top-level statement (a common trap):

ALTER SSO INTEGRATION sso_m2m SET additional_audiences = ["https://db.example.com"];

ALTER DATABASE … ALTER SSO INTEGRATION is only for assigning an integration to a database; using it to set properties fails with "syntax error … missing 'security' at 'sso'."


4. Part C — The SSO-only connectivity pool

A connectivity pool publishes the address clients connect to and can require SSO, so the public endpoint refuses password logins.

CREATE CONNECTIVITY_POOL IF NOT EXISTS sso_pool
  SOURCE_ADDRESS '0.0.0.0/0' PRIORITY 10
  SSO INTEGRATION sso_interactive REQUIRE_SSO true
  PARTICIPANTS ((NODE sql0
    LISTEN_ADDRESS '10.0.0.20' LISTEN_PORT 4051
    ADVERTISED_ADDRESS 'db.example.com' ADVERTISED_PORT 4050
    OPENAPI_PORT 443 OPENAPI_ADVERTISED_ADDRESS 'db.example.com' OPENAPI_ADVERTISED_PORT 443));

Notes and gotchas:


5. Part D — Multiple databases (inherit from system)

This is the payoff of putting integrations on system:

If you ever assign an integration directly to sandbox, it stops inheriting from system and uses only its own integrations. Usually you don't want that.

Apply everything: restart rolehostd on the SQL Node(s) so the integrations and pool take effect.


6. Part E — Connecting

6a. Interactive (device-grant — no local browser needed)

The driver prints a URL and a code; the user approves in any browser.

# pyocient
pyocient "ocient://db.example.com/sandbox?handshake=SSO&ssoOAuthFlow=deviceGrant"
# JDBC CLI
connect to jdbc:ocient://db.example.com:4050/sandbox;handshake=SSO;ssoOAuthFlow=deviceGrant;user=;password=;

Leave user/password empty — handshake=SSO drives it. Omit ssoOAuthFlow to let the driver auto-pick auth-code (browser present) vs device-grant (not).

6b. Machine-to-machine (pre-obtained token)

Mint a token (§2d), then present it via Ocient's SSO Token Flow. Two things must be set together, and this is the subtle part:

# pyocient
pyocient "ocient://id_token@system:<JWT>@db.example.com:4050/sandbox?handshake=SSO&identityprovider=sso_m2m&tls=on"
# JDBC CLI
connect to jdbc:ocient://db.example.com:4050/sandbox;handshake=SSO;identityprovider=sso_m2m;user=id_token@system;password=<JWT>;

Why both are required: the token flow resolves the integration from the connected database's primary. A plain sandbox connection falls back to the system primary (sso_interactive) without a name lookup, so identityprovider alone is silently ignored there. The @system qualifier is what makes the node look in the system scope by name, where identityprovider can then pick sso_m2m. (identityprovider is honored only when the named integration is in the resolved scope.)

Because the M2M token maps to default_group = readonly, it can only SELECT; writes are denied. There is no user, so audit logs identify it by iss::sub.


7. Troubleshooting — symptom → cause

Symptom (driver or log) Cause / fix
Browser: Could not resolve host: auth.example.com The SQL Node (not the browser) can't reach the IdP. Fix node egress/DNS.
404 on …//.well-known/openid-configuration Trailing slash on issuer. Remove it.
Application Type must be native when device_code is enabled (Auth0) The interactive app isn't Native. Device-grant needs a public/native app.
This connectivity pool requires SSO authentication You connected without handshake=SSO (driver fell back to password).
The OpenID provider does not support the request … introspection_endpoint You used user=access_token; Auth0 has no introspection endpoint. Use user=id_token with enable_id_token_authentication=true.
authorized party (azp) that does not match the configured client_id The token was minted by an app whose client_id ≠ the integration's. Use the integration whose client_id matches the token's app (e.g. sso_m2m for M2M tokens).
The token was issued for an alternate audience Same root cause family: wrong integration selected, or the token's aud isn't in additional_audiences. Verify identityprovider/@database and the audience list.
No SSO integration found for database X, falling back to system… (log) + M2M fails The M2M connection didn't reach the integration's scope. Add user=id_token@system and identityprovider=sso_m2m.
syntax error … missing 'security' at 'sso' You used ALTER DATABASE … ALTER SSO INTEGRATION … to set properties. Use top-level ALTER SSO INTEGRATION <name> SET ….
Read-only user sees no tables in SHOW TABLES Missing `GRANT VIEW {TABLE
Changes "don't take" Restart rolehostd. SSO/pool changes commit on restart.

8. What generalizes beyond Auth0

These are not Auth0-specific — expect them with any OIDC provider:

Provider-specific pieces to re-map for another IdP: