ADR-006: SASL mechanism scope (PLAIN always; EXTERNAL mTLS-conditional)
Date: 2025-01-20 (Phase 1) · revised 2025-07-27 (TICKET-079) Status: Superseded (TICKET-079) Supersedes: — Superseded by: —
Context
IRCv3 SASL (CAP sasl) lets a client authenticate during registration,
before the welcome sequence. The server advertises supported mechanisms in
the sasl cap value (e.g. sasl=PLAIN,EXTERNAL). Each mechanism has
different infrastructure requirements:
- PLAIN: the client sends
AUTHENTICATE PLAIN, then a base64 payload of\0username\0password. The server verifies the credentials against anAccountStore. No client certificates, no TLS-passthrough — works over any transport, including wss with edge-terminated TLS. - EXTERNAL: the server authenticates the client based on an external identity, typically a client certificate verified via mutual TLS (mTLS). This requires the TLS handshake to present the client cert to the application layer. On serverless platforms with edge-terminated TLS (Cloudflare, API Gateway), the application does not see the raw client cert — it must be configured at the edge (API Shield mTLS, custom-domain mTLS trust store). That infrastructure does not exist in v1 (ADR-005: wss-only, no raw TCP; mTLS passthrough is Phase 7, TICKET-054).
Decision
The server ships SASL PLAIN as the always-available mechanism and SASL EXTERNAL as an mTLS-conditional mechanism:
- When no
MtlsIdentityProvideris bound to the connection (the default for non-mTLS deployments), thesaslcap advertisesPLAINonly andAUTHENTICATE EXTERNALreturns908 ERR_SASLMECHSlistingPLAINas the sole available mechanism. - When an
MtlsIdentityProvideris bound (the operator configured mTLS at the edge — CF API Shield mTLS or AWS custom-domain mTLS), thesaslcap advertisesPLAIN,EXTERNALandAUTHENTICATE EXTERNALproceeds through the full handshake: the verified client-cert subject is resolved via the provider and verified against theAccountStore.
This means EXTERNAL advertisement and functionality are driven entirely by
whether mTLS is configured — operators enable EXTERNAL by configuring edge
mTLS, with no code change required. The saslCapValue(hasMtls) helper in
caps/capabilities.ts and the supportedMechs(ctx) helper in
commands/sasl.ts are the single sources of truth for the advertised value.
AccountStore is a port in irc-core consumed by the SASL reducer. The
verify(mechanism, payload) method is synchronous — adapters pre-load
credentials at boot so the reducer stays pure (ADR-001). PLAIN verification
decodes the base64 payload, extracts username + password, and matches
against the store. EXTERNAL verification resolves the cert subject via the
MtlsIdentityProvider and matches it against the store.
Consequences
Positive:
- PLAIN works end-to-end with no special infrastructure. The
AccountStoreport abstracts the credential store; adapters seed it from config (in-memory), DynamoDB (AWS), or a persistent backend (D1/DO for CF, TICKET-093). - EXTERNAL is fully functional when mTLS is configured (TICKET-054) and correctly unadvertised when it is not — clients never waste a round-trip on a mechanism that cannot succeed.
- The mTLS-conditional advertisement is driven by the
MtlsIdentityProviderbinding, so operators enable EXTERNAL by configuring edge mTLS with no code change. - The synchronous
AccountStore.verify()keeps the reducer pure and deterministic. Async credential lookups (DynamoDB) are resolved at cold start or via pre-loading, not inside the reducer.
Negative:
- Deployments without mTLS (the common case) cannot offer EXTERNAL. This is inherent — EXTERNAL requires client-certificate verification at the TLS layer, which is only available via edge mTLS configuration.
- PLAIN sends the password base64-encoded (not encrypted) inside the wss frame. wss provides the TLS layer, so the password is encrypted in transit — but PLAIN is still weaker than EXTERNAL (no mutual auth).
- The synchronous
AccountStorecontract means the adapter must pre-load all credentials. For deployments with many accounts, cold-start latency grows linearly with account count. A lazy/async lookup variant is a documented follow-up but would require an async-reducer or effect-based credential query — a significant architecture change.
References
- PLAN §3 — "sasl (mechanisms: PLAIN, EXTERNAL reserved for later with mTLS)"
- PLAN §9 — SASL mechanism scope
- TICKET-026 — SASL reducer implementation
- TICKET-071 — AccountStore wiring into ConnectionActor
- TICKET-072 — AWS DynamoDB AccountStore (scrypt hashing)
- TICKET-079 — SASL EXTERNAL: implement or stop advertising (interim decision)
- TICKET-054 — mTLS support (Phase 7, enables functional EXTERNAL)
packages/irc-core/src/commands/sasl.ts—authenticateReducerpackages/irc-core/src/ports.ts—AccountStoreport