Authentication
IdentityScribe supports four authentication methods across all channels and the monitoring endpoint.
Quick start
Section titled “Quick start”Two environment variables enable authentication with Bearer tokens:
SCRIBE_AUTH_ENABLED=trueSCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompanyOr in HOCON with named providers:
auth { enabled = true providers { default { issuer = "https://auth.example.com/realms/mycompany" audiences = ["scribe"] } }}How authentication works
Section titled “How authentication works”How requests are authenticated
Clients authenticate by sending an HTTP Authorization header. Scribe supports two header schemes:
| Scheme | Header format | What Scribe receives |
|---|---|---|
| Bearer | Authorization: Bearer <jwt> | A pre-obtained JWT token |
| Basic | Authorization: Basic <base64> | Username and password (base64-encoded) |
The authentication method determines how Scribe validates what it receives:
| Method | Header | Validation |
|---|---|---|
| Bearer tokens | Bearer | Verify JWT signature via JWKS, check claims |
| ROPC | Basic | Exchange credentials with IdP for JWT, then verify |
| LDAP bind | Basic | Search for user DN, bind to LDAP to verify password |
| Local account | Basic | Compare with a configured account; no backend bind |
Scribe tries the configured methods in order. A matching local account owns the credential decision: a wrong local password is rejected and is not retried through another method. An unknown local username may continue to another explicitly allowed Basic method.
Choosing a method
Section titled “Choosing a method”| Your client… | Use | Why |
|---|---|---|
| Already has a JWT from your IdP | Bearer | Fastest — no network calls per request |
| Sends username/password, needs OAuth claims | ROPC | Trades credentials for JWT, gets roles/scopes from IdP |
| Sends username/password, no IdP available | LDAP | Direct validation against directory |
| Repeatedly uses one technical account | Local | Validates configured credentials without a backend authentication request |
Use Bearer tokens unless clients can’t obtain tokens themselves.
MCP channel
Section titled “MCP channel”The MCP channel uses the same auth: OAuth Bearer first, with configured Basic methods (ROPC, LDAP bind, or local account) when tokens are unavailable. See MCP Channel — Authentication for client-specific setup and MCP Cursor callback issues when OAuth callbacks fail.
Authentication methods
Section titled “Authentication methods”Bearer tokens (recommended)
Section titled “Bearer tokens (recommended)”Clients obtain a JWT from your identity provider and include it in requests:
GET /api/users HTTP/1.1Authorization: Bearer eyJhbGciOiJSUzI1...- Client authenticates with IdP (browser redirect, client credentials, etc.)
- IdP issues a signed JWT
- Client sends JWT to Scribe in the Authorization header
- Scribe verifies signature against IdP’s public keys (cached from JWKS)
- Scribe checks expiration, audience, and issuer claims
No per-request calls to the IdP. After the initial JWKS fetch, validation happens locally.
JWT verification with JWKS
ROPC (Resource Owner Password Credentials)
Section titled “ROPC (Resource Owner Password Credentials)”Clients send username and password via HTTP Basic auth. Scribe exchanges those credentials with your IdP for a JWT, then validates the token.
GET /api/users HTTP/1.1Authorization: Basic YWxpY2U6c2VjcmV0Password-based token exchange
Configuration:
auth { enabled = true methods = [bearer, ropc]
providers { default { issuer = "https://auth.example.com" audiences = ["scribe"] client-id = "scribe" client-secret = ${SCRIBE_AUTH_CLIENT_SECRET} } }
ropc { scopes = "openid profile email" }}LDAP bind
Section titled “LDAP bind”Clients send username and password via HTTP Basic auth. Scribe searches for the user’s DN, then binds to LDAP with the provided password.
GET /api/users HTTP/1.1Authorization: Basic YWxpY2U6c2VjcmV0No IdP required. Authentication happens directly against your LDAP directory. LDAP-only auth does not require OIDC provider credentials — you do not need to configure auth.providers, issuer, client-id, or client-secret.
Direct password verification against directory
Configuration:
auth { enabled = true methods = [ldap]
ldap { base = "ou=users,dc=example,dc=com" bind-attribute = "uid" filter = "(objectClass=person)" }}LDAP connection settings (server URL, bind-dn, bind-password) are inherited from the root ldap {} configuration. Configure a service account there — Scribe uses it to search for user DNs before validating passwords. See the auth reference for pattern-based lookup, LDAP role mapping, and attribute configuration.
auth.ldap.filter decides which entries may authenticate with the ldap auth method. Keep it broad enough for every LDAP account that must reach Scribe, including service accounts that bind to the LDAP channel. Use role mapping and access rules for operator UI or admin access.
Map an LDAP group to the admin role and restrict Observe:
auth.ldap { filter = "(objectClass=person)"
roles { from = memberOf rules = [ { match = "cn=scribe-admins,ou=groups,dc=example,dc=com", format = "admin" } ] }}
monitoring.observe.auth.rules = [ { id = "observe-admins", action = allow, where = "subject.roles = admin" } { id = "deny-rest", action = deny }]Observe write actions and Full diagnostic report export already require the admin role by default. Set monitoring.observe.write.auth.rules only when writes need a different operator role.
Local accounts
Section titled “Local accounts”Local accounts are for fixed technical identities that Scribe should validate from trusted configuration. Authentication accepts the configured DN and, when set, its explicit username alias. The alias is not derived from the DN, and the configured DN does not need to exist in the backend directory.
auth { enabled = true methods = [local]
local.accounts = [{ bind-dn = "cn=scribe,dc=example,dc=com" bind-password = ${SCRIBE_LOCAL_PASSWORD} username = "scribe" delegation { enabled = true # Optional complete backend override. Without it, Scribe uses the # root ldap.bind-dn and ldap.bind-password pair. bind-dn = "cn=backend-reader,dc=example,dc=com" bind-password = ${SCRIBE_BACKEND_PASSWORD} } }]}delegation.enabled defaults to true. Set it to false when the account may authenticate and use local operations but must not forward an operation to backend LDAP. A local bind, successful login, and LDAP Who-Am-I response do not open a backend connection. Forwarded operations still pass Scribe access rules first and use the selected configured backend identity; the submitted local login password is never forwarded implicitly. A local account has no automatic roles or administrative access.
For browser sessions, set auth.session.method = local or make local the first configured method. In every mixed method list, local must be first. Failed local credentials, including an incorrect password for a matching account, use auth.failure-delay before the response. Local credentials are not cached. Account and delegation changes take effect after a process restart. A restart clears process-local authentication caches, closes that process’s LDAP connections, and does not revoke an existing browser cookie.
Authentication caching
Section titled “Authentication caching”auth.cache provides inherited defaults for the LDAP, ROPC, and opaque bearer-token introspection caches, with method-specific overrides under auth.ldap.cache, auth.ropc.cache, and auth.bearer.cache. LDAP cache expiry starts when the backend check succeeds; a cache hit does not extend it. Failed, expired, timed-out, or abandoned checks never become reusable successes. Setting a method TTL to 0s disables its result cache while simultaneous LDAP requests for the exact same credentials can still share one bounded backend check. JWT validation keeps its existing key-cache behavior; there is no additional global result cache.
Authentication cache expiry does not revoke an already authenticated LDAP connection or browser session. Use logout or wait for auth.session.session-ttl for browser sessions; connection rebind is the boundary for an LDAP connection.
Access rules
Section titled “Access rules”Access rules let you control who can do what. Rules evaluate top-to-bottom; first match wins. If nothing matches, access is denied.
Out of the box, Scribe allows authenticated requests and denies anonymous access. Prometheus and health endpoints are exceptions — they allow anonymous access for scrapers and Kubernetes.
auth.rules = [ { id = "admin-bypass", action = allow, where = "subject.roles = admin" } { id = "schema-public", action = allow, where = "request.channel = graphql and request.operation = schema" } { action = allow, where = "subject.authenticated = true and request.operation in [search, lookup]" }]Each rule takes action (allow/deny), an optional where filter expression, and an optional id for log/trace visibility.
Common patterns
Section titled “Common patterns”Authenticated access with public health checks:
auth.rules = [ { action = allow, where = "request.path startswith /health" } { action = allow, where = "subject.authenticated = true" }]Restrict history queries to admins:
auth.rules = [ { action = allow, where = "subject.roles = admin" } { action = deny, where = "request.operation = history" } { action = allow, where = "subject.authenticated = true" }]LDAP writes require admin role:
channels.ldap.auth.rules = [ { action = allow, where = "subject.roles = admin" } { action = deny, where = "request.operation = modify or request.operation = delete" } { action = allow, where = "subject.authenticated = true" }]Related
Section titled “Related”- Auth configuration reference — All settings: access rule attributes, identity model, LDAP role mapping, credential caching, JWT validation, browser sessions, multi-provider setup, channel overrides
- Production checklist — TLS, network isolation, token validation hardening
- API & Networking — TLS and socket configuration
- Error Handling — Auth error codes and response formats