Files
preauth/config/services.yaml
T
lyra ffb824c652 Add the passkey listener at priority 70
Priority 70 sits after RejectListener (77) and before LoginListener (66), and
both bounds are load-bearing:

- After 77 so a rate-limited IP never reaches a ceremony. Passkeys cannot be
  used to sidestep a lockout (D3), which is the point of the reviewer's third
  clarification.
- Before 66 because LoginListener treats any POST to the auth subdomain as a
  login attempt. A ceremony finish body has no username/totp, so Payload::load()
  returns null and the request would be scored as a failed login, burning a
  rate-limit token for every legitimate passkey login.

Verified in the live container rather than assumed: debug:event-dispatcher
confirms 77 -> 70 -> 66.

Other properties asserted by tests: every header-bearing request gets a JSON
response so fetch() callers never receive HTML; registration identity comes from
the live session, never the request body; a failed ceremony is indistinguishable
from a wrong TOTP code and spends the same shared budget; and begin is bounded
by a separate resource guard that deliberately does not consume failure budget.

The listener also marks its responses so SecurityHeadersListener can apply
no-store: these are the only browser-facing 2xx this application produces, since
the auth subdomain has no forward_auth in front of it. CSP gains
publickey-credentials-get/-create only when passkeys are available, so the
unavailable case stays byte-identical to before.
2026-09-27 10:50:09 +00:00

150 lines
7.1 KiB
YAML

# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.
# See also https://symfony.com/doc/current/service_container/import.html
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html
# #use-parameters-for-application-configuration
parameters:
# --- main options ---
# URI containing secret and config for TOTP, which determines the token to login
# app will generate one, if not provided, but you should copy it to your .env file
# format: "otpauth://totp/<label>?secret=<secret-key>"
env(TOTP_URI): '' # blank to have the app generate one at random
# how long will someone stay logged in, measured in seconds, zero for DEFAULT
env(COOKIE_TTL): '2592000' # default 30 days
# Enable optional redirection to a dedicated authentication subdomain
env(SUBDOMAIN_REDIRECT): '0' # boolean, 1 to enable
# The subdomain (e.g., auth.example.com) to which unauthenticated users are redirected
env(AUTH_SUBDOMAIN): ''
# --- extra options ---
# how long do we allow all traffic from an ip address after successful login
# could be useful if you have a system which does not handle cookies
env(IP_TTL): '0' # default disabled, time in seconds
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
env(TEAPOT): '1' # boolean
# --- remote-user header ---
# Controls the value sent in the Remote-User header on successful auth.
# session: the session id (default, backward-compatible)
# static: a fixed string (set via REMOTE_USER_STATIC)
# mapped: look up session id in REMOTE_USER_MAP (format: id1:user1,id2:user2)
# none: do not send the Remote-User header at all
env(REMOTE_USER): 'session'
env(REMOTE_USER_STATIC): 'authenticated'
env(REMOTE_USER_MAP): ''
# --- rate limiting ---
# Note: rate limiting can *NOT* be disabled, but you could allow hundreds of logins a second
# rate limiting, default is the lower of 2 per 30 seconds or 10 per hour
env(BURST_COUNT): 2 # 2 per 30 seconds
env(BURST_TIME): 30 # seconds
env(UPPER_COUNT): 10 # 10 per hour
env(UPPER_TIME): 3600 # seconds (1 hour)
# --- public access (rate-limited, no auth required) ---
# Comma-separated path patterns for public access. Wildcards: * (single
# segment), ** (cross segments). Optional host prefix: host.com/path/**
# When empty (default), the feature is fully disabled.
env(PUBLIC_PATHS): ''
env(PUBLIC_BURST_COUNT): 100 # max requests per burst window per IP
env(PUBLIC_BURST_TIME): 60 # burst window in seconds
env(PUBLIC_UPPER_COUNT): 500 # max requests per sustained window per IP
env(PUBLIC_UPPER_TIME): 3600 # sustained window in seconds (1 hour)
# --- passkey authentication ---
# Requires central auth (SUBDOMAIN_REDIRECT=1 + AUTH_SUBDOMAIN) and HTTPS.
# Enabling this without central auth makes the container fail at cache warmup
# rather than offering a feature that cannot work.
env(PASSKEY_ENABLED): '0' # boolean, 1 to offer passkeys on the auth subdomain
env(PASSKEY_RP_NAME): '' # blank to use TITLE
env(PASSKEY_USER_VERIFICATION): 'required' # required|preferred|discouraged
env(PASSKEY_TIMEOUT): '60000' # milliseconds
# Extra options, custom labels
env(PASSKEY_BUTTON_NAME): 'Sign in with a passkey'
env(PASSKEY_REGISTER_NAME): 'Register this device as a passkey'
# bounds how many ceremonies one caller can start (resource guard, not the
# login budget — see config/packages/rate_limiter.yaml)
env(PASSKEY_BEGIN_BURST_COUNT): 30
env(PASSKEY_BEGIN_BURST_TIME): 60
# --- styling options ---
env(TITLE): 'Pre-Authentication System'
env(BG_COLOR): '#029386' # teal
env(FG_COLOR): '#ffffff' # white
env(ERROR_COLOR): '#ffb16d' # apricot (light orange)
env(ID_NAME): 'Session ID'
env(TOKEN_NAME): 'Authentication Token'
env(SUBMIT_NAME): 'Submit'
env(ERROR_MESSAGE): 'Unsuccessful login attempt'
# title and message to use on block page, if teapot is true
env(TEAPOT_TITLE): "I'm a teapot"
env(TEAPOT_MESSAGE): 'I refuse to brew coffee'
# title and message to use on block page, if teapot is false
env(TOO_MANY_TITLE): 'Too many requests'
env(TOO_MANY_MESSAGE): 'Try again later'
# --- debug options ---
env(SHELL_VERBOSITY): '0' # set to 3 to log debug
# --- application variables ---
app.totp_uri: '%env(TOTP_URI)%'
app.cookie_ttl: '%env(int:COOKIE_TTL)%'
app.subdomain_redirect: '%env(bool:SUBDOMAIN_REDIRECT)%'
app.auth_subdomain: '%env(AUTH_SUBDOMAIN)%'
app.ip_ttl: '%env(int:IP_TTL)%'
app.teapot: '%env(bool:TEAPOT)%'
app.remote_user: '%env(REMOTE_USER)%'
app.remote_user_static: '%env(REMOTE_USER_STATIC)%'
app.remote_user_map: '%env(REMOTE_USER_MAP)%'
app.public_paths: '%env(PUBLIC_PATHS)%'
app.public_burst_count: '%env(int:PUBLIC_BURST_COUNT)%'
app.public_burst_time: '%env(int:PUBLIC_BURST_TIME)%'
app.public_upper_count: '%env(int:PUBLIC_UPPER_COUNT)%'
app.public_upper_time: '%env(int:PUBLIC_UPPER_TIME)%'
app.error_message: '%env(ERROR_MESSAGE)%'
app.teapot_title: '%env(TEAPOT_TITLE)%'
app.too_many_title: '%env(TOO_MANY_TITLE)%'
app.title: '%env(TITLE)%'
app.passkey_enabled: '%env(bool:PASSKEY_ENABLED)%'
app.passkey_rp_name: '%env(PASSKEY_RP_NAME)%'
app.passkey_user_verification: '%env(PASSKEY_USER_VERIFICATION)%'
app.passkey_timeout: '%env(int:PASSKEY_TIMEOUT)%'
app.passkey_button_name: '%env(PASSKEY_BUTTON_NAME)%'
app.passkey_register_name: '%env(PASSKEY_REGISTER_NAME)%'
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services.
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones
# the boot-time passkey configuration check runs during `cache:warmup`, so a
# misconfigured deployment fails to start instead of failing in a browser
App\Service\PasskeyPolicyInterface: '@App\Service\PasskeyPolicy'
App\Service\PasskeyInterface: '@App\Service\PasskeyManager'
App\Service\PasskeyCeremonyStoreInterface: '@App\Service\PasskeyCeremonyStore'
App\Service\PasskeyCredentialStoreInterface: '@App\Service\PasskeyCredentialStore'
App\Service\SessionIssuerInterface: '@App\Service\SessionIssuer'
# the ceremony factory takes no constructor arguments and holds no state, so
# it is built once and shared rather than re-created per ceremony
App\Service\PasskeyCeremonyFactory: ~