Governance (Tier 2):
- Advance ADR-022 and ADR-023 from Proposed to Accepted (specs already
depend on their types as source of truth)
- Amend ADR-015: mark Decision 3 and Assumption 6 as superseded by ADR-022;
update handler_identity type to CompositionAuthority
- Amend ADR-002: note handle() signature revised by ADR-007 (BiStream → Connection)
- Amend ADR-004: note 'enrich/replace' AuthContext language superseded by
ADR-011's immutability model; update to describe set_identity on Connection
- Update main README ADR table to show ADR-022/023 as Accepted
Spec-ADR consistency (Tier 3):
- Add abort_policy: AbortPolicy field to OperationContext struct (ADR-016
Decision 6 mandated this but the spec omitted it)
- Define AbortPolicy enum (AbortDependents | ContinueRunning) with Default impl
- Add abort_policy to build_root_context and LocalOperationEnv::invoke()
- Define the OperationEnv trait explicitly with invoke() and
invoke_with_policy() methods (was referenced as 'must remain a trait'
but never defined)
- Specify From<StreamError> for HandlerError impl with exact variant mapping
- Add Connection::from_quinn() / from_iroh() constructors (was referenced
as Connection::new() but never defined)
- Remove undefined CertAuthorityEntry placeholder from AuthPolicy v1 (will
be added additively when alknet-ssh lands)
- Fix config.md key-differences table: rate limits are in DynamicConfig,
not StaticConfig
Mechanical fixes (Tier 1):
- overview.md: 'closes the QUIC stream' → 'closes the connection' (stale
from pre-ADR-007 model)
- overview.md: OQ-04 entry updated from stale 'defer to implementation'
to 'resolved: static at startup'
- mnemonic-derivation.md: remove duplicate helper functions block (incomplete
first copy, complete second copy)
- ADR-003: add iroh (feature-gated) to alknet-core dependency list, added
by ADR-010
- ADR-021: fix ambiguous 'W1 drift issue from the vault review' cross-reference
- ADR-022: rephrase FromCall 'leaf locally' to 'leaf in the local registry'
- ADR-017: add error_schemas to from_call mirror list and services/schema
step (inconsistency with ADR-023)
- ADR-016: fix self-referential citation ('ADR-016 Assumption 5' → 'Assumption 5')
- Add ScopedOperationEnv::empty(), allows(), new() and
CompositionAuthority::none(), new() impl blocks (referenced but undefined)
- Add call.completed clarification for non-subscription calls
- Add services/schema leading-slash normalization note
- Crate README ADR tables: add missing ADR-013 (call), ADR-015 (core),
ADR-006 + ADR-010 (vault)
- Vault README: add consolidated 'Known Source Drift' table tracking all
four drift items (OsRng, unwrap, CURRENT_KEY_VERSION, spawn bug) in one
place, including the two previously missing from README
76 lines
5.7 KiB
Markdown
76 lines
5.7 KiB
Markdown
# ADR-004: Auth as Shared Core (IdentityProvider)
|
||
|
||
## Status
|
||
|
||
Accepted
|
||
|
||
## Context
|
||
|
||
The previous architecture had authentication spread across multiple layers: `CredentialProvider` with four phases (A–D), `AuthProtocol` as an irpc service, `server_auth` and `client_auth` as separate modules, and `IdentityProvider` as a trait in alknet-core. Different interface types presented credentials differently — SSH used key fingerprints, HTTP used Bearer tokens, DNS used query labels — but the resolution was ad-hoc and tied to the three-layer model.
|
||
|
||
The ALPN dispatch model simplifies this: every handler receives the same `AuthContext`, but the credential extraction (how a handler learns who the peer is) differs per ALPN. The resolution (turning a credential into an `Identity`) should be shared across all handlers.
|
||
|
||
## Decision
|
||
|
||
> **Note**: The original text of this decision described the handler
|
||
> "enriching or replacing" the `AuthContext`. This was superseded by
|
||
> ADR-011, which made `AuthContext` immutable in `handle()` (passed as
|
||
> `&AuthContext`). Handlers resolve identity into a local variable and
|
||
> store it on `Connection` via `set_identity()`. The text below has been
|
||
> updated to reflect the ADR-011 model.
|
||
|
||
Authentication and identity resolution live in `alknet-core` as shared infrastructure. Each handler presents credentials differently, but all resolve through the same `IdentityProvider`:
|
||
|
||
```rust
|
||
pub trait IdentityProvider: Send + Sync + 'static {
|
||
fn resolve_from_fingerprint(&self, fingerprint: &str) -> Option<Identity>;
|
||
fn resolve_from_token(&self, token: &AuthToken) -> Option<Identity>;
|
||
}
|
||
```
|
||
|
||
Credential presentation per handler:
|
||
|
||
| Handler | Credential presentation | Resolves via |
|
||
|---------|------------------------|-------------|
|
||
| SshAdapter | SSH public key handshake | `resolve_from_fingerprint()` |
|
||
| CallAdapter | AuthToken in first frame | `resolve_from_token()` |
|
||
| HttpAdapter | `Authorization: Bearer` header | `resolve_from_token()` |
|
||
| DnsAdapter | AuthToken in query labels | `resolve_from_token()` |
|
||
| WebTransportAdapter | AuthToken in CONNECT headers | `resolve_from_token()` |
|
||
| GitAdapter | Signed push certificate | `resolve_from_fingerprint()` |
|
||
|
||
Auth resolution is **hybrid** — the endpoint resolves what it can, and handlers resolve what they must:
|
||
|
||
1. **Endpoint-level resolution** (before `handle()` is called): If the TLS handshake provides a client certificate, the endpoint resolves the fingerprint to an `Identity` and passes it in `AuthContext`. This is the case for SSH (where the key exchange happens at the protocol level, but the TLS layer may also provide information).
|
||
|
||
2. **Handler-level resolution** (inside `handle()`): For protocols that carry credentials in application frames (AuthToken in the first call frame, Bearer header in HTTP), the handler extracts the credential from the stream and calls `IdentityProvider` to resolve it. The handler then resolves the `Identity` into a local variable and stores it on the `Connection` via `set_identity()` for observability — it does **not** mutate the `AuthContext` (which is passed as `&AuthContext`, an immutable reference — see ADR-011). The per-request identity (for ACL) is resolved separately by the `CallAdapter` at `call.requested` time.
|
||
|
||
The `AuthContext` passed to `handle()` may be partial — containing only transport-level information if no TLS client certificate was provided. Handlers must not assume `AuthContext` contains a fully resolved `Identity`. Each handler knows its own credential extraction protocol and is responsible for completing authentication.
|
||
|
||
The `CredentialProvider` concept from the previous architecture is simplified: there is no phase progression (A–D). The `IdentityProvider` has two resolution paths — fingerprint and token — and a `ConfigIdentityProvider` implementation that draws from static and dynamic config.
|
||
|
||
`alknet-vault` stays standalone. It does not depend on `alknet-core` or `IdentityProvider`. The vault provides derived keys on request; identity resolution is a separate concern.
|
||
|
||
## Consequences
|
||
|
||
**Positive:**
|
||
- Unified identity model — every handler resolves identities the same way through `IdentityProvider`
|
||
- Handlers own their credential extraction — SSH reads key fingerprints, call reads AuthTokens, HTTP reads Bearer headers
|
||
- Endpoint provides what it can for free (TLS-level auth), handlers complete what they need
|
||
- Adding a new credential type is adding a method to `IdentityProvider`, not a new phase
|
||
- alknet-secret stays standalone — no coupling between key derivation and identity resolution
|
||
- `AuthContext` is a value type — easy to construct in tests, can be partial for handler-level testing
|
||
|
||
**Negative:**
|
||
- `IdentityProvider` is in alknet-core — any change to it recompiles all handlers (mitigated: the trait should be stable; implementation changes don't force recompiles)
|
||
- Two resolution paths (fingerprint, token) may not cover all future auth schemes (mitigated: the trait can be extended, or a handler can do custom resolution after the initial AuthContext)
|
||
- Handlers must handle partial AuthContext — the endpoint may not have resolved an Identity, so handlers must be prepared to do credential extraction themselves
|
||
- WebTransport and browser-based auth needs careful design — AuthToken in CONNECT headers requires the token to be available before the stream is established
|
||
|
||
## References
|
||
|
||
- Pivot proposal: `docs/research/pivot/alpn-service-architecture.md`
|
||
- ADR-002: ProtocolHandler trait
|
||
- ADR-003: Crate decomposition
|
||
- ADR-005: irpc as call protocol foundation
|
||
- The previous architecture had equivalent decisions in ADR-023 (unified auth) and ADR-029 (identity as core type), which are archived in the reference implementation at `/workspace/@alkdev/alknet-main/`. |