Skip to content

Authentication

IdentityScribe supports four authentication methods across all channels and the monitoring endpoint.

Two environment variables enable authentication with Bearer tokens:

Terminal window
SCRIBE_AUTH_ENABLED=true
SCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompany

Or in HOCON with named providers:

auth {
enabled = true
providers {
default {
issuer = "https://auth.example.com/realms/mycompany"
audiences = ["scribe"]
}
}
}
Authentication Flow

How requests are authenticated

HTTP Request
?Authorization header?
Bearer token
JWT ValidationVerify signature via JWKS
Basic auth
ROPC, LDAP, or localExchange, bind, or check locally
No header
AnonymousIf allowed by config
Authenticated principal
or
401 Unauthorized

Clients authenticate by sending an HTTP Authorization header. Scribe supports two header schemes:

SchemeHeader formatWhat Scribe receives
BearerAuthorization: Bearer <jwt>A pre-obtained JWT token
BasicAuthorization: Basic <base64>Username and password (base64-encoded)

The authentication method determines how Scribe validates what it receives:

MethodHeaderValidation
Bearer tokensBearerVerify JWT signature via JWKS, check claims
ROPCBasicExchange credentials with IdP for JWT, then verify
LDAP bindBasicSearch for user DN, bind to LDAP to verify password
Local accountBasicCompare 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.

Your client…UseWhy
Already has a JWT from your IdPBearerFastest — no network calls per request
Sends username/password, needs OAuth claimsROPCTrades credentials for JWT, gets roles/scopes from IdP
Sends username/password, no IdP availableLDAPDirect validation against directory
Repeatedly uses one technical accountLocalValidates configured credentials without a backend authentication request

Use Bearer tokens unless clients can’t obtain tokens themselves.

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.

Clients obtain a JWT from your identity provider and include it in requests:

GET /api/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1...
  1. Client authenticates with IdP (browser redirect, client credentials, etc.)
  2. IdP issues a signed JWT
  3. Client sends JWT to Scribe in the Authorization header
  4. Scribe verifies signature against IdP’s public keys (cached from JWKS)
  5. Scribe checks expiration, audience, and issuer claims

No per-request calls to the IdP. After the initial JWKS fetch, validation happens locally.

Bearer Token Validation

JWT verification with JWKS

Client
Scribe
IdP
1
Authenticate (OIDC/OAuth)
2
JWT access token
3
Authorization: Bearer <token>
4
Fetch JWKS public keys
5
JWK Set
6
Validate JWT signature
7
API response

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.1
Authorization: Basic YWxpY2U6c2VjcmV0
ROPC Authentication

Password-based token exchange

Client
Scribe
IdP
1
Basic auth (user:pass)
2
Token request (ROPC grant)
3
JWT access token
4
Validate JWT signature
5
API response

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"
}
}

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.1
Authorization: Basic YWxpY2U6c2VjcmV0

No 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.

LDAP Bind Authentication

Direct password verification against directory

Client
Scribe
LDAP
1
Basic auth (user:pass)
2
Search user DN (service bind)
3
User DN found
4
BIND as user DN
5
BIND success
6
Extract identity from attributes
7
API response

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 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.

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 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.

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" }
]