GLog — Authentication & service integrations (Keycloak · JWT · GNotify)
How identity flows through the gStack audit pipeline: Keycloak ↔ GLog ↔ consuming services (GNotify), how to authenticate log ingestion two ways (Keycloak service client or GLog’s own JWT), and exactly what to create in Keycloak and why.
1. The big picture
Section titled “1. The big picture” ┌──────────────────────── Keycloak (realm: interdictii) ─────────────────────────┐ │ glog-web (public, PKCE) ROLE_ADMIN / ROLE_USER glog-ingest (confidential)│ │ → browser login → human permissions → service login (M2M) │ └───────▲───────────────────────────▲──────────────────────────────▲───────────────┘ │ RS256 (browser) │ RS256 (browser) │ RS256 (client-credentials) │ │ │ Human ───login──▶ GLog UI ────┘ │ GNotify ───────────┘ │ │ GLog own login (no Keycloak) ── HS512 ──▶ /api/auth/login ─┘ │ every delivery (SENT/FAILED) ▼ ┌───────────────────────── GLog (resource server, profile: dualauth) ──┐ │ accepts BOTH token types, routed by the `iss` claim: │ │ · Keycloak RS256 → validated vs realm JWKS │ │ · GLog HS512 → validated vs shared secret (iss=glog-localauth) │ │ → GlogJwtAuthConverter → audit:read|write|admin + `tenant` guard │ │ → POST /api/v1/app/audit-events (WORM hash-chained store) │ └───────────────────────────────────────────────────────────────────────┘One rule to remember: every write to GLog needs a token that carries (a) the
audit:write authority and (b) a tenant claim equal to the tenantId in the payload.
Everything below is just different ways to obtain such a token.
2. How GLog authorizes (the resource server)
Section titled “2. How GLog authorizes (the resource server)”GLog is a Spring OAuth2 resource server. It never issues tokens for Keycloak users — it
only validates them. Authorization is derived by GlogJwtAuthConverter from two claim
sources, so humans and services work through the same code:
| Claim in the token | Becomes | Who uses it |
|---|---|---|
scope contains audit:write (or audit:read) | SCOPE_audit:write … | service clients (client-credentials) |
realm_access.roles contains ROLE_ADMIN | audit:read + audit:write + audit:admin | human admins |
realm_access.roles contains ROLE_USER | audit:read | human viewers |
tenant = "demo" | tenant-isolation guard (tenantId in the request must equal it) | everyone |
Endpoint authorization:
| Endpoint | Requires |
|---|---|
POST /api/v1/app/audit-events (ingest) | audit:write |
GET /api/v1/app/audit-events (read/search) | audit:read |
/api/v1/admin/** (integrity, replay) | audit:admin |
/health/*, /metrics, /info, /api/v1/openapi.json | public |
Auth profiles (one per deployment)
Section titled “Auth profiles (one per deployment)”| Spring profile | Token source | Notes |
|---|---|---|
localdev | none (bypassed) | dev only — synthetic fully-scoped principal |
staging | Keycloak only (RS256) | classic SSO deployment |
localauth | GLog’s own username/password (HS512) | no Keycloak dependency |
dualauth | both — Keycloak RS256 and GLog HS512 | current live deployment |
Under dualauth, a JwtIssuerAuthenticationManagerResolver inspects the token’s iss
claim and routes it to the right validator: Keycloak issuer → JWKS decoder; glog-localauth
→ shared-secret HS512 decoder. Same authorization afterwards.
3. What to create in Keycloak, and why
Section titled “3. What to create in Keycloak, and why”Realm: interdictii (shared across gStack — GLog does not need its own realm).
3.1 glog-web — the browser login (humans)
Section titled “3.1 glog-web — the browser login (humans)”- Type: OpenID Connect, public, Standard flow On, PKCE = S256.
- Why: the GLog SPA is a browser app; it can’t hold a secret, so it uses the
Authorization-Code + PKCE flow. Redirect/web-origins point at
https://glog.gstack.esempla.systems. - Rule of thumb: browser → public client + PKCE; service → confidential client.
3.2 Realm roles + tenant (human permissions)
Section titled “3.2 Realm roles + tenant (human permissions)”- Roles:
ROLE_ADMIN(full) andROLE_USER(read). Assign them to accounts (or addROLE_USERto Default roles so every self-registered user gets read). tenantmapper: a Hardcoded claimtenant=demoonglog-web(dedicated scope), so human tokens carry the tenant GLog isolates on. Without it → 403.
3.3 glog-ingest — the service login (GNotify & other M2M callers)
Section titled “3.3 glog-ingest — the service login (GNotify & other M2M callers)”This is what lets a backend service write audit events without a human. Steps (the why in italics):
- Client scope
audit:write— Client scopes → Create client scope- Name
audit:write, Type Default, Include in token scope: On. - Why: GLog grants write from the
scopeclaim; “Include in token scope” is what actually puts the stringaudit:writeinto that claim.
- Name
- Client
glog-ingest— Clients → Create client → OpenID Connect- Client authentication: On (confidential), Service accounts roles: On, Standard flow & Direct access Off.
- Why: client-credentials is the machine-to-machine grant — no user, no browser. Confidential because the service can safely hold a secret.
- Attach the scope — glog-ingest → Client scopes → Add
audit:writeas Default.- Why: Default scopes are always included in a client-credentials token (Optional scopes
require an explicit
scope=request, which services don’t send).
- Why: Default scopes are always included in a client-credentials token (Optional scopes
require an explicit
- Tenant mapper — glog-ingest → dedicated scope → Hardcoded claim
tenant=demo(Add to access token: On).- Why: same tenant guard as humans — the service token must assert its tenant.
- Credentials tab → copy the Client secret — hand it to the consuming service.
A token minted by glog-ingest then carries scope: … audit:write + tenant: demo and is
accepted by GLog for ingestion.
4. Sending logs to GLog — the two ways
Section titled “4. Sending logs to GLog — the two ways”4.1 Via Keycloak (service, client-credentials) — recommended for M2M
Section titled “4.1 Via Keycloak (service, client-credentials) — recommended for M2M”# 1) get a service tokenTOKEN=$(curl -s -X POST \ https://sso.gstack.esempla.systems/realms/interdictii/protocol/openid-connect/token \ -d grant_type=client_credentials \ -d client_id=glog-ingest \ -d client_secret=<SECRET> | jq -r .access_token)
# 2) ingest (tenantId MUST equal the token's `tenant` claim)curl -X POST https://glog.gstack.esempla.systems/api/v1/app/audit-events \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"tenantId":"demo","eventTimestamp":"2026-07-27T09:01:05Z", "service":"gpay","objectType":"PAYMENT","actionType":"CREATE","status":"SUCCESS"}'# → 201 CreatedThe token lives ~5 min; cache it and refresh ~15 s before expiry (that’s what the SDKs and GNotify do).
4.2 Via GLog’s own JWT (username/password, no Keycloak)
Section titled “4.2 Via GLog’s own JWT (username/password, no Keycloak)”Under localauth/dualauth GLog issues its own HS512 tokens:
# login (bootstrap admin, or any account created via /api/auth/register)TOKEN=$(curl -s -X POST https://glog.gstack.esempla.systems/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"username":"admin","password":"qweASD123"}' | jq -r .accessToken)
curl -X POST https://glog.gstack.esempla.systems/api/v1/app/audit-events \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{ … }'ROLE_ADMIN accounts can write; ROLE_USER accounts are read-only. Same tenant
enforcement. Full round-trip is in docs/glog.postman_collection.json (folder Auth).
5. How GNotify reports to GLog
Section titled “5. How GNotify reports to GLog”GNotify audits every delivery attempt: when a channel worker finishes (SENT after the stub/ provider succeeds, or FAILED after the retry budget is exhausted), it fires an async audit event to GLog.
Flow for one notification:
POST /api/admin/notify (or mTLS /api/v1/notify) → NotificationDispatchService: persist Notificare + Livrare(QUEUED), publish to RabbitMQ → NotificationWorker (@RabbitListener): call the ChannelSender (email=SES, telegram=bot, push=FCM/stub) → on terminal SENT/FAILED: GlogAuditClient.logSent/logFailed ──▶ GLog ingest body: { tenantId, service:"gnotify", objectType:"NOTIFICATION", actionType:"DELIVER", actorType:"SERVICE", status:"SUCCESS|FAILURE", objectAffected/correlationId: <trackingId>, details:{channel,tier,systemId,…} }GlogAuditClient.bearer() obtains the token: if gnotify.glog.auth is configured it does the
client-credentials dance against glog-ingest (cached), otherwise a static
gnotify.glog.token, otherwise none. It is best-effort — a GLog failure never blocks the
actual notification delivery (it’s logged as a WARN).
GNotify configuration (env)
Section titled “GNotify configuration (env)”| Env var | Meaning |
|---|---|
GNOTIFY_GLOG_ENABLED | true to report deliveries |
GNOTIFY_GLOG_BASE_URL | GLog base URL (server-internal: http://glog-frontend) |
GNOTIFY_GLOG_TENANT | tenant the events are written under (demo) — must match the token’s tenant |
GNOTIFY_GLOG_AUTH_TOKEN_URI | Keycloak token endpoint (defaults to the interdictii realm) |
GNOTIFY_GLOG_AUTH_CLIENT_ID | glog-ingest |
GNOTIFY_GLOG_AUTH_CLIENT_SECRET | the client secret from §3.3 step 5 |
Note: the
gnotify.glog.auth.*client-credentials support was added in commit071bc68— the deployed image must be rebuilt from that commit or later, otherwiseisConfigured()is false and GNotify posts anonymously → 401 against an auth-enabled GLog.
6. Troubleshooting
Section titled “6. Troubleshooting”| Symptom | Cause | Fix |
|---|---|---|
ingest → 401 [no body] | no/invalid token reached GLog | caller isn’t attaching a bearer (or the jar predates the auth feature) |
| ingest → 403 | token valid but missing authority or tenant | ensure audit:write scope (service) / ROLE_ADMIN (human), and tenant claim == tenantId |
service token has scope: profile email only | audit:write not a Default client scope, or “Include in token scope” Off | fix per §3.3 (1) & (3) |
token endpoint → invalid_client | wrong client secret, or client auth Off | re-copy the secret; Client authentication On |
Sources: config/{SecurityConfig,DualAuthSecurityConfig,GlogJwtAuthConverter},
web/AuthResource, security/local/* (GLog); service/audit/GlogAudit{Client,Properties}
(GNotify). Last updated 2026-07-27.