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
5.7 KiB
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 madeAuthContextimmutable inhandle()(passed as&AuthContext). Handlers resolve identity into a local variable and store it onConnectionviaset_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:
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:
-
Endpoint-level resolution (before
handle()is called): If the TLS handshake provides a client certificate, the endpoint resolves the fingerprint to anIdentityand passes it inAuthContext. This is the case for SSH (where the key exchange happens at the protocol level, but the TLS layer may also provide information). -
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 callsIdentityProviderto resolve it. The handler then resolves theIdentityinto a local variable and stores it on theConnectionviaset_identity()for observability — it does not mutate theAuthContext(which is passed as&AuthContext, an immutable reference — see ADR-011). The per-request identity (for ACL) is resolved separately by theCallAdapteratcall.requestedtime.
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
AuthContextis a value type — easy to construct in tests, can be partial for handler-level testing
Negative:
IdentityProvideris 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/.