Skip to content

Authentication

Authentication is the first layer of IndentiaDB's three-layer security model. It establishes the caller's identity and produces a principal — a user identity with roles and SIDs (Security Identifiers) used by the authorization and ACL layers.


LDAP (including Windows Active Directory)

LDAP authentication supports any LDAP-compliant directory, including Microsoft Active Directory, OpenLDAP, and FreeIPA.

Configuration

[authentication.ldap]
server_url               = "ldaps://ldap.example.com:636"
bind_dn                  = "cn=service,ou=services,dc=example,dc=com"
bind_password            = "${LDAP_BIND_PASSWORD}"
user_search_base         = "ou=users,dc=example,dc=com"
user_search_filter       = "(sAMAccountName={0})"   # AD; use "(uid={0})" for OpenLDAP
group_member_attribute   = "memberOf"
sid_attribute            = "objectSid"
display_name_attribute   = "displayName"
email_attribute          = "mail"
timeout_seconds          = 10
pool_size                = 5
follow_referrals         = false

# Map AD groups to IndentiaDB roles
[authentication.ldap.group_role_mapping]
"CN=IndentiaAdmins,OU=Groups,DC=example,DC=com"         = "admin"
"CN=DataScientists,OU=Groups,DC=example,DC=com"         = "writer"
"CN=Readers,OU=Groups,DC=example,DC=com"                = "reader"

# Map AD groups to Windows SIDs for triple-level ACL
[authentication.ldap.group_sid_mapping]
"CN=Finance,OU=Groups,DC=example,DC=com"                = "S-1-5-21-domain-2001"
"CN=HR,OU=Groups,DC=example,DC=com"                     = "S-1-5-21-domain-2002"
"CN=Engineering,OU=Groups,DC=example,DC=com"            = "S-1-5-21-domain-2003"
"CN=Executive,OU=Groups,DC=example,DC=com"              = "S-1-5-21-domain-2004"

How LDAP Authentication Works

  1. Client sends Authorization: Basic base64(username:password) header.
  2. IndentiaDB takes a connection from the pool and performs a bind with the service DN.
  3. A search with user_search_filter (substituting {0} with the supplied username) retrieves the user's DN and group memberships.
  4. IndentiaDB attempts a second bind with the user's DN and supplied password.
  5. If the bind succeeds, the memberOf attribute is read to determine group membership.
  6. Groups are mapped to roles via group_role_mapping and to SIDs via group_sid_mapping.
  7. The connection is returned to the pool.

Windows SID Support (objectSid)

Active Directory stores SIDs as binary objectSid attributes. IndentiaDB automatically parses the binary SID format into the standard string representation (S-1-5-21-<domain>-<RID>). This allows triple-level ACL annotations to reference AD group SIDs directly.

If the user's own objectSid attribute is present, it is also included in the principal's SID set alongside group SIDs.

Connection Pooling

pool_size controls how many pre-established LDAP connections IndentiaDB maintains. Each authentication request borrows a connection from the pool, performs the bind verification, and returns it. Under high authentication load, requests queue until a connection is available.

Pool Size Tuning

Set pool_size to slightly above your peak concurrent authentication requests. A pool of 5–10 is sufficient for most deployments. Too-large pools waste AD server resources.

LDAPS (TLS)

Use ldaps:// (port 636) for encrypted connections. The server's TLS certificate is validated against the system trust store. For self-signed or internal CA certificates:

[authentication.ldap]
server_url    = "ldaps://ad.internal.example.com:636"
ca_cert_file  = "/etc/indentiadb/certs/internal-ca.pem"

OpenID Connect (OIDC) / Keycloak

OIDC is the recommended authentication provider for modern deployments. IndentiaDB acts as an OIDC Resource Server — it validates JWT access tokens issued by the OIDC provider.

Configuration

[authentication.oidc]
enabled                     = true
issuer_url                  = "https://auth.example.com/realms/myrealm"
client_id                   = "indentiadb-client"
audience                    = "indentiadb-api"
roles_claim                 = "realm_access.roles"
sids_claim                  = "groups"
# user_sid_claim            = "employee_sid"   # optional; see "User SIDs" below
# jwks_url                  = "https://auth.example.com/keys"   # optional; skips discovery
jwks_refresh_interval_secs  = 3600
http_timeout_secs           = 10

[authentication.oidc.role_mapping]
"realm-admin"   = "admin"
"realm-writer"  = "writer"
"realm-reader"  = "reader"

How OIDC Authentication Works

  1. Client obtains a JWT access token from the OIDC provider (e.g., via Authorization Code flow, Client Credentials, or Device Authorization Grant).
  2. Client sends Authorization: Bearer <token> with every request to IndentiaDB.
  3. IndentiaDB locates the OIDC provider's JWKS (JSON Web Key Set) through the discovery document at {issuer_url}/.well-known/openid-configuration — or directly at jwks_url when that is configured — on first request. The JWKS is cached and refreshed every jwks_refresh_interval_secs.
  4. The JWT signature is validated against the matching key in the JWKS.
  5. Standard claims are verified: iss (must match issuer_url), aud (must contain audience), exp (must be in the future).
  6. Roles are extracted from roles_claim (e.g., realm_access.roles for Keycloak) and mapped via role_mapping.
  7. The principal's user SID is taken from user_sid_claim or derived from the verified issuer and subject (see User SIDs). Group SIDs are extracted from sids_claim (e.g., AD group SIDs embedded as a claim by the OIDC provider) and added to the principal's SID set.

Issuer Endpoints

All issuer, discovery, and JWKS URLs must use HTTPS. Two options cover issuers that do not fit the standard discovery flow:

Option Default Effect
jwks_url unset Explicit JWKS endpoint. When set, discovery is skipped entirely — use it for issuers that publish signing keys but no /.well-known/openid-configuration. Tokens are still validated against issuer_url, and the URL is subject to the same transport rules.
allow_insecure_cluster_http false Permits plain HTTP for this provider's issuer and JWKS endpoints only when the host is a cluster-local service name (*.svc or *.svc.cluster.local), which never resolves outside the pod network. Public hosts stay HTTPS-only whether or not the option is set. Refused under IG_PRODUCTION: production cluster-local issuers must use HTTPS.

User SIDs

Every authenticated OIDC principal gets a stable user SID, so per-user triple-level ACL grants keep resolving to the same person across logins:

  1. If user_sid_claim is set and the token carries that claim, its value is the user SID — either an SDDL SID (S-1-5-21-…) or a principal label such as user:jan@example.com.
  2. Otherwise the user SID is the principal label user:id:<iss>|<sub>, built from the token's verified issuer and subject. Subjects are only unique per issuer, so including the issuer keeps users of different identity providers apart.

A grant for one OIDC user therefore looks like:

@prefix security: <http://indentiagraph.io/security#> .
@prefix ex:       <http://example.org/> .

<< ex:report ex:salary "82000" >>
    security:allowedSid "user:id:https://auth.example.com/realms/myrealm|4f1b9c2e-8d7a-4e21-9b3f-0c6d5e7a8b91" .

When an ACL context needs a user SID for a principal that has none, IndentiaDB falls back to a synthetic SID derived from a hash of the principal id: S-1-5-21-synthetic-<8 hex digits>-<rid>, with the high 32 bits of the hash in the domain and the low 32 bits as the RID. (Earlier releases dropped the high bits, so synthetic SIDs from before this format no longer match.) Set IG_ACL_SYNTHETIC_SID_SALT to key them per deployment, and keep the value constant once set — changing it re-keys every synthetic SID. Prefer grants on principal labels or directory SIDs over synthetic SIDs.

Multiple Issuers and Subject ACLs

Additional trusted issuers are configured as [[authentication.additional_oidc]] entries with the same fields as [authentication.oidc]; a token from any listed issuer is accepted against that issuer's own keys.

Issuer selection uses the token's verified issuer configuration. A token from an additional issuer is not validated against the primary issuer's key set. Keep each issuer's audience, claim mapping, and tenant binding explicit.

Delegated SIDs for a trusted service

On the primary API, only an explicitly configured service token with the sid-delegator role may supply per-request SIDs through X-Indentia-Sid. The first valid value is the user SID; subsequent values are group SIDs. This replaces that request's SID set, while the token's tenant and operation permissions remain authoritative. It is a trust grant to the service.

OIDC/JWT callers and registry-backed machine identities cannot acquire this behavior by supplying the header or using the same role name. Ordinary service tokens retain their configured principals. The separate :9200 identity-proxy contract also requires its configured proxy token; see Elasticsearch authentication.

Because an OIDC subject is only unique within its issuer, a subject-specific ACL entry for an OIDC principal must name the verified issuer with security:identityIssuer in the ACL file ([acl] config_file):

@prefix access:   <http://indentiagraph.io/access#> .
@prefix security: <http://indentiagraph.io/security#> .

access:alice
    access:graphs ( <https://id.indentia.ai/sources/acme/finance> ) ;
    security:identityIssuer "https://auth.example.com/realms/myrealm" .
  • The entry applies only to tokens whose verified iss equals the declared issuer. A token for the same subject from another issuer does not match it and is denied, rather than falling back to role or default graph access.
  • An entry without security:identityIssuer whose name matches an OIDC subject now denies that principal instead of granting access. Add the issuer to existing subject entries when you upgrade.
  • Each actor entry may declare exactly one issuer; role entries cannot declare one. Entries for LDAP users and service tokens are unaffected.

Supported Signing Algorithms

Algorithm Type Notes
RS256 RSA PKCS#1 v1.5, SHA-256 Most common; Keycloak default
RS384 RSA PKCS#1 v1.5, SHA-384 —
RS512 RSA PKCS#1 v1.5, SHA-512 —
ES256 ECDSA P-256, SHA-256 Smaller tokens, faster verification
ES384 ECDSA P-384, SHA-384 —
PS256 RSA-PSS, SHA-256 —
PS384 RSA-PSS, SHA-384 —
PS512 RSA-PSS, SHA-512 —
EdDSA Ed25519 Fastest verification; used by newer providers

HS256 Not Supported

HMAC-based JWT algorithms (HS256, HS384, HS512) are not supported because they require sharing the signing secret with IndentiaDB, which violates the principle of the OIDC provider being the sole issuer. Use RS256 or ES256.

Keycloak Setup

Configure IndentiaDB as a client in Keycloak:

  1. Create a client with Client Protocol: openid-connect and Access Type: bearer-only.
  2. Set Valid Redirect URIs to * (not required for bearer-only clients).
  3. Add a mapper: Token Claim Name: realm_access.roles, Claim JSON Type: JSON.
  4. If using AD group SIDs, configure a Group Membership mapper with claim name groups.

Fetching a Token for Testing

# Client Credentials (service-to-service)
TOKEN=$(curl -s -X POST \
  "https://auth.example.com/realms/myrealm/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=my-service" \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token')

# Use the token
curl -X POST http://localhost:7001/sparql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/sparql-query" \
  -d 'SELECT * WHERE { ?s ?p ?o } LIMIT 10'

JWT Bearer Tokens

For service accounts and programmatic access, IndentiaDB can validate JWTs issued by any standard-compliant OIDC provider or by an internal token service. Configure as OIDC above — JWT bearer authentication is the underlying mechanism used by the OIDC integration.

For self-issued JWTs (e.g., tokens issued by your own backend):

[authentication.jwt]
algorithm          = "RS256"
public_key_file    = "/etc/indentiadb/certs/jwt-public-key.pem"
audience           = "indentiadb-api"
issuer             = "https://my-service.example.com"
roles_claim        = "roles"
sids_claim         = "sids"

JWT Claim Structure

IndentiaDB reads the following claims from the JWT:

Claim Required Description
sub Yes Subject (user identifier)
iss Yes Issuer (validated against config)
aud Yes Audience (validated against config)
exp Yes Expiry timestamp
iat No Issued-at timestamp
Configured roles_claim No Array of role strings
Configured sids_claim No Array of SID strings

Service Tokens

Service tokens are tenant-scoped, file-backed bearer credentials for machine identities — monitoring agents, reconciliation loops, internal services — that should not depend on an external OIDC provider. Unlike JWTs they are opaque: no claims, no signature, no expiry. Identity, roles, and tenant are bound to the token in server configuration, not carried in the token itself.

Token Format

A service token is the prefix igsvc_ followed by exactly 64 lowercase hex characters (32 random bytes):

echo "igsvc_$(openssl rand -hex 32)" > /etc/indentiagraph/tokens/prometheus

Configuration

[[authentication.service_tokens]]
enabled    = true
subject    = "prometheus"
token_file = "/etc/indentiagraph/tokens/prometheus"   # absolute path required
roles      = ["reader"]
tenant     = "platform-monitoring"
# principals = ["user:prometheus"]                    # optional ACL identities

Validation is strict and happens before the HTTP listener binds: the token file must exist and contain a well-formed token, duplicate subjects and duplicate token material are rejected, and the tenant must be a valid tenant slug. A misconfigured entry fails startup rather than silently degrading.

Usage

curl -H "Authorization: Bearer igsvc_4f2a…e9c1" http://localhost:7001/health/details
  • Tokens are compared in constant time. An unknown igsvc_-prefixed token fails authentication outright — it never falls through to other providers. Non-igsvc_ bearer tokens are passed on to OIDC/JWT as usual, so both can coexist.
  • The tenant is bound to the credential: the configured tenant is stamped onto the authenticated principal, and no request header can override it. It is authoritative for tenant-scoped authorization; see Service Token Tenants and Principals, which also covers the optional principals list.
  • A configured service-token provider satisfies the "at least one authentication provider" requirement of the IG_PRODUCTION boot gate.

Rotation

Service tokens have no built-in expiry. Rotate operationally: replace the token file (or the mounted Kubernetes Secret) and restart the server so the identity re-resolves. Keep one identity per consumer so a rotation affects exactly one client.


HTTP Basic Auth

Basic Auth is supported for backward compatibility and development convenience. Credentials are sent as Authorization: Basic base64(username:password).

Use Only for Development

HTTP Basic Auth transmits credentials with every request. Without TLS, credentials are exposed in plaintext. In production, always use OIDC/JWT with HTTPS, or LDAP over LDAPS. Basic Auth is suitable only for local development on localhost.

[authentication.basic]
enabled = true
users = [
    { username = "dev-admin", password_hash = "$argon2id$v=19$m=65536,t=3,p=4$...", roles = ["admin"] },
    { username = "dev-reader", password_hash = "$argon2id$v=19$m=65536,t=3,p=4$...", roles = ["reader"] },
]

Passwords are stored as Argon2id hashes. Generate a hash:

indentiagraph auth hash-password --password "mysecretpassword"
# Output: $argon2id$v=19$m=65536,t=3,p=4$...

CLI Authentication

The IndentiaDB CLI supports OIDC Device Authorization Grant (device flow) for interactive logins — no browser redirect required for CLI use.

OIDC Device Login

indentiagraph auth login \
  --profile dev \
  --endpoint http://localhost:7001 \
  --issuer https://auth.example.com/realms/main \
  --client-id indentiagraph-cli \
  --scope "openid profile email offline_access" \
  --default

The CLI prints a verification URL and code:

Open https://auth.example.com/realms/main/device in your browser
Enter code: ABCD-EFGH
Waiting for authorization...
✓ Logged in as alice@example.com (roles: admin)
Profile 'dev' saved.

--default marks this profile as the default for subsequent CLI commands.

List Profiles

indentiagraph auth profiles list
NAME     ENDPOINT                  USER                    EXPIRES
dev *    http://localhost:7001     alice@example.com       2025-11-15T10:30:00Z
staging  http://staging:7001       alice@example.com       2025-11-15T08:00:00Z
prod     http://prod-lb:7001       svc-deploy@example.com  never (client_credentials)

Get Current Token

indentiagraph auth token --profile dev

Prints the raw JWT access token to stdout. Useful for scripting:

TOKEN=$(indentiagraph auth token --profile dev)
curl -H "Authorization: Bearer $TOKEN" http://localhost:7001/sparql ...

Refresh Token

indentiagraph auth token --profile dev --refresh

Uses the stored refresh token to obtain a new access token from the OIDC provider. The profile is updated with the new tokens. The CLI automatically refreshes expired tokens before each command.


Rate Limiting (Brute Force Protection)

IndentiaDB rate-limits failed authentication attempts per client IP and per attempted username to slow down brute-force attacks. The defaults are:

[authentication.rate_limit]
max_attempts_per_ip   = 10    # failed attempts from one IP before lockout
max_attempts_per_user = 5     # failed attempts against one username before lockout
lockout_duration_secs = 300   # lockout duration (5 min)

Behavior: - Repeated failures from the same IP or against the same username impose an increasing delay before the next attempt is accepted: 2 seconds after the second failure, doubling with each further failure, capped at 5 minutes. - Reaching max_attempts_per_ip or max_attempts_per_user locks that IP or username out for lockout_duration_secs. - Rate-limited requests return 429 Too Many Requests with error rate_limited and a Retry-After header. - A successful authentication resets the counters for that IP and username. - Lockouts are recorded as RateLimitViolation audit events. - Limiter state is bounded: idle entries that are not locked out are swept periodically, each table (IPs, usernames) holds at most 100,000 entries — evicting the oldest first and active lockouts last — and username hints are truncated to 256 bytes, so a flood of distinct keys cannot grow memory without bound.

Per-Node State

Rate-limit state is kept in-process. In a multi-node Raft cluster each node tracks its own failure counts.


HTTP Error Codes

Code Meaning Common Causes
401 Unauthorized Authentication required or failed Missing Authorization header; expired JWT; invalid Basic Auth credentials; LDAP bind failure
403 Forbidden Authenticated but not authorized Role has Read permission but request needs Write; ACL blocks access to the requested graph
429 Too Many Requests Rate limit exceeded Too many failed login attempts from this IP

The 401 response includes a WWW-Authenticate header indicating the supported schemes:

WWW-Authenticate: Bearer realm="IndentiaDB", error="invalid_token", error_description="Token expired"
WWW-Authenticate: Basic realm="IndentiaDB"