Skip to main content

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.

OrderCredentialMethod (auth_method)Resulting principal
1Authorization: Bearer eai_<prefix>_<secret>api_keyapikey:<id>, kind service
2Authorization: Bearer eais_<prefix>_<secret> (portal session opened by POST /v1/auth/login)passwordlocal:<login>, kind user
3Authorization: Bearer <OIDC JWT>oidctoken sub, kind user
4X-API-Key: eai_<prefix>_<secret>api_keyas 1
5X-SPIFFE-ID (only with EAI_AUTH_SPIFFE=true and a peer of EAI_TRUSTED_PROXIES)spiffeservice:<name>, kind service
6X-Dev-Tenant, X-Dev-User, X-Dev-Roles (only with EAI_AUTH_DEV=true outside prod)devvalue 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_URL or discovered from EAI_OIDC_ISSUER (300 s cache, at most one refetch per 30 s window for an unknown kid). Mandatory claims: exp, iat, iss, sub, and aud when EAI_OIDC_AUDIENCE is set. Only algorithms of EAI_JWT_ALGORITHMS pass, never HS* nor none.
  • 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, except platform_admin. With no role at all, the user is viewer.
  • Directory administrator groups: a group of EAI_OIDC_PLATFORM_ADMIN_GROUPS (default navin-platform-admins) gives platform_admin; a group <EAI_OIDC_TENANT_ADMIN_GROUP_PREFIX><slug> (default navin-admins-) gives tenant_admin on that tenant. Matching ignores case and only exact names count.
  • Clearance: the clearance claim when it is public, internal, confidential, restricted or secret, else internal. Scopes: the scope claim, or * when it is absent or contains openid.
  • 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​

OperationRouteRoles
ListGET /v1/api-keystenant_admin, security_officer, developer (a developer only sees its own keys)
CreatePOST /v1/api-keystenant_admin, developer
RotatePOST /v1/api-keys/{key_id}/rotatetenant_admin, security_officer, developer (own keys only)
RevokeDELETE /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_admin grants platform_admin, a caller without tenant_admin only grants its own roles, and every scope must be held by the caller (403 scope_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: 401 invalid_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-identities and DELETE /v1/service-identities/{identity_id} (tenant_admin, security_officer), with the same role and scope guards as API keys.
  • X-SPIFFE-ID is only trusted from a trusted peer (mesh sidecar); an unknown or inactive ID gets 401 unknown_spiffe_id. The SVID is issued by SPIRE and verified over TLS 1.3 by eai.domains.identity.spiffe (allow list, deny by default).
  • POST /v1/service-identities/{identity_id}/assume gives a caller the rights of a service identity, never more than its own: roles held on both sides (never platform_admin), scopes granted by the identity and held by the caller (else 403 service_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) or tenant (slug) claim, else the single tenant administered through directory groups. Several administrator groups without a claim give 403 tenant_required; a platform_admin may 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).

RoleWhat it may do (examples taken from the code)
platform_adminCreate 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_adminAdminister 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_officerSecurity 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
auditorRead the audit trail, decisions, engine state, group mappings, file scans; decrypt audit payloads with a reason; seals and legal holds; modifies nothing else
developerCreate its own API keys and secrets, simulate policies and read their history without publishing, risk assessments and exceptions, record a realtime consent; default approver
finopsRead and administer budgets, quotas and costs; request an approval
agentTechnical identity of an agent: request an approval, record a consent, storage presigned URLs, model reading
viewerRead models, FinOps and observability; default role without any other role
soc_analystSOC analyst: alerts, detections, incidents, reactions allowed by the proportionality guard
ai_governance_officerRisk assessments, models and agents, quality and security screens read only
data_stewardRetention policies, erasure, legal hold, RAG quarantine review
application_ownerAgents and applications it owns (owner relation)
ai_platform_engineerAdminister and disable LLM providers, models and routes
business_ownerDefault approver of requests (use cases, sensitive actions)
support_engineerObservability 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_scope needs a scope of the token or key; patterns (admin:*, *) are admitted (403 insufficient_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_relation checks the resource, then its project, then the tenant. Tenant level: tenant_admin = admin, developer and security_officer = editor, others = member. A denial is audited (authz.deny) and answers 403 relation_required.
  • Without EAI_OPENFGA_URL, tuples live in eai.authz_tuples (direct tuples, user:*, one level of groups, owner implies editor implies viewer). 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 reason backend_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 packageFileDecides
eai.agent_autonomydeploy/policies/opa/agent_autonomy.regoAgent runs and autonomous actions
eai.data_accessdeploy/policies/opa/data_access.regoRAG and data access (ACL, clearance)
eai.gateway_accessdeploy/policies/opa/gateway_access.regoGateway entry points other than models and tools
eai.model_invocationdeploy/policies/opa/model_invocation.regoEvery model call
eai.tool_invocationdeploy/policies/opa/tool_invocation.regoEvery tool call
eai.soc_responsedeploy/policies/opa/soc_response.regoSOC containment orders
eai.libdeploy/policies/opa/lib.regoShared 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.

OperationRouteRoles
List, readGET /v1/identity/group-mappings, GET /v1/identity/group-mappings/{mapping_id}any tenant member
Create, update, deletePOST /v1/identity/group-mappings, PATCH /v1/identity/group-mappings/{mapping_id}, DELETE /v1/identity/group-mappings/{mapping_id}tenant_admin
Quota profilesGET /v1/identity/quota-profiles, POST /v1/identity/quota-profilesread: 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.