Skip to content

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.

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.

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).

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 json
import pathlib
import sys
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
import jwt
from cryptography.hazmat.primitives import serialization
from 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:

Terminal window
python3 idp.py serve
Terminal window
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

The ID token is the password of the user token:

Terminal window
$ 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,role
ann@example.com,user
Terminal window
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'
Terminal window
$ 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 .detail
User 'eve@elsewhere.org' is not authorized. Contact your administrator.

The CLI shows the same reason, and exits 1:

Terminal window
$ 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):

Terminal window
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 user

Then, 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;
Terminal window
$ HARDCHARGER_PASSWORD="$(python3 idp.py token ann@example.com okta-analysts)" hardcharger --host localhost --tls=false \
--username token --csv --command "SELECT amount FROM sales"
amount
42

Ann’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.

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.

  1. 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.

  2. 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.

Terminal window
hardcharger --host db.example.com --auth-type external

The CLI opens your browser (HARDCHARGER_BROWSER names another command) and waits.

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):

Terminal window
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'
Terminal window
$ 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.