Authentication
Every Glow request needs a caller identity. Authentication is a single scheme — a bearer token attached as an HTTP header:
| Scheme | Header | Issued by | Resolves to |
|---|---|---|---|
| BearerAuth | Authorization: Bearer <jwt> | Keycloak (interactive) or service-account exchange | profile + session |
The CLI handles this automatically: glow login writes the bearer to
~/.config/glow/tokens.json, and every subsequent call attaches the
Authorization header for you.
The bearer JWT is verified and resolved to a profile + session before the route handler runs.
Interactive flow (glow login)
glow login --instance-url https://my-school.example.comWhat happens:
- The CLI hits
GET /.well-known/openid-configurationon the instance to discover Keycloak endpoints. - It spawns a localhost listener and opens your browser to the
authorize_endpointwith a generatedstate+code_verifier(PKCE — no client secret needed for the public CLI client). - Keycloak logs you in (or returns to your existing SSO session) and
redirects to
http://127.0.0.1:<port>/callback?code=…&state=…. - The CLI exchanges the code at
token_endpointfor an access token- refresh token and writes both to
~/.config/glow/tokens.json, keyed by instance URL.
- refresh token and writes both to
After login, every subsequent CLI call attaches Authorization: Bearer <access_token> automatically. The refresh token is used silently when
the access token expires.
Service-account flow (--token)
For CI / scripted use, mint a service-account JWT once and pass it explicitly:
glow login --instance-url https://my-school.example.com \
--token "$SERVICE_ACCOUNT_JWT"This skips the browser dance and stores the token directly. The JWT
must be signed by the same Keycloak realm and carry a
profile.is_service_account = true claim.
Server-side logout
glow logoutFires GET /logout on the instance (which writes a logouts_entry
row so the next request mints a fresh session) and then clears the
local tokens.json. The server-side call is best-effort — the local
clear always runs even if the server is unreachable.
Raw HTTP
The curl examples throughout these docs read two environment variables — your instance URL and your bearer token — so you can export them once and every example is copy-paste-ready:
export GLOW_INSTANCE_URL=https://your-school.example.edu
export GLOW_TOKEN=$(cat ~/.config/glow/tokens.json | jq -r '.[].access_token' | head -1)
# …or paste a token directly: export GLOW_TOKEN=eyJhbG...GLOW_INSTANCE_URL is the same variable the CLI reads; GLOW_TOKEN is the
JWT glow login already stored under ~/.config/glow/. With both set, the
canonical request shape is:
curl -X POST $GLOW_INSTANCE_URL/persona/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $GLOW_TOKEN" \
-d '{}'That single Authorization: Bearer header authenticates every
endpoint — read and mutation paths alike.
Discovery endpoints
| Endpoint | Purpose |
|---|---|
GET /.well-known/openid-configuration | OIDC discovery doc (issuer, endpoints) |
GET /jwks | public signing keys for verifying issued JWTs |
GET /authorize | OAuth authorization endpoint (browser redirect) |
GET /oidc_callback | OAuth callback (used by browser flows) |
POST /token | code → access token / refresh exchange |
GET /userinfo | introspect the bearer’s profile claims |
GET /login / GET /logout | bare browser-flow entry points |
These mirror the standard OIDC surface — most clients only need
/.well-known/... to bootstrap.
Multiple instances
glow login --instance-url https://school-a.example.com
glow login --instance-url https://school-b.example.com
# tokens.json now has both; the --instance-url flag (or GLOW_INSTANCE_URL
# env var) selects which one a given command uses.
GLOW_INSTANCE_URL=https://school-b.example.com glow personas searchSessions are listed in the CLI via the instance config under
~/.config/glow/. There’s no first-class glow sessions list command
today — read the file directly if needed.
Service accounts
Service accounts are the non-interactive flavour of bearer auth: a
long-lived JWT that a CI pipeline, cron job, or programmatic client
attaches without ever opening a browser. Same Authorization: Bearer <jwt> shape as a human session — only the issuance path differs.
The recap (covered in Service-account flow above):
glow login --instance-url https://my-school.example.com \
--token "$SERVICE_ACCOUNT_JWT"The CLI’s --token branch (save_token in
src/auth.rs)
writes the JWT straight into ~/.config/glow/tokens.json with no
refresh_token and no expires_in — the server enforces lifetime via
the JWT’s own exp claim.
Minting a service-account JWT
Today this is an operator-side step, done directly against Keycloak:
- In the realm’s Keycloak admin console, create (or pick) a user that
represents the service identity (e.g.
ci-runner@yourdomain). - Add a
profile.is_service_account = trueclaim via a user attribute- a protocol mapper, so it lands in the issued token. Server-side
resolution in
resolve_identity.pyaccepts the JWT the same way as a human bearer; the claim is what downstream policy uses to distinguish “machine” from “person” when it matters (e.g. skipping interactive consent prompts).
- a protocol mapper, so it lands in the issued token. Server-side
resolution in
- Mint a token for that user — admin REST API
(
POST /auth/realms/<realm>/protocol/openid-connect/token) with the service account’s credentials, or via the admin console’s “Get access token” helper.
Honest gap: there is no first-class CLI helper for minting
service-account JWTs today (no glow service-accounts create). The
operator does it via Keycloak admin or a custom script. The CLI’s job
ends at consuming an already-minted token via --token.
Scope, rotation, storage
-
TTL. Driven by the Keycloak client / user token lifespan setting. Default Keycloak access-token lifespan is 5 minutes; service-account tokens are typically configured for hours-to-days so the CI run completes without a refresh dance.
-
Rotation. Re-mint in Keycloak, update the CI secret, re-run
glow login --token "$NEW_JWT". No refresh token is stored, so expiry is hard-stop — plan rotation cadence to match TTL. -
Storage in CI. Treat the JWT as a bearer credential. Standard pattern:
# GitHub Actions env: SERVICE_ACCOUNT_JWT: ${{ secrets.GLOW_SERVICE_ACCOUNT_JWT }} run: | glow login --instance-url "$GLOW_INSTANCE_URL" \ --token "$SERVICE_ACCOUNT_JWT" glow personas searchNever check the token into the repo; never echo it in logs.
Related
- Start — install + first-boot
- CLI Reference: login · logout
- API Reference:
POST /profile/contextreturns the authenticated profile - Permissions — roles, role_artifacts, and per-route policy