Skip to main content

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}