Authentication and authorization
This page explains how the Navin Platform recognises an identity (user, API key, service), how it derives the tenant from it, then how it decides on an action: roles, scopes, clearance, OpenFGA relations, OPA policies and delegations. Source: services/control-plane/src/eai/auth.py, services/control-plane/src/eai/policy.py, services/control-plane/src/eai/integrations/openfga.py, services/control-plane/src/eai/domains/identity.
Version française : ../authentication-authorization.md
Accepted credentials
eai.auth.authenticate reads credentials in this order; the first one present decides, no other is tried afterwards.
| Order | Credential | Method (auth_method) | Resulting principal |
|---|---|---|---|
| 1 | Authorization: Bearer eai_<prefix>_<secret> | api_key | apikey:<id>, kind service |
| 2 | Authorization: Bearer eais_<prefix>_<secret> (portal session opened by POST /v1/auth/login) | password | local:<login>, kind user |
| 3 | Authorization: Bearer <OIDC JWT> | oidc | token sub, kind user |
| 4 | X-API-Key: eai_<prefix>_<secret> | api_key | as 1 |
| 5 | X-SPIFFE-ID (only with EAI_AUTH_SPIFFE=true and a peer of EAI_TRUSTED_PROXIES) | spiffe | service:<name>, kind service |
| 6 | X-Dev-Tenant, X-Dev-User, X-Dev-Roles (only with EAI_AUTH_DEV=true outside prod) | dev | value of X-Dev-User |
Without a valid credential the answer is 401 unauthenticated with WWW-Authenticate: Bearer. Every 401 counts in the failure budget of the client IP (EAI_RATE_LIMIT_AUTH_FAILURES_PER_MINUTE); past it, every attempt of that IP gets 429 until the window ends. A token longer than 8192 characters is refused before any decoding. GET /v1/identity/whoami returns the resolved identity (id, kind, tenant, roles, scopes, clearance, method).
Human identity: Keycloak and OIDC
- The token is verified against the JWKS keys of
EAI_OIDC_JWKS_URLor discovered fromEAI_OIDC_ISSUER(300 s cache, at most one refetch per 30 s window for an unknownkid). Mandatory claims:exp,iat,iss,sub, andaudwhenEAI_OIDC_AUDIENCEis set. Only algorithms ofEAI_JWT_ALGORITHMSpass, neverHS*nornone. - Roles: the token roles (Keycloak realm and client roles) that belong to
eai.auth.ROLES; an unknown role is ignored. Roles granted in the platform (eai.memberships) are added, exceptplatform_admin. With no role at all, the user isviewer. - Directory administrator groups: a group of
EAI_OIDC_PLATFORM_ADMIN_GROUPS(defaultnavin-platform-admins) givesplatform_admin; a group<EAI_OIDC_TENANT_ADMIN_GROUP_PREFIX><slug>(defaultnavin-admins-) givestenant_adminon that tenant. Matching ignores case and only exact names count. - Clearance: the
clearanceclaim when it ispublic,internal,confidential,restrictedorsecret, elseinternal. Scopes: thescopeclaim, or*when it is absent or containsopenid. - MFA, SAML and sign out are carried by Keycloak: see identity-policy-plane.md.
Local portal accounts
POST /v1/auth/login checks the password (scrypt) and opens an eais_ session shown once; only its sha256 is stored (eai.local_sessions). Lifetime: EAI_LOCAL_SESSION_TTL_SECONDS. Lock after EAI_LOCAL_AUTH_MAX_FAILURES wrong passwords for EAI_LOCAL_AUTH_LOCKOUT_SECONDS. While must_change_password is true, the session only reaches POST /v1/auth/password, POST /v1/auth/logout, GET /v1/me and GET /v1/identity/whoami (403 password_change_required elsewhere). Changing the password revokes the other sessions of the account. Every outcome is audited.
API keys
| Operation | Route | Roles |
|---|---|---|
| List | GET /v1/api-keys | tenant_admin, security_officer, developer (a developer only sees its own keys) |
| Create | POST /v1/api-keys | tenant_admin, developer |
| Rotate | POST /v1/api-keys/{key_id}/rotate | tenant_admin, security_officer, developer (own keys only) |
| Revoke | DELETE /v1/api-keys/{key_id} | tenant_admin, security_officer, developer (own keys only) |
- Format
eai_<12 hex>_<secret>, checked before any database read; only the sha256 is stored (eai.api_keys), compared in constant time. The full value is shown only at creation and rotation. - A key carries its roles, scopes, clearance, a project, an environment and an expiry. Nobody delegates more than they hold: only a
platform_admingrantsplatform_admin, a caller withouttenant_adminonly grants its own roles, and every scope must be held by the caller (403scope_not_held). - Rotation issues a new key (same name, scopes and roles) and revokes the old one, or expires it after
grace_seconds. Refusal codes: 401invalid_api_key,api_key_revoked,api_key_expired.
Service identities (SPIFFE)
- A service identity (
eai.service_identities) binds a SPIFFE ID to a tenant, roles, scopes and a clearance. Create and delete:POST /v1/service-identitiesandDELETE /v1/service-identities/{identity_id}(tenant_admin,security_officer), with the same role and scope guards as API keys. X-SPIFFE-IDis only trusted from a trusted peer (mesh sidecar); an unknown or inactive ID gets 401unknown_spiffe_id. The SVID is issued by SPIRE and verified over TLS 1.3 byeai.domains.identity.spiffe(allow list, deny by default).POST /v1/service-identities/{identity_id}/assumegives a caller the rights of a service identity, never more than its own: roles held on both sides (neverplatform_admin), scopes granted by the identity and held by the caller (else 403service_identity_scope_exceeds), the lower of both clearances.
Tenant resolution
The tenant always comes from the credential, never from the request body:
- API key, local session and service identity: the tenant stored with the credential;
- OIDC token: the
tenant_id(id) ortenant(slug) claim, else the single tenant administered through directory groups. Several administrator groups without a claim give 403tenant_required; aplatform_adminmay act without a tenant.
An unknown tenant answers 401 unknown_tenant, an inactive tenant 403 tenant_suspended. After authentication the Kill Switch Registry is read again: revoked credential (401 credential_revoked), suspended identity (403 identity_suspended), suspended tenant (403 tenant_suspended). In the database, eai.db.tx sets the transaction tenant and forced RLS filters every row.
Roles
Fifteen roles (eai.auth.ROLES, constraint of eai.memberships in sql/postgres/V161). platform_admin passes every require_role check (Principal.has_role), except the explicit checks that exclude it (decryption of audit payloads). The capabilities below come from the role checks of the code and from portal/src/lib/kit.ts (ROLES).
| Role | What it may do (examples taken from the code) |
|---|---|
platform_admin | Create tenants, Platform Manager routes and the IT vault of the Control Plane (only role admitted), licences, revocation of Context Authority keys; never decryption of audit payloads |
tenant_admin | Administer the tenant: members, group mappings and quota profiles, API keys, service identities, policies, security settings, retention, DLP, encryption keys, risk, file security policy; default approver, SOC lead, evidence reader |
security_officer | Security posture and settings, policies, relations and service identities, key revocation, DLP, encryption keys, retention, quarantine review, file security, decryption of audit payloads, seals and legal holds, SOC analyst and lead, break glass approval |
auditor | Read the audit trail, decisions, engine state, group mappings, file scans; decrypt audit payloads with a reason; seals and legal holds; modifies nothing else |
developer | Create its own API keys and secrets, simulate policies and read their history without publishing, risk assessments and exceptions, record a realtime consent; default approver |
finops | Read and administer budgets, quotas and costs; request an approval |
agent | Technical identity of an agent: request an approval, record a consent, storage presigned URLs, model reading |
viewer | Read models, FinOps and observability; default role without any other role |
soc_analyst | SOC analyst: alerts, detections, incidents, reactions allowed by the proportionality guard |
ai_governance_officer | Risk assessments, models and agents, quality and security screens read only |
data_steward | Retention policies, erasure, legal hold, RAG quarantine review |
application_owner | Agents and applications it owns (owner relation) |
ai_platform_engineer | Administer and disable LLM providers, models and routes |
business_owner | Default approver of requests (use cases, sensitive actions) |
support_engineer | Observability and model reading, no writes |
The separation matrix is proven by services/control-plane/tests/test_iam_role_matrix.py; the business roles are detailed in identity-policy-plane.md.
Scopes and clearance
- Scopes:
require_scopeneeds a scope of the token or key; patterns (admin:*,*) are admitted (403insufficient_scope). - Clearance: five ordered levels
public<internal<confidential<restricted<secret. The policy engine compares the data classification with the subject clearance (clearance_required,classification_max).
OpenFGA relations
- Model:
deploy/policies/openfga/model.fga. Types:user,group,tenant,project,agent,model,tool,knowledge_base,dataset,prompt,connector,secret,collection,document,service_identity. Relations:owner,admin(tenant),editor,member,viewer,can_use,can_invoke,can_read. require_relationchecks the resource, then its project, then the tenant. Tenant level:tenant_admin=admin,developerandsecurity_officer=editor, others =member. A denial is audited (authz.deny) and answers 403relation_required.- Without
EAI_OPENFGA_URL, tuples live ineai.authz_tuples(direct tuples,user:*, one level of groups,ownerimplieseditorimpliesviewer). With OpenFGA, objects and non user subjects are prefixed by the tenant id. Management:POST /v1/authz/tuples,DELETE /v1/authz/tuples,POST /v1/authz/check(contextual tuples never stored). - OpenFGA outage: 502
authz_backend_error, denial audited with the reasonbackend_unavailable.
OPA policies and builtin engine
eai.policy.evaluate decides before every sensitive action: the Kill Switch Registry first, then the tenant policies (a matching deny rule wins; allow rules that target the action without applying deny), then the baseline guards (clearance, classification ceiling, regions), then the default EAI_POLICY_DEFAULT or the tenant policy_default setting. Every decision is recorded (eai.policy_decisions, GET /v1/policies/decisions) and audited.
| OPA package | File | Decides |
|---|---|---|
eai.agent_autonomy | deploy/policies/opa/agent_autonomy.rego | Agent runs and autonomous actions |
eai.data_access | deploy/policies/opa/data_access.rego | RAG and data access (ACL, clearance) |
eai.gateway_access | deploy/policies/opa/gateway_access.rego | Gateway entry points other than models and tools |
eai.model_invocation | deploy/policies/opa/model_invocation.rego | Every model call |
eai.tool_invocation | deploy/policies/opa/tool_invocation.rego | Every tool call |
eai.soc_response | deploy/policies/opa/soc_response.rego | SOC containment orders |
eai.lib | deploy/policies/opa/lib.rego | Shared helpers, per tenant overrides |
An obligation (dlp, approval) is returned by POST /v1/policies/evaluate. Publishing, simulation and revisions: identity-policy-plane.md.
Delegations, mandates and break glass
Every agent action applies the intersection of the rights of the original user, the agent, the delegation, the resource and the policy: see identity-context-delegation.md. The delegation administration routes are reserved to tenant_admin and security_officer in the portal (delegationAdmins).
Group mappings
eai.domains.identity.group_mappings applies, at each OIDC authentication (EAI_OIDC_GROUP_SYNC=true), the tenant mappings to the token groups (groups and entra_groups claims): project membership, role and quota profile. What a mapping granted is recorded (eai.group_mapping_grants) and removed when the group leaves the token; memberships granted by hand are never touched.
| Operation | Route | Roles |
|---|---|---|
| List, read | GET /v1/identity/group-mappings, GET /v1/identity/group-mappings/{mapping_id} | any tenant member |
| Create, update, delete | POST /v1/identity/group-mappings, PATCH /v1/identity/group-mappings/{mapping_id}, DELETE /v1/identity/group-mappings/{mapping_id} | tenant_admin |
| Quota profiles | GET /v1/identity/quota-profiles, POST /v1/identity/quota-profiles | read: member; write: tenant_admin |
Roles a mapping may grant through the API: tenant_admin, security_officer, finops, developer, auditor, agent, viewer; never platform_admin.
Tests
services/control-plane/tests/test_security_auth.py (JWT, API keys), services/control-plane/tests/test_iam_role_matrix.py (role separation), services/control-plane/tests/test_iam_openfga_real.py, services/control-plane/tests/test_iam_fail_closed.py, services/control-plane/tests/test_iam_keycloak_real.py.