arcp_core/auth/mod.rs
1//! Authentication scheme adapters (RFC §8.2).
2//!
3//! Each [`Authenticator`] implementation validates one auth scheme. The
4//! runtime composes these into an [`AuthRegistry`]; on `session.open` the
5//! runtime dispatches by [`AuthScheme`] and either accepts directly or
6//! issues a [`session.challenge`][crate::messages::SessionChallengePayload].
7
8use std::collections::HashMap;
9
10use async_trait::async_trait;
11
12use crate::error::ARCPError;
13use crate::messages::{AuthScheme, Capabilities, ClientIdentity, Credentials};
14
15/// Outcome of an authentication attempt.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum AuthOutcome {
18 /// Credentials suffice; the session can be accepted directly.
19 Accept {
20 /// Principal identifier extracted from the credentials.
21 principal: String,
22 },
23 /// Runtime needs to issue a challenge; client must respond with
24 /// `session.authenticate`.
25 Challenge {
26 /// Challenge nonce or instructions.
27 challenge: String,
28 },
29 /// Credentials rejected.
30 Reject {
31 /// Human-readable reason.
32 reason: String,
33 },
34}
35
36/// Adapter trait for one auth scheme.
37#[async_trait]
38pub trait Authenticator: Send + Sync {
39 /// Scheme this authenticator handles.
40 fn scheme(&self) -> AuthScheme;
41
42 /// Validate `creds` against the runtime trust store.
43 ///
44 /// `client` is the attestation block from `session.open`; `negotiated`
45 /// is the capability set the runtime is willing to honour. The
46 /// `none` scheme uses `negotiated` to gate on `anonymous: true`.
47 ///
48 /// # Errors
49 ///
50 /// Returns [`ARCPError`] for unrecoverable internal failures (e.g.
51 /// trust store unreachable). Credential rejection is reported through
52 /// [`AuthOutcome::Reject`], not via `Err`.
53 async fn authenticate(
54 &self,
55 creds: &Credentials,
56 client: &ClientIdentity,
57 negotiated: &Capabilities,
58 ) -> Result<AuthOutcome, ARCPError>;
59
60 /// Verify the response to a previously issued challenge. Default
61 /// implementation rejects everything (single-shot schemes don't need
62 /// to override).
63 ///
64 /// # Errors
65 ///
66 /// Returns [`ARCPError`] for unrecoverable internal failures.
67 async fn verify_challenge_response(
68 &self,
69 _challenge: &str,
70 _response: &str,
71 ) -> Result<AuthOutcome, ARCPError> {
72 Ok(AuthOutcome::Reject {
73 reason: "this scheme does not use challenges".into(),
74 })
75 }
76}
77
78/// Runtime-owned set of authenticators, keyed by scheme.
79pub struct AuthRegistry {
80 by_scheme: HashMap<AuthSchemeKey, Box<dyn Authenticator>>,
81}
82
83impl Default for AuthRegistry {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl std::fmt::Debug for AuthRegistry {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("AuthRegistry")
92 .field("schemes", &self.by_scheme.keys().collect::<Vec<_>>())
93 .finish()
94 }
95}
96
97impl AuthRegistry {
98 /// Construct an empty registry.
99 #[must_use]
100 pub fn new() -> Self {
101 Self {
102 by_scheme: HashMap::new(),
103 }
104 }
105
106 /// Register `auth` for the scheme it advertises.
107 pub fn register(&mut self, auth: Box<dyn Authenticator>) {
108 self.by_scheme.insert(auth.scheme().into(), auth);
109 }
110
111 /// Look up the authenticator for `scheme`, or `None` if unsupported.
112 #[must_use]
113 pub fn get(&self, scheme: &AuthScheme) -> Option<&dyn Authenticator> {
114 self.by_scheme
115 .get(&AuthSchemeKey::from(scheme.clone()))
116 .map(AsRef::as_ref)
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
121struct AuthSchemeKey(String);
122
123impl From<AuthScheme> for AuthSchemeKey {
124 fn from(s: AuthScheme) -> Self {
125 let name = match s {
126 AuthScheme::Bearer => "bearer",
127 AuthScheme::SignedJwt => "signed_jwt",
128 AuthScheme::None => "none",
129 AuthScheme::Mtls => "mtls",
130 AuthScheme::Oauth2 => "oauth2",
131 };
132 Self(name.to_owned())
133 }
134}