Keycloak operations and sign-in flows
This guide consolidates OakGate's working Google, email and NFC sign-in setup. It describes the implementation and the Beelink deployment, including the fixes made during commissioning. Use it alongside the detailed NFC deployment guide and identity and backup design.
What Keycloak does
Keycloak authenticates people and issues OpenID Connect (OIDC) tokens. OakGate validates those tokens and creates its own application session. Keycloak does not store an Acorn's recovery phrase or operate the wallet. OakGate holds the custody material, and user private-key operations go through safebox-acorn.
There is one Keycloak service, with two relevant realms:
- master: administration of Keycloak itself. Keep administrative access separate from ordinary OakGate user access.
- oakgate: application users, Google identity links, email configuration, registration and recovery, clients and authentication flows.
A realm is an identity/configuration boundary. A client is an application registration within it. An identity provider supplies an upstream login, such as Google. An authentication flow defines the steps Keycloak performs for a login.
| Component | Responsibility |
|---|---|
| Keycloak | Users, passwords, verification/recovery emails, Google links and OIDC authentication |
| Optional upstream authentication; its own account recovery | |
| Brevo | SMTP delivery of emails generated by Keycloak |
| OakGate | Custody, preferences, card enrollment and application sessions |
| Redis | Temporary OakGate sessions and short-lived authentication transactions |
| OakGate SQLite | Durable custody, payment recovery and card configuration |
| POS | Separate restricted terminal; its activation credential is not a Keycloak user login |
Google is replaceable, provided an alternative login reaches the same existing
Keycloak user. OakGate owns wallets by validated (issuer, subject), not email,
handle, Google address or social npub. Deleting and recreating a user with the
same email produces a different identity and does not recover their Acorn.
Do not change the issuer or subject mapping casually.
Which flow is used?
flowchart TD
A[OakGate sign-in] --> B[Google or email]
A --> C[NFC card]
B --> D[oakgate-web: normal realm browser flow]
C --> E[oakgate-nfc: dedicated NFC flow]
D --> F[Existing Keycloak user in oakgate realm]
E --> F
F --> G[OIDC callback: validate tokens and identity]
G --> H[OakGate Redis session]
H --> I[Same user's Acorn]
| User action | Client and behavior |
|---|---|
| Google sign-in | oakgate-web; Keycloak brokers Google authentication |
| Email/password sign-in | oakgate-web; Keycloak authenticates the local realm user |
| Registration and verification | Normal Keycloak registration and Verify Email steps |
| Forgotten password | Keycloak's normal reset-credentials flow and email delivery |
| Recovery disclosure, card-login activation/unlock or PIN-policy change | Fresh normal oakgate-web authentication, bound to the existing OakGate session and same identity |
| NFC login | oakgate-nfc; dedicated authenticator verifies an enrolled card through OakGate, then selects an existing realm user |
Fresh authentication requests prompt=login and max_age=0. Google still controls
its upstream session policy, so this does not promise a Google password prompt
on every occasion. NFC must never become the only route to account recovery.
Addresses and clients
Admin console: https://keycloak.safebox.dev/admin/master/console/. After entering, select oakgate before configuring application email, users, clients or flows. Configuring SMTP in master alone does not configure oakgate.
| Setting | Value |
|---|---|
| Public Keycloak hostname | https://keycloak.safebox.dev |
| OIDC issuer | https://keycloak.safebox.dev/realms/oakgate |
| OakGate root/home URL | https://oakgate.safebox.dev |
| Exact valid redirect URI | https://oakgate.safebox.dev/auth/callback |
| Valid post-logout redirect URI | https://oakgate.safebox.dev/ |
| Normal client | oakgate-web |
| NFC client | oakgate-nfc |
Both clients use OpenID Connect, Client authentication ON, Standard flow ON,
PKCE S256, and the ordinary openid profile email scopes. Disable implicit
flow, direct access grants and service accounts. Keep the standard subject
mapping and RS256 signing. Disable deprecated Full scope allowed; OakGate
needs no administrator roles. Never use wildcard redirect URIs.
Client secrets are under Clients → client → Credentials. If Credentials is
missing, first enable Client authentication and save. Each client has its own
secret. For Web origins, an exact https://oakgate.safebox.dev is sufficient;
the server-side code exchange does not require browser token-endpoint CORS.
Development callback entries, if needed on the normal client, must also be exact,
for example http://127.0.0.1:8801/auth/callback.
Google login
In the Google Cloud oakgate project, configure the consent screen and a Web
application OAuth client. Add the redirect URI shown by Keycloak's Google identity
provider. With the alias google, it is:
https://keycloak.safebox.dev/realms/oakgate/broker/google/endpoint
This is the Google-to-Keycloak callback, not OakGate's /auth/callback.
Authorized JavaScript origins are separate: they contain an origin only, with no
path. This server-side broker flow does not need a blank origin entry; remove
empty entries rather than submitting them.
In oakgate → Identity providers → Google, enter the Google client ID and
secret, enable the provider, and save. The downloaded Google JSON is a credential:
keep it out of Git. Its secret belongs in Keycloak, not in
OAKGATE_OIDC_CLIENT_SECRET. Review first-login/account-linking behavior before
changing it; an email match alone must not silently transfer custody ownership.
Email registration and password recovery
Brevo delivers mail; Keycloak creates verification and reset links and enforces
those flows. Sending as noreply@safebox.dev does not create an inbox. Test messages
arrive at the recipient's existing mailbox, such as Gmail.
- In Brevo, authenticate
safebox.devand verify the sender. Copy its exact DNS records into the DNS provider: root verification TXT, DKIM and DMARC. At Rebel,@represents the root. Keep unrelated existing TXT records. - Wait until domain authentication and sender checks are green. Use the values displayed by Brevo, rather than inventing DKIM targets or replacing an existing DMARC policy blindly.
- In oakgate → Realm settings → Email, configure the From address/display name
and SMTP server
smtp-relay.brevo.com, authentication and the SMTP login/key supplied by Brevo. Use a matching encrypted port/mode, such as port 465 with SSL, or 587 with StartTLS. Do not enable both modes indiscriminately. An SMTP key is distinct from a Brevo API key. - Use Test connection, with a valid recipient email on the administrator account performing the test. Check that mailbox. A successful test was received during commissioning.
- In Realm settings → Login, enable User registration, Forgot password and Verify email. Review email-as-username/login-with-email settings to match the intended account policy. Retain the standard registration/reset flows.
- Test a new account in a private browser, complete verification, then separately test Forgot password and sign back in. Registration plus verification was tested successfully in Safari; card scanning has different browser requirements.
The Keycloak administration guide and Brevo SMTP guide are the upstream references. Email settings are per realm; master can have its own SMTP configuration for administrator recovery.
NFC login: separate client, same account
The custom provider is packaged into the existing Keycloak image using
compose.nfc.yaml. It adds no second Keycloak instance, realm or database.
In Authentication → Flows, create a basic oakgate-nfc flow with exactly one
execution: OakGate NFC card and PIN, set to REQUIRED. Under
Clients → oakgate-nfc → Advanced → Authentication flow overrides (near the
bottom), select that flow for Browser flow. Leave the realm browser binding and
oakgate-web unchanged. Do not add Cookie, password or Google alternatives inside
the NFC flow: normal sign-in remains a separate choice.
The holder opts in during card issuance, saves the generated four-digit PIN, and completes card write/readback. Enrollment is still pending until the holder uses Preferences → Your NFC cards → Verify sign-in and save and completes fresh normal sign-in. Existing cards without the required PIN/enrollment need reissuance. The PIN verifier is stored server-side; the PIN is not written onto the card or given to the POS.
At login, Web NFC reads the card in a compatible HTTPS browser. Keycloak sends it, the PIN and a short-lived challenge to OakGate's authenticated internal verification endpoint. OakGate checks card validity, issuer identity, enrollment, wallet binding, revocation, expiry and PIN policy. Keycloak accepts only the returned existing, enabled user in its realm. The subsequent OIDC callback creates the ordinary OakGate session; the card never creates a session directly.
PIN checking defaults on. A holder can explicitly select card-only login after
acknowledging the risk and completing fresh normal authentication. That exception
is server-side and per card, not a global .env bypass. Five wrong PIN attempts
lock card login; successful logins do not reset that counter. Fresh normal
reauthentication can unlock it; a forgotten PIN requires reissuance.
Web NFC is available on supported Android/Chrome devices, not Safari/iPhone. Card plus PIN is not equivalent to a FIDO2 security key: a copied card and known PIN can authenticate. With PIN disabled, the copied credential alone is enough. Withdrawing enrollment stops new logins; revoking the card also stops its other capabilities. Neither action terminates sessions already issued.
See the NFC guide for deployment, rollback and failure handling, and card/payment design for the separate payment permissions. Login enrollment does not expand POS permissions.
Configuration and secrets: where each belongs
| Location | Configuration |
|---|---|
| Tracked Compose files | Services, health checks, required volume variables and optional NFC image override |
Private deployment .env |
Host paths, public URLs, feature flags and application/server secrets |
| Keycloak persistent database | Users, passwords, Google links, clients/secrets, SMTP settings and flows |
| Google/Brevo consoles | OAuth consent/client settings, sender/domain configuration and SMTP credentials |
| OakGate database | Card enrollment/PIN policy, custody and preferences |
Relevant .env values for Beelink:
KEYCLOAK_DATA_DIR=/mnt/lexar/keycloak
OAKGATE_DATA_DIR=/mnt/lexar/oakgate
KC_HOSTNAME=https://keycloak.safebox.dev
KEYCLOAK_BIND_ADDRESS=0.0.0.0
KEYCLOAK_PORT=8800
OAKGATE_PUBLIC_URL=https://oakgate.safebox.dev
OAKGATE_OIDC_ISSUER=https://keycloak.safebox.dev/realms/oakgate
OAKGATE_OIDC_CLIENT_ID=oakgate-web
OAKGATE_OIDC_CLIENT_SECRET=<normal client's secret>
OAKGATE_SESSION_TTL_SECONDS=604800
COMPOSE_FILE=compose.yaml:compose.nfc.yaml
OAKGATE_NFC_LOGIN_ENABLED=true
OAKGATE_NFC_CLIENT_ID=oakgate-nfc
OAKGATE_NFC_CLIENT_SECRET=<NFC client's separate secret>
OAKGATE_NFC_BACKCHANNEL_SECRET=<output of openssl rand -hex 32>
These are selected settings, not a replacement for the complete existing .env.
Preserve custody, service identity, POS and other keys. Protect .env with mode
600 and keep it out of Git. Generate the backchannel secret once; it authenticates
Keycloak to OakGate and must match in both containers. It is never a user PIN or
browser credential. KC_BOOTSTRAP_ADMIN_PASSWORD bootstraps initial administration;
changing it is not a password-reset mechanism for an existing administrator.
COMPOSE_FILE is essential while NFC is enabled: future refreshes must keep the
custom image. An app-only refresh does not rebuild the Keycloak plugin. Keep
storage paths in .env, not local edits to tracked Compose files that may be
lost during stash/pull operations.
HTTPS and the internal connection
Nginx terminates public HTTPS and forwards to Keycloak's internal HTTP port 8800.
The Compose configuration sets KC_PROXY_HEADERS=xforwarded, a strict public
hostname, and HTTP enabled for this backend hop. In the HTTPS proxy location,
set each forwarded header once:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://100.70.55.66:8800;
That upstream is the Beelink VPN address used in this deployment; use loopback
only when Nginx runs on the same host. Restrict backend access to the trusted
proxy/network, especially with 0.0.0.0 binding. Missing HTTPS forwarding caused
the earlier non-secure-cookie warnings and authentication failures.
The NFC backchannel is a different hop:
http://app:8000/api/cards/v1/login/verify inside Compose. It uses the shared
secret and explicitly HTTP/1.1. Java's attempted cleartext HTTP/2 upgrade
caused an empty request body in the tested Uvicorn path, producing OakGate
JSONDecodeError and Keycloak phase=backchannel http_status=400. Forcing HTTP/1.1
on the Java client and request resolved the reproduced failure, followed by a
successful live login. This is not a reason to disable browser-facing HTTPS or
HTTP/2 on Nginx. Separate-host backchannels require HTTPS.
Sessions and recovery boundaries
OakGate's session cookie is an opaque reference to Redis state: HttpOnly, Secure on HTTPS and SameSite=Lax. The default lifetime is a fixed week, not sliding. OakGate does not retain refresh tokens. Keycloak SSO/client session settings are separate; adjust those in Keycloak deliberately rather than assuming the OakGate TTL changes them.
Redis restart logs users out but does not delete their accounts or wallets. App restart preserves sessions while Redis remains running. Authentication state is short-lived, browser-bound and consumed once. Recovery and card-policy changes require fresh authentication despite an existing application session.
Keycloak backchannel logout is not implemented for OakGate sessions. Disabling a user or withdrawing card login does not immediately invalidate an existing OakGate session. Account-compromise response must consider both session stores; a Redis restart is a broad logout of everyone, not a targeted recovery tool.
Updating and verifying the deployment
From the OakGate deployment directory, first check the mount and configuration:
mountpoint /mnt/lexar
docker compose config --quiet
Use ./refresh-app.sh for app updates; it waits for app health and leaves
Keycloak/Redis running. When the NFC provider changes, rebuild the existing
Keycloak service on Beelink:
docker compose up -d --build --no-deps keycloak
docker compose ps keycloak
docker compose logs --since=2m --tail=80 keycloak
Wait for healthy status before testing login. A plain restart does not apply
changed container environment. ./refresh-containers.sh refreshes the broader
deployment with a bounded readiness wait; retain the NFC Compose override.
Keycloak startup can take tens of seconds and briefly use substantial CPU.
Its readiness endpoint is internal port 9000, not a public management port.
After a change:
- [ ] Both application and Keycloak use their intended
/mnt/lexar/...directories. - [ ] App, Keycloak and Redis report healthy.
- [ ] Admin access and existing Google/email sign-in still work.
- [ ] Both methods reach the same expected wallet and preferences.
- [ ] Email verification/reset delivery works when those settings changed.
- [ ] NFC login works for an enrolled card on a compatible device.
- [ ] A non-enrolled/revoked card is refused; test PIN lockout only with a test card.
- [ ] Normal login remains available for card loss or unsupported browsers.
The current Compose command is start-dev with file-backed H2. The development
mode warning is accurate: HTTPS and successful tests do not turn this into a
production-hardened Keycloak deployment. Moving to production mode and a supported
production database requires the separate PostgreSQL migration checklist. Its opt-in override is prepared; the actual migration must be rehearsed and explicitly performed.
Troubleshooting quick reference
| Symptom | Check |
|---|---|
| No NFC execution under Add execution | NFC Compose override is loaded and custom Keycloak image was built/recreated |
| Credentials tab missing | Client authentication is enabled and saved |
| Authentication flow overrides hard to find | NFC client's Advanced tab, near the bottom |
| Non-secure-cookie warning | Public hostname and Nginx forwarded HTTPS/host/port headers |
| SMTP test works only for admin realm | Configure Email in oakgate as well as master |
NFC rejected, JSONDecodeError, backchannel 400 |
Updated HTTP/1.1 provider deployed; inspect phase logs, never credential bodies |
| Card login denied but normal login works | Enrollment, activation, card/PIN policy, lockout and user enabled status |
| Login works but wrong/new wallet identity | Check issuer and subject continuity; do not repair by matching email |
| App healthy but login unavailable | Health is not an end-to-end OIDC, Google or SMTP test |
Backups and continuity
Back up the active Keycloak data directory consistently, with Keycloak stopped for the current file-backed H2 deployment. Preserve its version and matching NFC provider source/image. A realm export alone is not the complete recovery plan.
Also preserve a consistent OakGate database backup, custody/service keys and private configuration. Card data includes encrypted credentials/identity bindings, PIN verifiers and policy; losing or rolling back that state affects authentication and revocation. The Keycloak backup must preserve user IDs and identity links. Never run an old restored payment database concurrently with the live instance.
Redis is deliberately ephemeral and need not be restored to recover wallets. Keep protected backups on a separate device: moving all active services to Lexar improves latency but does not itself create a backup. See the backup inventory and restore constraints.