Skip to main content

siloxide_security/
authentication.rs

1//! What an application supplies so this server can issue access tokens.
2
3use std::time::Duration;
4
5use async_trait::async_trait;
6use siloxide_core::fqi::FullyQualifiedFeatureIdentifier;
7use uuid::Uuid;
8
9use crate::secret::{AccessToken, Password};
10
11/// One `Login` call, as the standard Feature declares it.
12#[derive(Debug, Clone)]
13pub struct LoginRequest {
14    /// The user name, or whatever identifies the user to this backend.
15    pub user_identification: String,
16    /// The password offered for it.
17    pub password: Password,
18    /// The `ServerUUID` the client asked to be authorized for.
19    ///
20    /// Checking it against this server's own identity is the backend's decision, because a backend
21    /// fronting several servers may legitimately accept another.
22    pub requested_server: Uuid,
23    /// The Features the client asked for. Empty means every Feature, as Part C p.9 defines it.
24    pub requested_features: Vec<FullyQualifiedFeatureIdentifier>,
25}
26
27/// A token, and how long it stays valid after the last request.
28#[derive(Debug, Clone)]
29pub struct AccessGrant {
30    token: AccessToken,
31    lifetime: Duration,
32}
33
34impl AccessGrant {
35    /// A grant of `token`, valid for `lifetime` after the last request.
36    ///
37    /// The standard response is a constrained Integer of seconds, so a lifetime with a fractional
38    /// part cannot be sent. It is refused when the grant is answered rather than rounded here,
39    /// because rounding would quietly change how long a client believes its token lives.
40    #[must_use]
41    pub fn new(token: AccessToken, lifetime: Duration) -> Self {
42        Self { token, lifetime }
43    }
44
45    /// The token itself.
46    #[must_use]
47    pub fn token(&self) -> &AccessToken {
48        &self.token
49    }
50
51    /// How long it stays valid after the last request.
52    #[must_use]
53    pub fn lifetime(&self) -> Duration {
54        self.lifetime
55    }
56}
57
58/// Why a backend did not issue a token.
59#[derive(Debug, thiserror::Error)]
60pub enum LoginRejection {
61    /// The credentials are not the ones this backend holds.
62    ///
63    /// Becomes the Feature's declared `AuthenticationFailed`.
64    #[error("the provided credentials are not valid")]
65    InvalidCredentials,
66    /// The backend could not answer at all.
67    ///
68    /// Becomes an Undefined Execution Error with a message that says nothing about why, because a
69    /// directory outage is not something a client should learn from a login attempt. The source
70    /// stays on this server, in the log.
71    #[error("the authentication backend could not answer: {0}")]
72    Unavailable(#[source] Box<dyn std::error::Error + Send + Sync>),
73}
74
75impl LoginRejection {
76    /// A backend failure carrying `source`.
77    #[must_use]
78    pub fn unavailable(source: impl std::error::Error + Send + Sync + 'static) -> Self {
79        Self::Unavailable(Box::new(source))
80    }
81}
82
83/// Why a backend did not invalidate a token.
84#[derive(Debug, thiserror::Error)]
85pub enum LogoutRejection {
86    /// The token is not one this backend issued, or it has already been invalidated.
87    ///
88    /// Becomes the Feature's declared `InvalidAccessToken`.
89    #[error("the sent access token is not valid")]
90    InvalidToken,
91    /// The backend could not answer at all. Sanitized exactly as [`LoginRejection::Unavailable`].
92    #[error("the authentication backend could not answer: {0}")]
93    Unavailable(#[source] Box<dyn std::error::Error + Send + Sync>),
94}
95
96impl LogoutRejection {
97    /// A backend failure carrying `source`.
98    #[must_use]
99    pub fn unavailable(source: impl std::error::Error + Send + Sync + 'static) -> Self {
100        Self::Unavailable(Box::new(source))
101    }
102}
103
104/// Identity, credentials, and tokens, as the application implements them.
105///
106/// Siloxide owns the standard Feature, its wire mapping, and its errors. Everything a token *is* —
107/// how it is minted, what it is scoped to, when it expires, how it is revoked — belongs here, and
108/// is never inspected or cached on the way through.
109#[async_trait]
110pub trait AuthenticationBackend: Send + Sync + 'static {
111    /// Issue a token for these credentials.
112    ///
113    /// # Errors
114    ///
115    /// Returns [`LoginRejection::InvalidCredentials`] when the credentials are wrong, and
116    /// [`LoginRejection::Unavailable`] when the backend itself failed. The two are not the same
117    /// answer and must not be collapsed: one is a Defined Execution Error a client can act on, the
118    /// other is this server being broken.
119    async fn login(&self, request: LoginRequest) -> Result<AccessGrant, LoginRejection>;
120
121    /// Invalidate a token immediately.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`LogoutRejection::InvalidToken`] for a token this backend does not hold, and
126    /// [`LogoutRejection::Unavailable`] when the backend itself failed.
127    async fn logout(&self, token: AccessToken) -> Result<(), LogoutRejection>;
128}