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}