Architecture & Token Flow
How authentication and data flow between Hrida AI Studio, hrida-mcpo, and HridaOne — so you can reason about security, debug issues, and safely extend the integration.
Component map
Browser
│ Keycloak OIDC login (SSO) or native login
▼
Hrida AI Studio (FastAPI :8080)
│ Chat: Digital Employee filter stores the user's KC token → Redis hrida:kc_token:{user_id}
│ Workflows: run is enqueued (with the KC token) → Redis Stream hrida:workflow_jobs
▼
Workflow Worker (hrida_workflow_worker.py)
│ Writes the run's KC token → Redis hrida:kc_token:{run_id}
▼
hrida-mcpo (MCP-to-OpenAPI bridge)
│ HR tool (scripts/hr-agent/tool.py) reads hrida:kc_token:{context_id} on each call
│ Falls back to the service account when no per-user token is found
▼
HridaOne (Spring Boot :8081)
│ KcJwtAuthenticationFilter validates the KC token
│ Resolves tenant, role, and employee from token claims
│ JIT-provisions the employee on first request
▼
Result: HridaOne → hrida-mcpo → Hrida AI Studio → browser
Two identities: service account and per-user token
Service account. hrida-mcpo signs in to HridaOne with a dedicated account (HRIDAONE_EMAIL + HRIDAONE_PASSWORD, or a pre-issued HRIDAONE_JWT_TOKEN). All HR calls run as that account; the agent narrows results to the asking user by email lookup. See HridaOne Service Account.
Per-user Keycloak token. With SSO and HRIDAONE_USE_PER_USER_TOKEN=true, each call carries the employee's own Keycloak token. This means:
- Two employees asking the same question get their own data (leave balance, payslips)
- HridaOne's audit log records the actual employee, not a service account
- HridaOne's RBAC enforces scope at the API level — a crafted prompt cannot reach another employee's data
The order the HR tool tries is: per-user token from Redis → HRIDAONE_JWT_TOKEN → email/password login. See Token resolution priority.
Token lifecycle
In chat
- The user signs in to Hrida AI Studio through Keycloak; Studio keeps the OAuth session.
- When the user messages the HR Agent, its Digital Employee filter takes the user's newest Keycloak access token from their OAuth sessions and writes it to
hrida:kc_token:{user_id}(TTL 600 s). - The filter injects
_context_id={user_id}into the system message; the model passes it on every tool call. - The HR tool reads
hrida:kc_token:{context_id}and sends it to HridaOne asAuthorization: Bearer.
In workflow runs
- The user starts a run (
POST /api/v1/agent-workflows/{id}/run). The token from the request'sAuthorizationheader is saved on the run record (triggered_by_kc_token). - The run is enqueued to the Redis Stream.
- The worker writes the token to
hrida:kc_token:{run_id}with a TTL ofWORKFLOW_MAX_TIMEOUT_SECONDS+ 60 s (660 s by default), so it outlives the run. - The agent passes the run ID to HR tools as
_context_id. - If the client disconnects mid-stream, the key is deleted immediately; otherwise it expires with its TTL.
Runs started by cron or webhook triggers have no user session, so no token — the service account is used.
Keycloak access tokens expire quickly (5 minutes by default). If a long run outlives the token, later HridaOne calls fail with 401. Raise the realm's Access Token Lifespan (Realm Settings → Tokens) if your HR workflows run longer.
Why Redis?
The MCP server runs as a separate process under hrida-mcpo, so environment variables set in Studio cannot reach it. Redis is the shared channel: Studio and the worker write the token, the tool reads it. Both must use the same Redis (REDIS_URL).
HridaOne auth filter
KcJwtAuthenticationFilter runs before HridaOne's native JWT filter. Both token types work on every endpoint:
| Token type | How identified | What happens |
|---|---|---|
| Native JWT (HS256) | HS256 algorithm header | Standard HridaOne login session |
| Keycloak JWT (RS256) | RS256 algorithm header | Signature checked against Keycloak's JWKS; iss must match KEYCLOAK_ISSUER_URI or KEYCLOAK_PUBLIC_ISSUER_URI; azp must be in KEYCLOAK_ALLOWED_CLIENT_IDS |
Claims read from a Keycloak token:
| Claim | Used for |
|---|---|
email | Finding the HridaOne user |
sub | Keycloak user ID |
name (falls back to preferred_username) | Display name for JIT provisioning |
hrida_tenant_code | Tenant (company) the user belongs to |
hrida_role | Role, when set |
hrida_employee_id | Employee code, when set |
When a Keycloak user reaches HridaOne for the first time, the filter creates an EMPLOYEE record under the tenant from hrida_tenant_code. Managers and admins must still be promoted in HridaOne.
Where data lives
- The Keycloak token lives in Redis with a short TTL and, for workflow runs, on the run record so a paused run can resume.
- The KC token is never logged by the workflow runtime.
- HridaOne responses (payslip amounts, leave counts) flow back into the chat or the run's output and follow Studio's normal data retention.
Network topology (Docker)
All services share the hridaai-network Docker bridge. Only the reverse proxy is exposed to the internet.
internet
│ HTTPS :443
▼
Nginx (reverse proxy + TLS termination)
├── / → hrida-ai-studio :8080
└── HridaOne host → hridaone :8081 (only if employees use HridaOne's own UI)
internal network (hridaai-network)
hrida-ai-studio :8080
hridaone :8081
keycloak :9090
redis :6379
hrida-postgres :5432
hrida-mcpo :8000 (container)
Security properties
| Property | How it is achieved |
|---|---|
| Per-employee data isolation | KC token carries the employee's identity; HridaOne enforces RBAC per user |
| Token not persisted long-term | Redis TTL; key deleted early if the client disconnects |
| Token signature verification | RS256 checked against Keycloak's JWKS endpoint |
| Accepted clients restricted | azp must be listed in KEYCLOAK_ALLOWED_CLIENT_IDS |
| Audit trail | HridaOne records the real employee as the actor |
| Prompt isolation | HridaOne only sees tool parameters, never the raw prompt |