Auth
Authentication Configuration
Protects all channels and monitoring endpoints with token or credential validation. Channels inherit these settings and can override them per-channel.
Quick Start
Section titled “Quick Start”Enable auth with environment variables:
SCRIBE_AUTH_ENABLED=trueSCRIBE_AUTH_ISSUER=https://auth.example.com/realms/mycompanyOr in HOCON:
auth { enabled = true providers.default.issuer = "https://auth.example.com/realms/mycompany"}Which Method Should I Use?
Section titled “Which Method Should I Use?”| Your situation | Use this | Why |
|---|---|---|
| Modern apps with OAuth/OIDC | bearer | Tokens validated offline, no IdP calls per request |
| Legacy apps sending username/password | ropc | Exchanges creds for tokens at runtime |
| Internal tools, no IdP available | ldap | Binds directly to LDAP, no tokens needed |
Most deployments should use bearer tokens. Add ropc or ldap only for clients that cannot obtain tokens themselves.
Channel Overrides
Section titled “Channel Overrides”Channels inherit from this section and can override settings:
channels.rest.auth { enabled = true}See also: Production Checklist in the documentation.
auth.bearer
Section titled “auth.bearer”Bearer token validation
Settings for validating bearer tokens (JWT or opaque).
auth.bearer.cache
Section titled “auth.bearer.cache”Opaque token cache
Caches introspection results to reduce IdP calls. Only applies to opaque tokens; JWTs are validated locally.
| Property | Value |
|---|---|
| Default | ${auth.cache} |
Inherits from: auth.cache
auth.bearer.cache = ${auth.cache}auth.bearer.cache
Section titled “auth.bearer.cache”Opaque token cache
Caches introspection results to reduce IdP calls. Only applies to opaque tokens; JWTs are validated locally.
auth.bearer.cache.max-size
Section titled “auth.bearer.cache.max-size”Maximum cache entries (LRU eviction) Inherits from auth.cache.max-size if not set.
Priority: SCRIBE_AUTH_BEARER_CACHE_MAX_SIZE > auth.cache.max-size
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_BEARER_CACHE_MAX_SIZE (optional) |
auth.bearer.cache.max-size = ${?SCRIBE_AUTH_BEARER_CACHE_MAX_SIZE}auth.bearer.cache.ttl
Section titled “auth.bearer.cache.ttl”How long to cache successful introspection results Inherits from auth.cache.ttl if not set.
Priority: SCRIBE_AUTH_BEARER_CACHE_TTL > auth.cache.ttl
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_BEARER_CACHE_TTL (optional) |
auth.bearer.cache.ttl = ${?SCRIBE_AUTH_BEARER_CACHE_TTL}auth.bearer.provider
Section titled “auth.bearer.provider”Provider for opaque token introspection When a bearer token is opaque (not a JWT), Scribe calls the provider’s introspection endpoint to validate it. Requires client credentials. Auto-selection:
- If only one provider has client credentials → auto-selected
- If multiple providers have credentials → must be explicitly set
JWTs are validated offline via JWKS and don’t require this setting.
Priority: SCRIBE_AUTH_BEARER_PROVIDER > config
| Property | Value |
|---|---|
| Default | "default" |
| Override | SCRIBE_AUTH_BEARER_PROVIDER (optional) |
auth.bearer.provider = ${?SCRIBE_AUTH_BEARER_PROVIDER}auth.cache
Section titled “auth.cache”Auth result cache (global defaults) ======================================================================= Global cache settings for authentication results. ropc.cache, bearer.cache, and ldap.cache inherit from here but can override via environment variables. Sub-caches have NO hardcoded values—only env var overrides. This ensures changes to global defaults propagate automatically.
auth.cache.max-size
Section titled “auth.cache.max-size”Maximum cache entries (LRU eviction) Size based on expected concurrent users. Each entry is ~500 bytes (principal + metadata).
Priority: SCRIBE_AUTH_CACHE_MAX_SIZE > config
| Property | Value |
|---|---|
| Default | 1000 |
| Override | SCRIBE_AUTH_CACHE_MAX_SIZE (optional) |
auth.cache.max-size = 1000auth.cache.max-size = ${?SCRIBE_AUTH_CACHE_MAX_SIZE}auth.cache.ttl
Section titled “auth.cache.ttl”How long to cache successful authentications Short values (1-5m) balance performance with security. Longer values reduce backend load but delay credential changes.
Priority: SCRIBE_AUTH_CACHE_TTL > config
| Property | Value |
|---|---|
| Default | 5m |
| Override | SCRIBE_AUTH_CACHE_TTL (optional) |
auth.cache.ttl = 5mauth.cache.ttl = ${?SCRIBE_AUTH_CACHE_TTL}auth.discovery
Section titled “auth.discovery”OIDC Discovery cache
Controls caching of IdP metadata (/.well-known/openid-configuration). Endpoints like token_endpoint, introspection_endpoint, and userinfo_endpoint are discovered from the cached metadata. Each provider can override these settings in their discovery {} block.
auth.discovery.refresh-interval
Section titled “auth.discovery.refresh-interval”Fallback refresh interval
Used when the IdP doesn’t send Cache-Control headers. Most IdPs send max-age, so this is rarely needed.
Priority: SCRIBE_AUTH_DISCOVERY_REFRESH_INTERVAL > config
| Property | Value |
|---|---|
| Default | 5m |
| Override | SCRIBE_AUTH_DISCOVERY_REFRESH_INTERVAL (optional) |
auth.discovery.refresh-interval = 5mauth.discovery.refresh-interval = ${?SCRIBE_AUTH_DISCOVERY_REFRESH_INTERVAL}auth.discovery.stale-ttl
Section titled “auth.discovery.stale-ttl”How long stale data can be used After metadata expires (per Cache-Control headers), it remains usable as “stale” for this duration while a refresh happens in the background.
Priority: SCRIBE_AUTH_DISCOVERY_STALE_TTL > config
| Property | Value |
|---|---|
| Default | 1h |
| Override | SCRIBE_AUTH_DISCOVERY_STALE_TTL (optional) |
auth.discovery.stale-ttl = 1hauth.discovery.stale-ttl = ${?SCRIBE_AUTH_DISCOVERY_STALE_TTL}auth.discovery.strategy
Section titled “auth.discovery.strategy”Caching strategy
How to handle expired discovery metadata: stale-while-revalidate - Return stale immediately, refresh async in background Best for low latency, eventual consistency stale-if-error - Return stale if refresh fails (IdP unavailable) Best for resilience, availability strict - Block until fresh data, fail if unavailable Best for strong consistency
Priority: SCRIBE_AUTH_DISCOVERY_STRATEGY > config
| Property | Value |
|---|---|
| Default | stale-while-revalidate |
| Override | SCRIBE_AUTH_DISCOVERY_STRATEGY (optional) |
auth.discovery.strategy = stale-while-revalidateauth.discovery.strategy = ${?SCRIBE_AUTH_DISCOVERY_STRATEGY}auth.enabled
Section titled “auth.enabled”Enable authentication
When false: auth subsystem is off, no credentials extracted, no principal set. Browser UIs (/ui, /observe) remain accessible without login. When true: credentials are extracted and validated per configured methods
Priority: SCRIBE_AUTH_ENABLED > config
| Property | Value |
|---|---|
| Default | false |
| Override | SCRIBE_AUTH_ENABLED (optional) |
auth.enabled = falseauth.enabled = ${?SCRIBE_AUTH_ENABLED}auth.failure-delay
Section titled “auth.failure-delay”Auth failure delay
Delay before responding to failed authentication attempts. This mitigates brute-force password guessing attacks by making each failed attempt slower, similar to standard LDAP server behavior. Applies to all channels (LDAP, REST, GraphQL, MCP) and monitoring endpoints. Set to 0 to disable the delay (e.g., for testing).
Priority: SCRIBE_AUTH_FAILURE_DELAY > config
| Property | Value |
|---|---|
| Default | 2s |
| Override | SCRIBE_AUTH_FAILURE_DELAY (optional) |
auth.failure-delay = 2sauth.failure-delay = ${?SCRIBE_AUTH_FAILURE_DELAY}auth.http
Section titled “auth.http”HTTP client settings (global defaults) ======================================================================= Shared HTTP client configuration for all auth-related network calls: discovery, ROPC token exchange, token introspection, and userinfo. Method-specific overrides: auth.ropc.http, auth.bearer.http, auth.discovery.http
auth.http.connect-timeout
Section titled “auth.http.connect-timeout”Connection timeout
How long to wait when establishing a connection to the IdP.
Priority: SCRIBE_AUTH_HTTP_CONNECT_TIMEOUT > config
| Property | Value |
|---|---|
| Default | 10s |
| Override | SCRIBE_AUTH_HTTP_CONNECT_TIMEOUT (optional) |
auth.http.connect-timeout = 10sauth.http.connect-timeout = ${?SCRIBE_AUTH_HTTP_CONNECT_TIMEOUT}auth.http.request-timeout
Section titled “auth.http.request-timeout”Request timeout
Maximum time for a complete request (connect + send + response).
Priority: SCRIBE_AUTH_HTTP_REQUEST_TIMEOUT > config
| Property | Value |
|---|---|
| Default | 30s |
| Override | SCRIBE_AUTH_HTTP_REQUEST_TIMEOUT (optional) |
auth.http.request-timeout = 30sauth.http.request-timeout = ${?SCRIBE_AUTH_HTTP_REQUEST_TIMEOUT}auth.http.ssl
Section titled “auth.http.ssl”SSL/TLS settings
Inherits from global ssl {} section by default.
Override here for IdP-specific certificates.
auth.http.ssl.insecure
Section titled “auth.http.ssl.insecure”Skip certificate verification (INSECURE - dev only)
Priority: SCRIBE_AUTH_HTTP_SSL_INSECURE > config
| Property | Value |
|---|---|
| Default | false |
| Override | SCRIBE_AUTH_HTTP_SSL_INSECURE (optional) |
auth.http.ssl.insecure = falseauth.http.ssl.insecure = ${?SCRIBE_AUTH_HTTP_SSL_INSECURE}auth.http.ssl.trust-store
Section titled “auth.http.ssl.trust-store”Trust store path (PEM or JKS) If not set, uses system default trust store.
Priority: SCRIBE_AUTH_HTTP_SSL_TRUST_STORE > ssl.trust-store
trust-store = ${?ssl.trust-store}
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_HTTP_SSL_TRUST_STORE (optional) |
auth.http.ssl.trust-store = ${?SCRIBE_AUTH_HTTP_SSL_TRUST_STORE}auth.jwt
Section titled “auth.jwt”JWT validation settings
These settings control how bearer tokens are validated. Most defaults follow security best practices - only change if you understand the implications.
auth.jwt.allowed-algorithms
Section titled “auth.jwt.allowed-algorithms”Allowed signing algorithms
Allowed algorithms for token signatures. Tokens signed with other algorithms are rejected.
Common algorithms:
RS256, RS384, RS512 - RSA with SHA-256/384/512 (recommended) ES256, ES384, ES512 - ECDSA with SHA-256/384/512 PS256, PS384, PS512 - RSA-PSS with SHA-256/384/512
Default: All RS*, ES*, PS* algorithms
| Property | Value |
|---|---|
| Default | [RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512] |
| Override | SCRIBE_AUTH_JWT_ALLOWED_ALGORITHMS (optional) |
auth.jwt.allowed-algorithms = [RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512]auth.jwt.allowed-algorithms = ${?SCRIBE_AUTH_JWT_ALLOWED_ALGORITHMS}auth.jwt.clock-skew
Section titled “auth.jwt.clock-skew”Clock skew tolerance (in seconds) Allows for minor time differences between Scribe and the IdP. The exp, nbf, and iat claims are validated with this tolerance.
Default: 60 seconds (1 minute)
| Property | Value |
|---|---|
| Default | 60 |
| Override | SCRIBE_AUTH_JWT_CLOCK_SKEW (optional) |
auth.jwt.clock-skew = 60auth.jwt.clock-skew = ${?SCRIBE_AUTH_JWT_CLOCK_SKEW}auth.jwt.max-lifetime
Section titled “auth.jwt.max-lifetime”Maximum token lifetime (in minutes) Tokens with ‘exp’ claim further than this into the future are rejected. Set to 0 to disable this check.
Default: 1440 (24 hours)
| Property | Value |
|---|---|
| Default | 1440 |
| Override | SCRIBE_AUTH_JWT_MAX_LIFETIME (optional) |
auth.jwt.max-lifetime = 1440auth.jwt.max-lifetime = ${?SCRIBE_AUTH_JWT_MAX_LIFETIME}auth.jwt.require-expiration
Section titled “auth.jwt.require-expiration”Require expiration claim
When true (default), tokens without an ‘exp’ claim are rejected. Strongly recommended to keep enabled for security.
| Property | Value |
|---|---|
| Default | true |
| Override | SCRIBE_AUTH_JWT_REQUIRE_EXPIRATION (optional) |
auth.jwt.require-expiration = trueauth.jwt.require-expiration = ${?SCRIBE_AUTH_JWT_REQUIRE_EXPIRATION}auth.jwt.require-issued-at
Section titled “auth.jwt.require-issued-at”Require issued-at claim
When true, tokens without an ‘iat’ claim are rejected.
| Property | Value |
|---|---|
| Default | false |
| Override | SCRIBE_AUTH_JWT_REQUIRE_ISSUED_AT (optional) |
auth.jwt.require-issued-at = falseauth.jwt.require-issued-at = ${?SCRIBE_AUTH_JWT_REQUIRE_ISSUED_AT}auth.jwt.require-not-before
Section titled “auth.jwt.require-not-before”Require not-before claim
When true, tokens without an ‘nbf’ claim are rejected. The nbf claim is validated when present regardless of this setting.
| Property | Value |
|---|---|
| Default | false |
| Override | SCRIBE_AUTH_JWT_REQUIRE_NOT_BEFORE (optional) |
auth.jwt.require-not-before = falseauth.jwt.require-not-before = ${?SCRIBE_AUTH_JWT_REQUIRE_NOT_BEFORE}auth.ldap
Section titled “auth.ldap”LDAP Bind Authentication
Validates credentials by binding directly to an LDAP directory. No IdP needed—authentication happens against the upstream LDAP server. LDAP-only deployments do not require auth.providers or OIDC credentials.
How it works
Section titled “How it works”- Client sends: Authorization: Basic <base64(username:password)>
- Scribe searches for the user’s DN using a service account
- Scribe binds as that DN with the provided password
- On success, extracts attributes → AuthPrincipal
Connection settings
Section titled “Connection settings”Inherits from root ldap {} config:
- url (LDAP server)
- bind-dn, bind-password (service account used to search for user DNs)
- ssl (TLS settings)
The service account must have read access to search under auth.ldap.base. Override auth.ldap.url here only if auth uses a different server than sync.
Example
Section titled “Example”auth.ldap {base = "ou=users,dc=example,dc=com"filter = "(objectClass=inetOrgPerson)"bind-attribute = "uid"}User “alice” → search for (uid=alice) → bind as found DN
auth.ldap.attributes
Section titled “auth.ldap.attributes”Attribute Mapping
Maps LDAP attributes to identity claims. Defaults work for standard LDAP directories. See: LDAP Attributes
auth.ldap.attributes.email
Section titled “auth.ldap.attributes.email”Email address
| Property | Value |
|---|---|
| Default | mail |
| Override | SCRIBE_AUTH_LDAP_EMAIL_ATTR (optional) |
auth.ldap.attributes.email = mailauth.ldap.attributes.email = ${?SCRIBE_AUTH_LDAP_EMAIL_ATTR}auth.ldap.attributes.family-name
Section titled “auth.ldap.attributes.family-name”Last name
| Property | Value |
|---|---|
| Default | sn |
| Override | SCRIBE_AUTH_LDAP_FAMILY_NAME_ATTR (optional) |
auth.ldap.attributes.family-name = snauth.ldap.attributes.family-name = ${?SCRIBE_AUTH_LDAP_FAMILY_NAME_ATTR}auth.ldap.attributes.given-name
Section titled “auth.ldap.attributes.given-name”First name
| Property | Value |
|---|---|
| Default | givenName |
| Override | SCRIBE_AUTH_LDAP_GIVEN_NAME_ATTR (optional) |
auth.ldap.attributes.given-name = givenNameauth.ldap.attributes.given-name = ${?SCRIBE_AUTH_LDAP_GIVEN_NAME_ATTR}auth.ldap.attributes.name
Section titled “auth.ldap.attributes.name”Display name
| Property | Value |
|---|---|
| Default | cn |
| Override | SCRIBE_AUTH_LDAP_NAME_ATTR (optional) |
auth.ldap.attributes.name = cnauth.ldap.attributes.name = ${?SCRIBE_AUTH_LDAP_NAME_ATTR}auth.ldap.attributes.subject-id
Section titled “auth.ldap.attributes.subject-id”Subject identifier (user ID)
| Property | Value |
|---|---|
| Default | uid |
| Override | SCRIBE_AUTH_LDAP_SUBJECT_ATTR (optional) |
auth.ldap.attributes.subject-id = uidauth.ldap.attributes.subject-id = ${?SCRIBE_AUTH_LDAP_SUBJECT_ATTR}auth.ldap.base
Section titled “auth.ldap.base”Base DN for user search (REQUIRED to enable LDAP auth) The starting point for finding user entries. Setting this value enables LDAP authentication. If not set, LDAP auth is disabled even if other auth.ldap settings are present.
Example: “ou=users,dc=example,dc=com”
Priority: SCRIBE_AUTH_LDAP_BASE > config
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_LDAP_BASE (optional) |
auth.ldap.base = ${?SCRIBE_AUTH_LDAP_BASE}auth.ldap.bind-attribute
Section titled “auth.ldap.bind-attribute”Username attribute
LDAP attribute used to search for users during authentication. When a user logs in with “john.doe”, Scribe searches for entries where this attribute equals “john.doe”.
Two configuration styles:
Simple (single attribute for all users)
Section titled “Simple (single attribute for all users)”bind-attribute = uidAll logins search by uid. Good when users log in consistently (e.g., always username, never email).
Pattern-based (attribute depends on username format)
Section titled “Pattern-based (attribute depends on username format)”bind-attribute {mail = "*@*" # contains @ -> search by mailworkforceId = "/\\d+/" # digits only -> search by workforceIduid = true # everything else -> search by uid}Patterns are evaluated top-to-bottom. First match wins. Use this when users might log in with email, employee ID, or username.
Pattern syntax
Section titled “Pattern syntax”| Pattern | Type | Matches |
|---|---|---|
true | bool | Everything (use as fallback) |
"*" | glob | Everything (shorthand for true) |
*@* | glob | Contains @ (emails) |
admin* | glob | Starts with “admin” |
*.corp | glob | Ends with “.corp” |
/\d+/ | regex | Digits only (employee IDs) |
/[A-Z]{2}\d+/ | regex | Two letters + digits (e.g., AB123) |
/pattern/i | regex | Case-insensitive regex |
Glob patterns: * matches any characters, ? matches one character.
Globs are case-insensitive.
Regex patterns: Wrapped in /slashes/, optionally with flags. Flags: i (case-insensitive), m (multiline), s (dotall). Regex must match the entire username (anchored).
Common attributes
Section titled “Common attributes”| Attribute | Directory | Notes |
|---|---|---|
| uid | POSIX/OpenLDAP | Default, most common |
| sAMAccountName | Active Directory | Pre-Windows 2000 name |
| userPrincipalName | Active Directory | UPN format (user@domain) |
| Any | Email address | |
| cn | Any | Common name |
Priority: SCRIBE_AUTH_LDAP_BIND_ATTR > config
| Property | Value |
|---|---|
| Default | uid |
| Override | SCRIBE_AUTH_LDAP_BIND_ATTR (optional) |
auth.ldap.bind-attribute = uidauth.ldap.bind-attribute = ${?SCRIBE_AUTH_LDAP_BIND_ATTR}auth.ldap.cache
Section titled “auth.ldap.cache”Credential cache
Caches successful bind operations to reduce LDAP server load. The cache key is a hash of (username, password)—credentials are never stored in plaintext. Trade-offs:
- Reduces LDAP bind operations and latency
- Password changes take effect after TTL expires
- Locked/disabled accounts remain valid until TTL expires
Set ttl = 0 to disable caching.
| Property | Value |
|---|---|
| Default | ${auth.cache} |
Inherits from: auth.cache
auth.ldap.cache = ${auth.cache}auth.ldap.cache
Section titled “auth.ldap.cache”Credential cache
Caches successful bind operations to reduce LDAP server load. The cache key is a hash of (username, password)—credentials are never stored in plaintext. Trade-offs:
- Reduces LDAP bind operations and latency
- Password changes take effect after TTL expires
- Locked/disabled accounts remain valid until TTL expires
Set ttl = 0 to disable caching.
auth.ldap.cache.max-size
Section titled “auth.ldap.cache.max-size”Maximum cache entries (LRU eviction) Inherits from auth.cache.max-size if not set.
Priority: SCRIBE_AUTH_LDAP_CACHE_MAX_SIZE > auth.cache.max-size
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_LDAP_CACHE_MAX_SIZE (optional) |
auth.ldap.cache.max-size = ${?SCRIBE_AUTH_LDAP_CACHE_MAX_SIZE}auth.ldap.cache.ttl
Section titled “auth.ldap.cache.ttl”How long to cache successful authentications Inherits from auth.cache.ttl if not set.
Priority: SCRIBE_AUTH_LDAP_CACHE_TTL > auth.cache.ttl
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_LDAP_CACHE_TTL (optional) |
auth.ldap.cache.ttl = ${?SCRIBE_AUTH_LDAP_CACHE_TTL}auth.ldap.filter
Section titled “auth.ldap.filter”User filter (optional)
Restricts which entries can authenticate. Combined with the username lookup as: (&{filter}({bind-attribute}={username}))
Examples:
(objectClass=person) - Any person entry (employeeStatus=active) - Only active employees (!(accountLocked=TRUE)) - Exclude locked accounts (&(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))
- AD: enabled accounts only
Priority: SCRIBE_AUTH_LDAP_FILTER > config
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_LDAP_FILTER (optional) |
auth.ldap.filter = ${?SCRIBE_AUTH_LDAP_FILTER}auth.ldap.roles
Section titled “auth.ldap.roles”Role Extraction
Extracts roles from LDAP group memberships using pattern-based rules. Rules evaluate top-to-bottom; first match wins. Unmatched groups are excluded (explicit inclusion model). See: Roles and Scopes
Basic Setup
Section titled “Basic Setup”All groups become roles, extracted as RDN:
auth.ldap.roles {from = memberOf}Group DN cn=admins,ou=groups,dc=example,dc=com becomes role admins.
Rule-Based Extraction
Section titled “Rule-Based Extraction”auth.ldap.roles {from = memberOfrules = [# Exclude audit groups{ match = "*,ou=audit,*", format = false }# Strip ROLE_ prefix using regex capture{ match = "/cn=ROLE_([^,]+),/", format = "$1" }# Add prefix for team groups{ match = "*,ou=teams,*", format = "team:$rdn" }# Catch-all: use RDN{ match = "*", format = "$rdn" }]}Rule Fields
Section titled “Rule Fields”| Field | Description |
|---|---|
| match | Pattern: glob (*,ou=groups,*), regex (/cn=([^,]+)/), bool |
| format | Template with $rdn, $cn, $dn, $1, $2. Use false to exclude |
Format Substitutions
Section titled “Format Substitutions”| Variable | Value |
|---|---|
$rdn | First RDN value (e.g., admins) |
$cn, $ou, $uid, … | Any RDN component by attribute name |
$dn | Full DN |
$1, $2 | Regex capture groups (when match is a regex) |
Any RDN attribute type works: $sAMAccountName, $userPrincipalName, etc.
auth.ldap.roles.from
Section titled “auth.ldap.roles.from”Source attribute containing group DNs
Priority: SCRIBE_AUTH_LDAP_ROLES_ATTR > config
| Property | Value |
|---|---|
| Default | memberOf |
| Override | SCRIBE_AUTH_LDAP_ROLES_ATTR (optional) |
auth.ldap.roles.from = memberOfauth.ldap.roles.from = ${?SCRIBE_AUTH_LDAP_ROLES_ATTR}auth.ldap.scope
Section titled “auth.ldap.scope”Search scope
base - Only the base DN itself one - One level below base DN sub - Entire subtree (default)
Priority: SCRIBE_AUTH_LDAP_SCOPE > config
| Property | Value |
|---|---|
| Default | sub |
| Override | SCRIBE_AUTH_LDAP_SCOPE (optional) |
auth.ldap.scope = subauth.ldap.scope = ${?SCRIBE_AUTH_LDAP_SCOPE}auth.ldap.scopes
Section titled “auth.ldap.scopes”Scope Extraction
Extracts OAuth scopes from LDAP group memberships. Uses the same syntax as role extraction above.
Example
Section titled “Example”auth.ldap.scopes {from = memberOfrules = [{ match = "*,ou=scopes,*", format = "$rdn" }# No catch-all: only explicit scope groups included]}auth.ldap.scopes.from
Section titled “auth.ldap.scopes.from”Source attribute containing scope group DNs
Priority: SCRIBE_AUTH_LDAP_SCOPES_ATTR > config
| Property | Value |
|---|---|
| Default | memberOf |
| Override | SCRIBE_AUTH_LDAP_SCOPES_ATTR (optional) |
auth.ldap.scopes.from = memberOfauth.ldap.scopes.from = ${?SCRIBE_AUTH_LDAP_SCOPES_ATTR}auth.ldap.url
Section titled “auth.ldap.url”Connection settings fall back to root ldap {} config Override LDAP URL if auth uses a different server
Priority: SCRIBE_AUTH_LDAP_URL > config > ldap.url
url = ${ldap.url}
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_LDAP_URL (optional) |
Inherits from: ldap
auth.ldap.url = ${?SCRIBE_AUTH_LDAP_URL}auth.metadata
Section titled “auth.metadata”OAuth metadata endpoint
Configures the /.well-known/oauth-authorization-server endpoint (RFC 8414). MCP clients use this for OAuth discovery.
auth.metadata.provider
Section titled “auth.metadata.provider”Provider for metadata endpoint Which provider’s OIDC metadata to expose at /.well-known/oauth-authorization-server. The public-issuer (if set) replaces the internal issuer in the response. Auto-selection:
- If only one provider exists → auto-selected
- If multiple providers exist → must be explicitly set
Priority: SCRIBE_AUTH_METADATA_PROVIDER > config
| Property | Value |
|---|---|
| Default | "default" |
| Override | SCRIBE_AUTH_METADATA_PROVIDER (optional) |
auth.metadata.provider = ${?SCRIBE_AUTH_METADATA_PROVIDER}auth.methods
Section titled “auth.methods”Authentication methods
How clients can authenticate. Scribe validates credentials and creates an AuthPrincipal for downstream handlers.
| Method | Header | How it works |
|---|---|---|
| bearer | Authorization: Bearer <jwt> | JWT validated offline via JWKS |
| ropc | Authorization: Basic <base64> | Exchanges credentials with IdP for JWT |
| ldap | Authorization: Basic <base64> | Validates credentials via LDAP bind |
bearer (default, recommended)
Section titled “bearer (default, recommended)”Client sends a JWT. Scribe validates the signature against the IdP’s public keys (fetched from JWKS), checks claims, and extracts identity. No per-request calls to the IdP after initial key fetch.
ropc (Resource Owner Password Credentials)
Section titled “ropc (Resource Owner Password Credentials)”Client sends username:password via HTTP Basic auth. Scribe exchanges these with the IdP’s token endpoint for a JWT, then validates it normally. Useful for legacy apps or CLI tools that can’t do browser-based OAuth. Security: ROPC is deprecated in OAuth 2.1 because it exposes passwords to the client application. Use bearer tokens when possible.
ldap (LDAP bind)
Section titled “ldap (LDAP bind)”Client sends username:password via HTTP Basic auth. Scribe searches for the user’s DN using a service account, then binds as that user to verify the password. No IdP needed—authentication happens against LDAP directly. Multiple methods can be enabled. Bearer tokens are checked first. If no Bearer token, Basic auth is tried against ropc or ldap. Supports both array and comma-separated string formats:
methods = [bearer, basic]methods = "bearer,basic"SCRIBE_AUTH_METHODS=bearer,basicPriority: SCRIBE_AUTH_METHODS > config
| Property | Value |
|---|---|
| Default | [bearer] |
| Override | SCRIBE_AUTH_METHODS (optional) |
auth.methods = [bearer]auth.methods = ${?SCRIBE_AUTH_METHODS}auth.providers
Section titled “auth.providers”Named OIDC providers
Each key is the provider name, used for referencing in ropc.provider, bearer.provider, and metadata.provider. Multiple providers are supported for multi-tenant or migration scenarios. Provider names must be valid HOCON keys (alphanumeric, hyphens, underscores). Example with multiple providers:
providers {internal {issuer = "https://internal-auth.example.com"audiences = ["scribe"]}partner {issuer = "https://partner-auth.example.com"audiences = ["partner-access"]}}auth.providers.default
Section titled “auth.providers.default”Default provider (named “default” for simplicity) The name “default” is used when no explicit provider is configured for ropc.provider or bearer.provider and only one provider exists.
auth.providers.default.audiences
Section titled “auth.providers.default.audiences”Expected audiences (validated against ‘aud’ claim) The token’s ‘aud’ claim must contain at least one of these values. Supports HOCON array or comma-separated string.
Priority: SCRIBE_AUTH_AUDIENCES > config
| Property | Value |
|---|---|
| Default | ["scribe"] |
| Override | SCRIBE_AUTH_AUDIENCES (optional) |
auth.providers.default.audiences = ["scribe"]auth.providers.default.audiences = ${?SCRIBE_AUTH_AUDIENCES}auth.providers.default.client-id
Section titled “auth.providers.default.client-id”OAuth client credentials (required for ROPC or opaque token introspection) If this provider is used for ropc.provider or bearer.provider, both client-id and client-secret must be set. Validated at startup. Must be a confidential client configured in your IdP with:
- For ROPC: “Resource Owner Password” grant enabled
- For bearer introspection: Token Introspection permission
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_CLIENT_ID (optional) |
auth.providers.default.client-id = ${?SCRIBE_AUTH_CLIENT_ID}auth.providers.default.client-secret
Section titled “auth.providers.default.client-secret”| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_CLIENT_SECRET (optional) |
auth.providers.default.client-secret = ${?SCRIBE_AUTH_CLIENT_SECRET}auth.providers.default.enabled
Section titled “auth.providers.default.enabled”Whether this provider is active. Uses same env var as auth.enabled - when auth is enabled globally, the default provider is also enabled (if issuer is configured). Set explicitly to override.
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_ENABLED (optional) |
auth.providers.default.enabled = ${?SCRIBE_AUTH_ENABLED}auth.providers.default.issuer
Section titled “auth.providers.default.issuer”Issuer URL (internal - used by Scribe for JWKS fetch and validation) This is the URL Scribe uses to fetch the OIDC discovery document and validate the ‘iss’ claim in tokens.
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_ISSUER (optional) |
auth.providers.default.issuer = ${?SCRIBE_AUTH_ISSUER}auth.providers.default.jwks-refresh-interval
Section titled “auth.providers.default.jwks-refresh-interval”JWKS refresh interval (fallback when IdP sends no Cache-Control headers) How often to check for updated signing keys. HTTP caching headers (Cache-Control, ETag) from the IdP take precedence when present.
| Property | Value |
|---|---|
| Default | 5m |
| Override | SCRIBE_AUTH_JWKS_REFRESH_INTERVAL (optional) |
auth.providers.default.jwks-refresh-interval = 5mauth.providers.default.jwks-refresh-interval = ${?SCRIBE_AUTH_JWKS_REFRESH_INTERVAL}auth.providers.default.public-issuer
Section titled “auth.providers.default.public-issuer”Public issuer URL (external - returned to clients for OAuth flows) If your IdP is behind a reverse proxy or load balancer, clients may need a different URL than Scribe uses internally. If not set, falls back to the issuer URL.
| Property | Value |
|---|---|
| Default | "https://auth.example.com/application/o/scribe/" |
| Override | SCRIBE_AUTH_PUBLIC_ISSUER (optional) |
auth.providers.default.public-issuer = ${?SCRIBE_AUTH_PUBLIC_ISSUER}auth.ropc
Section titled “auth.ropc”ROPC (Resource Owner Password Credentials)
Exchanges username/password for an access token via the IdP’s token endpoint. The returned JWT is validated like any bearer token.
How it works
Section titled “How it works”- Client sends: Authorization: Basic <base64(username:password)>
- Scribe POSTs to token endpoint with grant_type=password
- IdP returns access_token (JWT)
- Scribe validates JWT via JWKS → AuthPrincipal
When to use
Section titled “When to use”- Legacy applications that can’t implement OAuth flows
- CLI tools where browser redirects aren’t practical
- Migration path from Basic auth to OAuth
Security notes
Section titled “Security notes”ROPC is deprecated in OAuth 2.1. It exposes user credentials to Scribe (the client), which violates the principle that only the IdP should see passwords. Prefer bearer tokens with proper OAuth flows when possible. Credentials are never stored—they’re exchanged immediately and discarded.
auth.ropc.cache
Section titled “auth.ropc.cache”Credential cache
Caches successful authentications to reduce IdP calls. The cache key is a hash of (username, password)—credentials are never stored in plaintext. Trade-offs:
- Reduces IdP load and latency for repeated requests
- Password changes take effect after TTL expires
- Revoked users remain valid until TTL expires
Set ttl = 0 to disable caching.
| Property | Value |
|---|---|
| Default | ${auth.cache} |
Inherits from: auth.cache
auth.ropc.cache = ${auth.cache}auth.ropc.cache
Section titled “auth.ropc.cache”Credential cache
Caches successful authentications to reduce IdP calls. The cache key is a hash of (username, password)—credentials are never stored in plaintext. Trade-offs:
- Reduces IdP load and latency for repeated requests
- Password changes take effect after TTL expires
- Revoked users remain valid until TTL expires
Set ttl = 0 to disable caching.
auth.ropc.cache.max-size
Section titled “auth.ropc.cache.max-size”Maximum cache entries (LRU eviction) Size based on expected concurrent users. Each entry is ~500 bytes (principal + metadata). Inherits from auth.cache.max-size if not set.
Priority: SCRIBE_AUTH_ROPC_CACHE_MAX_SIZE > auth.cache.max-size
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_ROPC_CACHE_MAX_SIZE (optional) |
auth.ropc.cache.max-size = ${?SCRIBE_AUTH_ROPC_CACHE_MAX_SIZE}auth.ropc.cache.ttl
Section titled “auth.ropc.cache.ttl”How long to cache successful authentications Short values (1-5m) balance performance with security. Longer values reduce IdP load but delay credential changes. Inherits from auth.cache.ttl if not set.
Priority: SCRIBE_AUTH_ROPC_CACHE_TTL > auth.cache.ttl
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_ROPC_CACHE_TTL (optional) |
auth.ropc.cache.ttl = ${?SCRIBE_AUTH_ROPC_CACHE_TTL}auth.ropc.provider
Section titled “auth.ropc.provider”Provider name for ROPC authentication
References a provider from auth.providers by name. The provider must have client-id and client-secret configured. Auto-selection:
- If only one provider has client credentials → auto-selected
- If multiple providers have credentials → must be explicitly set
The token_endpoint is discovered from the provider’s OIDC metadata.
Priority: SCRIBE_AUTH_ROPC_PROVIDER > config
| Property | Value |
|---|---|
| Default | "default" |
| Override | SCRIBE_AUTH_ROPC_PROVIDER (optional) |
auth.ropc.provider = ${?SCRIBE_AUTH_ROPC_PROVIDER}auth.ropc.scopes
Section titled “auth.ropc.scopes”OAuth scopes to request Space-separated list of scopes. Most IdPs require “openid” for JWT tokens.
Priority: SCRIBE_AUTH_ROPC_SCOPES > config
| Property | Value |
|---|---|
| Default | "openid profile email" |
| Override | SCRIBE_AUTH_ROPC_SCOPES (optional) |
auth.ropc.scopes = "openid profile email"auth.ropc.scopes = ${?SCRIBE_AUTH_ROPC_SCOPES}auth.rules
Section titled “auth.rules”Access Rules (ABAC)
Fine-grained access control using attribute-based policies. Rules are evaluated top-to-bottom; first matching rule wins (firewall style). If no rule matches, access is denied (secure by default).
Quick Start
Section titled “Quick Start”auth.rules = [# Allow admin role everywhere{ action = allow, where = "subject.roles = admin" }# Allow anonymous on GraphQL schema introspection{ action = allow, where = "subject.anonymous = true and request.channel = graphql and request.operation = schema" }# Allow authenticated users to read{ action = allow, where = "subject.authenticated = true and request.operation in [search, lookup]" }# Deny writes outside business hours{ action = deny, where = "request.operation in [modify, add, delete] and (time.hour < 9 or time.hour >= 17)" }# Catch-all deny (explicit, but also the default if no rule matches){ action = deny }]Rule Syntax
Section titled “Rule Syntax”| Field | Required | Description |
|---|---|---|
action | Yes | allow or deny |
where | No | Filter expression (matches all if omitted) |
id | No | Rule identifier for telemetry/logging (default: Rule #N) |
The where field accepts any filter syntax. We recommend FleX for readability. See Filters Reference for syntax details.
Example with IDs:
auth.rules = [{ id = "admin-bypass", action = allow, where = "subject.roles = admin" }{ id = "deny-anon", action = deny, where = "subject.anonymous = true" }{ action = allow } # id defaults to "Rule #3"]Available Attributes
Section titled “Available Attributes”| Namespace | Attribute | Example |
|---|---|---|
| subject.* | anonymous | true (no credentials) |
| subject.* | authenticated | true (valid credentials) |
| subject.* | principal.id | ”user@example.com” |
| subject.* | roles | ”admin”, “reader” |
| subject.* | scopes | ”read:users” |
| service.* | principal.id | ”hr-portal” (service client) |
| request.* | channel | ”ldap”, “rest”, “graphql” |
| request.* | operation | ”search”, “lookup”, “modify” |
| request.* | method | ”GET”, “POST” (HTTP only) |
| request.* | path | ”/api/entries/users” |
| request.* | host | ”api.example.com” |
| request.* | client.ip | ”192.168.1.100” |
| request.* | secure | true (TLS active) |
| request.* | protocol | ”http/1.1”, “http/2” |
| time.* | hour | 14 (0-23) |
| time.* | dayOfWeek | ”monday”…“sunday” |
| time.* | weekend | true (Saturday or Sunday) |
| time.* | date | ”2026-01-29” |
| env.* | (any env var) | env.FEATURE_BETA = “true” |
Examples
Section titled “Examples”Allow public health checks:
{ action = allow, where = "request.path startswith /health" }Restrict by IP:
{ action = allow, where = "request.client.ip startswith 10.0." }Feature flag from environment:
{ action = allow, where = "env.FEATURE_BETA = true and subject.roles = beta-tester" }Default: allow any authenticated (non-anonymous) access.
Anonymous requests are denied (secure by default). Add rules before the default to customize access:
auth.rules = [{ id = "admin-bypass", action = allow, where = "subject.roles = admin" }] ${auth.rules}| Property | Value |
|---|---|
| Default | [{, id, ...] |
auth.rules = [ { id = "authenticated-access", action = allow, where = "subject.anonymous = false" } ]auth.session
Section titled “auth.session”Browser Session (Cookie + Login)
Browser session management for authenticated users. Provides:
- Cookie-based authentication (encrypted session cookie)
- Login endpoints (/auth/login, /auth/callback, /auth/logout, /auth/whoami)
Flow: Browser login → encrypted cookie → subsequent requests authenticated Auto-detection:
- Session is auto-enabled when any browser UI is active (site, REST Scalar,
GraphQL GraphiQL, or Observe dashboard)
- Sockets and allowed return paths are auto-detected from enabled UIs
Security:
- Cookie uses __Host- prefix (requires Secure, same-origin)
- HttpOnly prevents JavaScript access
- SameSite=Lax prevents CSRF for safe methods
- JWE encryption (A256KW + A256GCM) protects contents
Endpoints (registered on sockets where UIs are enabled): GET /auth/config - Auth configuration for clients GET /auth/login - OIDC redirect or login form
POST /auth/login - Username/password authentication
GET /auth/callback - OIDC callback handler POST /auth/logout - Clears auth cookie GET /auth/whoami - Current authenticated identity
auth.session.cookie-keys
Section titled “auth.session.cookie-keys”Encryption keys for cookie JWE
Array of 32-byte keys for A256KW. A random key is selected for each encryption; all keys are tried for decryption (enables zero-downtime key rotation). Format: base64-encoded keys, comma-separated or as array If not set, keys are derived from a SHA-256 chain seeded by the auth config hash. The derived keys are 256-bit AES keys - cryptographically equivalent to explicit keys. They’re stable across restarts (same config = same keys), so cookies survive application restarts. When to set explicit keys:
- Multiple instances with different configs (load-balanced deployments)
- You rotate config frequently and want cookies to survive
- Compliance requires documented key management
To generate a key: openssl rand -base64 32 Key rotation procedure (zero-downtime):
- Add new key to end of array (old keys remain for decryption)
cookie-keys = ["old-key-1", "new-key-1"]- Restart application - new sessions use new key, old cookies still work
- Wait for session-ttl (default 10h) for all old cookies to expire
- Remove old keys from array
cookie-keys = ["new-key-1"]- Restart application
Priority: SCRIBE_AUTH_SESSION_KEYS > config
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_SESSION_KEYS (optional) |
auth.session.cookie-keys = ${?SCRIBE_AUTH_SESSION_KEYS}auth.session.cookie-name
Section titled “auth.session.cookie-name”Cookie name
The base name for the session cookie. In production (HTTPS), the cookie uses __Host- prefix for security (e.g., __Host-scribe-auth).
Default: “scribe-auth”
| Property | Value |
|---|---|
| Default | "scribe-auth" |
auth.session.cookie-name = "scribe-auth"auth.session.enabled
Section titled “auth.session.enabled”Enable browser session management When enabled, cookie authentication and /auth/* routes are active. Browser UI gating (/ui, /observe, etc.) requires both auth.enabled and session.enabled; when auth.enabled=false, /ui routes remain accessible.
Default behavior (auto):
- Enabled when any browser UI is active (site, REST Scalar, GraphQL
GraphiQL, or Observe dashboard)
- Disabled otherwise
Set explicitly to override auto-detection:
enabled = true # Always enable browser sessionsenabled = false # Never enable browser sessions (header-based auth only)Priority: SCRIBE_AUTH_SESSION_ENABLED > config > auto-detect
enabled = auto # (default: auto-detect based on UI settings)| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_SESSION_ENABLED (optional) |
auth.session.enabled = ${?SCRIBE_AUTH_SESSION_ENABLED}auth.session.method
Section titled “auth.session.method”Login method
Which authentication method to use for browser login. Defaults to first method in auth.methods if not specified. Options: bearer - Redirect to OIDC provider (recommended) ldap - Username/password form → LDAP bind ropc - Username/password form → OIDC token exchange
Fallback behavior:
- If auth.methods = [ldap], browser login uses LDAP form
- If auth.methods = [bearer, ldap], browser uses OIDC redirect
- To use OIDC when LDAP is first, set method = bearer explicitly
Priority: SCRIBE_AUTH_SESSION_METHOD > config > auth.methods[0]
| Property | Value |
|---|---|
| Override | SCRIBE_AUTH_SESSION_METHOD (optional) |
auth.session.method = ${?SCRIBE_AUTH_SESSION_METHOD}auth.session.scopes
Section titled “auth.session.scopes”OAuth scopes to request Space-separated list of scopes for OIDC authorization. Most IdPs require “openid” for JWT tokens.
Default: “openid profile email”
| Property | Value |
|---|---|
| Default | "openid profile email" |
| Override | SCRIBE_AUTH_SESSION_SCOPES (optional) |
auth.session.scopes = "openid profile email"auth.session.scopes = ${?SCRIBE_AUTH_SESSION_SCOPES}auth.session.session-ttl
Section titled “auth.session.session-ttl”Session TTL
How long the session cookie remains valid. After expiry, user must re-authenticate.
Default: 10 hours
| Property | Value |
|---|---|
| Default | 10h |
| Override | SCRIBE_AUTH_SESSION_TTL (optional) |
auth.session.session-ttl = 10hauth.session.session-ttl = ${?SCRIBE_AUTH_SESSION_TTL}auth.session.state-ttl
Section titled “auth.session.state-ttl”OIDC state TTL
How long the OIDC state parameter is valid during authorization flow. This protects against replay attacks by limiting the time window for a callback to be valid after initiating login. Only applies when method = bearer (OIDC flow).
Relationship with session-ttl:
- state-ttl (default 5m): How long user has to complete login at IdP
- session-ttl (default 10h): How long session remains valid after login
- state-ttl should be much shorter than session-ttl
- If user takes longer than state-ttl at IdP, they must restart login
Default: 5 minutes
| Property | Value |
|---|---|
| Default | 5m |
| Override | SCRIBE_AUTH_SESSION_STATE_TTL (optional) |
auth.session.state-ttl = 5mauth.session.state-ttl = ${?SCRIBE_AUTH_SESSION_STATE_TTL}