SSO, OIDC and browser sign-in
Hardcharger™ accepts ID tokens from an OpenID Connect provider (Okta, Entra ID, Google, Auth0, Keycloak, …). A client exchanges the provider’s token for a Hardcharger token, and the user is whoever the token names. Nobody’s password reaches Hardcharger.
How it fits together
Section titled “How it fits together”sequenceDiagram
participant U as User / client
participant I as Identity provider
participant H as Hardcharger
U->>I: sign in (browser, device code, workload identity, ...)
I-->>U: ID token (JWT, signed by the provider)
U->>H: POST /warp/v1/auth/token {grant_type: token_exchange, subject_token}
H->>I: (once, cached) OpenID configuration and JWKS
H-->>U: Hardcharger access token
Hardcharger checks the token’s signature (RS256/384/512 or ES256/384/512, by the key its kid
names), its issuer (iss) and audience (aud), and its expiry. The user name is the email
claim, or sub without one.
Configure the server
Section titled “Configure the server”| Option | Meaning |
|---|---|
token_allowed_issuer |
The provider’s issuer URL. With an audience and no static key, the server reads <issuer>/.well-known/openid-configuration at startup to find the JWKS. |
token_allowed_audience |
The audience the tokens must carry: usually your OAuth client id. |
token_jwks_uri |
The JWKS URL, if you’d rather name it than discover it. |
token_signature_verify_cert_path |
Or a PEM public key or certificate, for a provider without a JWKS. |
token_default_role |
Without access control: the role (admin, user, readonly) for tokens that carry no role claim. |
token_authorized_emails |
Who may sign in: case-insensitive globs, such as *@example.com. Default *. |
token_groups_claim |
With access control: the claim listing the user’s groups (default groups). |
Try it with a stand-in provider
Section titled “Try it with a stand-in provider”To see the whole flow on your machine, this script plays an identity provider: it serves the
OpenID configuration and a JWKS on 127.0.0.1:8099, and prints ID tokens it signed.
"""A stand-in OpenID Connect provider for trying Hardcharger's SSO locally.
python3 idp.py serve # discovery and JWKS on http://127.0.0.1:8099 python3 idp.py token ann@example.com # print an ID token for that user"""import jsonimport pathlibimport sysimport timefrom http.server import BaseHTTPRequestHandler, HTTPServer
import jwtfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import rsa
ISSUER = "http://127.0.0.1:8099"KEY = pathlib.Path("idp-key.pem")if not KEY.exists(): KEY.write_bytes(rsa.generate_private_key(public_exponent=65537, key_size=2048).private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()))private_key = serialization.load_pem_private_key(data=KEY.read_bytes(), password=None)
class Provider(BaseHTTPRequestHandler): def do_GET(self): documents = { "/.well-known/openid-configuration": { "issuer": ISSUER, "jwks_uri": f"{ISSUER}/jwks.json", "authorization_endpoint": f"{ISSUER}/authorize", "token_endpoint": f"{ISSUER}/token"}, "/jwks.json": {"keys": [dict(json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(private_key.public_key())), kid="docs-key", use="sig", alg="RS256")]}, } body = json.dumps(documents.get(self.path, {})).encode() self.send_response(200 if self.path in documents else 404) self.send_header("Content-Type", "application/json") self.end_headers() self.wfile.write(body)
if sys.argv[1] == "serve": HTTPServer(("127.0.0.1", 8099), Provider).serve_forever()else: now = int(time.time()) claims = {"iss": ISSUER, "aud": "hardcharger", "sub": sys.argv[2], "email": sys.argv[2], "groups": sys.argv[3:], "iat": now, "exp": now + 3600} print(jwt.encode(payload=claims, key=private_key, algorithm="RS256", headers={"kid": "docs-key"}))It needs pip install pyjwt cryptography. Start it, then start Hardcharger trusting it:
python3 idp.py serveHARDCHARGER_ADMIN_PASSWORD='change-me' hardcharger-server --database my.duckdb --health-port 31338 \ --token-allowed-issuer http://127.0.0.1:8099 --token-allowed-audience hardcharger \ --token-default-role userSign in with an ID token
Section titled “Sign in with an ID token”The ID token is the password of the user token:
$ HARDCHARGER_PASSWORD="$(python3 idp.py token ann@example.com)" hardcharger --host localhost --tls=false \ --username token --csv --command "SELECT HARDCHARGER_USER() AS who, HARDCHARGER_ROLE() AS role"who,roleann@example.com,userimport subprocess
from adbc_driver_hardcharger import dbapi as hardcharger
id_token = subprocess.run(args=["python3", "idp.py", "token", "ann@example.com"], capture_output=True, text=True, check=True).stdout.strip()with hardcharger.connect(uri="hardcharger://localhost?tls=false", username="token", password=id_token) as conn: print(conn.current_user())ann@example.comtoken_exchange swaps the ID token for a Hardcharger token:
$ ID_TOKEN=$(python3 idp.py token ann@example.com)$ curl -s localhost:31337/warp/v1/auth/token -H 'Content-Type: application/json' \ -d "{\"grant_type\": \"token_exchange\", \"subject_token\": \"$ID_TOKEN\"}" | jq -r .token_typeBearerThe Warp SQL ADBC driver takes the ID token as adbc.warp.auth.subject_token, and JDBC as
subjectToken.
Limit who may sign in
Section titled “Limit who may sign in”python3 idp.py serve & HARDCHARGER_ADMIN_PASSWORD='change-me' hardcharger-server --database my.duckdb \ --health-port 31338 --token-allowed-issuer http://127.0.0.1:8099 --token-allowed-audience hardcharger \ --token-default-role user --token-authorized-emails '*@example.com'$ ID_TOKEN=$(python3 idp.py token eve@elsewhere.org)$ curl -s localhost:31337/warp/v1/auth/token -H 'Content-Type: application/json' \ -d "{\"grant_type\": \"token_exchange\", \"subject_token\": \"$ID_TOKEN\"}" | jq -r .detailUser 'eve@elsewhere.org' is not authorized. Contact your administrator.The CLI shows the same reason, and exits 1:
$ HARDCHARGER_PASSWORD="$(python3 idp.py token eve@elsewhere.org)" hardcharger --host localhost --tls=false \ --username token --command "SELECT 1"Error: Authentication failed for http://localhost:31337: invalid_credentials (User 'eve@elsewhere.org' is not authorized. Contact your administrator.)With access control: users and group mappings
Section titled “With access control: users and group mappings”Under access control, an identity-provider user must exist in the store (it can have no password: it signs in only through the provider), and its grants decide what it may do. Roles can also come from the provider’s groups. Start the server with the store and the provider (the docs’ tests run exactly this, with their own PostgreSQL):
HARDCHARGER_ADMIN_PASSWORD='change-me' \HARDCHARGER_RBAC_STORE_URI='postgresql://hardcharger:secret@localhost:5432/hardcharger' \ hardcharger-server --database my.duckdb --token-allowed-issuer http://127.0.0.1:8099 \ --token-allowed-audience hardcharger --token-default-role userThen, as the administrator:
CREATE ROLE analyst;GRANT SELECT ON SCHEMA main TO analyst;CREATE USER "ann@example.com";CREATE GROUP MAPPING 'okta-analysts' ISSUER 'http://127.0.0.1:8099' TO ROLE analyst;CREATE TABLE sales AS SELECT 42 AS amount;$ HARDCHARGER_PASSWORD="$(python3 idp.py token ann@example.com okta-analysts)" hardcharger --host localhost --tls=false \ --username token --csv --command "SELECT amount FROM sales"amount42Ann’s token lists the group okta-analysts, which maps to analyst. A group never creates a
user: a token naming a user the store doesn’t have is refused. SHOW GROUP MAPPINGS lists the
mappings; DROP GROUP MAPPING 'okta-analysts' ISSUER '…' removes one.
Browser sign-in
Section titled “Browser sign-in”For people at a desktop (the CLI, a notebook, a driver in a desktop tool) and for the
web UI, Hardcharger runs the OAuth authorization-code flow with your provider
itself, as Warp SQL’s browser sign-in (capability oauth_browser). It is part of the protocol, on
the server’s own port: there is no separate sign-in port.
-
Register Hardcharger with your provider as a confidential web client and register the redirect URI
https://<your server>/warp/v1/auth/oauth/callback: the server’s public URL (oauth_base_url) followed by/warp/v1/auth/oauth/callback, on the same port clients connect to. -
Start the server with the provider and the client:
Terminal window HARDCHARGER_OAUTH_CLIENT_SECRET='<client secret>' hardcharger-server \--token-allowed-issuer https://login.example.com --token-allowed-audience <client id> \--oauth-client-id <client id> --oauth-base-url https://db.example.com \--oauth-provider-name Okta
How a sign-in runs:
sequenceDiagram
participant C as Client
participant B as Browser
participant I as Identity provider
participant H as Hardcharger
C->>H: POST /warp/v1/auth/oauth/flows
H-->>C: flow_id, poll_secret, authorize_url
C->>B: open authorize_url
B->>I: sign in (state, PKCE, nonce)
I-->>B: redirect to /warp/v1/auth/oauth/callback?code&state
B->>H: callback
H->>I: code + PKCE verifier + client secret
I-->>H: ID token
C->>H: POST /warp/v1/auth/oauth/flows/{flow_id} {poll_secret}
H-->>C: Hardcharger access token (once)
The client never sees the provider’s settings or tokens: the server exchanges the code (a
confidential client, with PKCE), checks the ID token as it checks token_exchange sign-ins
(signature, issuer, audience, expiry, the nonce, token_authorized_emails, and under access
control the store user), and hands the client its own access token. Only the client that created
the flow holds the poll secret, so whoever sees the sign-in URL can’t collect the token. The
callback page runs no script; the web UI is sent back to its own page instead.
hardcharger --host db.example.com --auth-type externalThe CLI opens your browser (HARDCHARGER_BROWSER names another command) and waits.
from adbc_driver_hardcharger import dbapi as hardcharger
with hardcharger.connect(uri="hardcharger://db.example.com", auth_type="external") as conn: print(conn.current_user()) # the e-mail address you signed in withopen_browser=False prints the URL instead, for a remote shell; share_sign_in=True shares
one sign-in among a process’s connections.
The sign-in page shows Sign in with Okta (the
oauth_provider_name; “single sign-on” without
one) beside the user name and password form.
Other Warp SQL drivers call it oauth_browser (JDBC authType=oauth_browser, which also takes
external).
With the stand-in provider, the server’s side of the flow can be checked (the stand-in has no login page, so the browser part stops there):
python3 idp.py serve & HARDCHARGER_ADMIN_PASSWORD='change-me' HARDCHARGER_OAUTH_CLIENT_SECRET='not-a-real-secret' \ hardcharger-server --database my.duckdb --health-port 31338 --token-allowed-issuer http://127.0.0.1:8099 \ --token-allowed-audience hardcharger --oauth-client-id hardcharger --oauth-provider-name 'Stand-in IdP'$ curl -s localhost:31337/warp | jq -c .auth.oauth_browser{"provider":"Stand-in IdP"}$ curl -s -X POST localhost:31337/warp/v1/auth/oauth/flows | jq -r '.authorize_url | split("?")[0]'http://127.0.0.1:8099/authorize| Option | Meaning |
|---|---|
oauth_client_id, oauth_client_secret |
The client registered with the provider. Setting the id turns browser sign-in on. Prefer HARDCHARGER_OAUTH_CLIENT_SECRET to the flag. |
oauth_base_url |
The server’s public URL, as browsers reach it (default http(s)://localhost:<port>). The redirect URI is this followed by /warp/v1/auth/oauth/callback. |
oauth_redirect_uri |
The redirect URI registered with the provider, given whole (overrides the one from oauth_base_url). |
oauth_scopes |
Scopes requested (default openid profile email). |
oauth_provider_name |
The provider’s name for people: “Sign in with name” in the web UI, and in the protocol’s discovery document. |
oauth_max_flows |
Sign-ins in progress at once (default 10000); beyond it new ones are refused until some finish or expire. |
oauth_instance_id |
A prefix for the OAuth state, when several servers share one callback proxy that routes the provider’s redirects by state. Set oauth_base_url too, so the web UI is sent back to this server. |
A sign-in expires after 10 minutes. Only open sign-in links your own client opened: a sign-in someone else started and sent you gives them your token.