Skip to content

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.


┌──────────────────────── 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 tokenBecomesWho uses it
scope contains audit:write (or audit:read)SCOPE_audit:write …service clients (client-credentials)
realm_access.roles contains ROLE_ADMINaudit:read + audit:write + audit:adminhuman admins
realm_access.roles contains ROLE_USERaudit:readhuman viewers
tenant = "demo"tenant-isolation guard (tenantId in the request must equal it)everyone

Endpoint authorization:

EndpointRequires
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.jsonpublic
Spring profileToken sourceNotes
localdevnone (bypassed)dev only — synthetic fully-scoped principal
stagingKeycloak only (RS256)classic SSO deployment
localauthGLog’s own username/password (HS512)no Keycloak dependency
dualauthboth — Keycloak RS256 and GLog HS512current 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.


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) and ROLE_USER (read). Assign them to accounts (or add ROLE_USER to Default roles so every self-registered user gets read).
  • tenant mapper: a Hardcoded claim tenant=demo on glog-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):

  1. 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 scope claim; “Include in token scope” is what actually puts the string audit:write into that claim.
  2. 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.
  3. Attach the scope — glog-ingest → Client scopes → Add audit:write as Default.
    • Why: Default scopes are always included in a client-credentials token (Optional scopes require an explicit scope= request, which services don’t send).
  4. 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.
  5. 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.


Section titled “4.1 Via Keycloak (service, client-credentials) — recommended for M2M”
Terminal window
# 1) get a service token
TOKEN=$(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 Created

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

Terminal window
# 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).


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

Env varMeaning
GNOTIFY_GLOG_ENABLEDtrue to report deliveries
GNOTIFY_GLOG_BASE_URLGLog base URL (server-internal: http://glog-frontend)
GNOTIFY_GLOG_TENANTtenant the events are written under (demo) — must match the token’s tenant
GNOTIFY_GLOG_AUTH_TOKEN_URIKeycloak token endpoint (defaults to the interdictii realm)
GNOTIFY_GLOG_AUTH_CLIENT_IDglog-ingest
GNOTIFY_GLOG_AUTH_CLIENT_SECRETthe client secret from §3.3 step 5

Note: the gnotify.glog.auth.* client-credentials support was added in commit 071bc68 — the deployed image must be rebuilt from that commit or later, otherwise isConfigured() is false and GNotify posts anonymously → 401 against an auth-enabled GLog.


SymptomCauseFix
ingest → 401 [no body]no/invalid token reached GLogcaller isn’t attaching a bearer (or the jar predates the auth feature)
ingest → 403token valid but missing authority or tenantensure audit:write scope (service) / ROLE_ADMIN (human), and tenant claim == tenantId
service token has scope: profile email onlyaudit:write not a Default client scope, or “Include in token scope” Offfix per §3.3 (1) & (3)
token endpoint → invalid_clientwrong client secret, or client auth Offre-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.