siloxide_security/adapters/
authentication_service.rs1use std::sync::Arc;
4
5use siloxide_core::errors::{SilaError, UndefinedExecutionError, ValidationError};
6use siloxide_core::fqi::FullyQualifiedFeatureIdentifier;
7use siloxide_std::Context;
8use siloxide_std::standard::core::authentication_service::v1::{
9 AuthenticationService as GeneratedAuthenticationService, Element, LoginResponses,
10 RequestedServer, TokenLifetime, authentication_service as generated,
11};
12use siloxide_transport::invocation::handler::FeatureHandler;
13use siloxide_transport::server::snapshot::FeatureSpec;
14use uuid::Uuid;
15
16use crate::authentication::{AuthenticationBackend, LoginRejection, LoginRequest, LogoutRejection};
17use crate::secret::{AccessToken, Password};
18
19pub const FEATURE: &str = generated::FEATURE;
21
22const SANITIZED: &str = "authentication is temporarily unavailable";
27
28#[must_use]
36pub fn default_feature<B: AuthenticationBackend>(backend: B) -> AuthenticationService {
37 AuthenticationService {
38 handler: Arc::new(generated::Handler::new(Adapter { backend })),
39 }
40}
41
42pub struct AuthenticationService {
44 handler: Arc<dyn FeatureHandler>,
45}
46
47impl std::fmt::Debug for AuthenticationService {
48 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 formatter.write_str("AuthenticationService")
50 }
51}
52
53impl From<AuthenticationService> for FeatureSpec {
54 fn from(service: AuthenticationService) -> Self {
55 FeatureSpec::from_fdl(generated::DEFINITION, service.handler)
56 }
57}
58
59fn parameter_fqi(command: &str, name: &str) -> String {
61 format!("{FEATURE}/Command/{command}/Parameter/{name}")
62}
63
64fn unavailable(source: &dyn std::error::Error) -> SilaError {
66 tracing::error!(error = %source, "the authentication backend failed");
67 SilaError::UndefinedExecutionError(UndefinedExecutionError {
68 message: SANITIZED.to_owned(),
69 })
70}
71
72struct Adapter<B> {
73 backend: B,
74}
75
76impl<B: AuthenticationBackend> GeneratedAuthenticationService for Adapter<B> {
77 type Error = SilaError;
78
79 async fn login(
80 &self,
81 _context: &Context,
82 user_identification: String,
83 password: String,
84 requested_server: RequestedServer,
85 requested_features: Vec<Element>,
86 ) -> Result<LoginResponses, SilaError> {
87 let requested_server =
91 Uuid::parse_str(&requested_server.into_inner()).map_err(|error| {
92 SilaError::ValidationError(ValidationError {
93 parameter_fqi: parameter_fqi("Login", "RequestedServer"),
94 message: format!("not a server UUID: {error}"),
95 })
96 })?;
97
98 let requested_features = requested_features
99 .into_iter()
100 .map(|raw| {
101 raw.into_inner()
102 .parse::<FullyQualifiedFeatureIdentifier>()
103 .map_err(|error| {
104 SilaError::ValidationError(ValidationError {
105 parameter_fqi: parameter_fqi("Login", "RequestedFeatures"),
106 message: format!("not a Fully Qualified Feature Identifier: {error}"),
107 })
108 })
109 })
110 .collect::<Result<Vec<_>, _>>()?;
111
112 let grant = self
113 .backend
114 .login(LoginRequest {
115 user_identification,
116 password: Password::new(password),
117 requested_server,
118 requested_features,
119 })
120 .await
121 .map_err(|rejection| match rejection {
122 LoginRejection::InvalidCredentials => {
123 generated::errors::AuthenticationFailed::raise(
124 "the provided credentials are not valid".to_owned(),
125 )
126 }
127 LoginRejection::Unavailable(source) => unavailable(source.as_ref()),
128 })?;
129
130 let lifetime = whole_seconds(grant.lifetime())?;
131
132 Ok(LoginResponses {
133 access_token: grant.token().reveal().to_owned(),
134 token_lifetime: lifetime,
135 })
136 }
137
138 async fn logout(&self, _context: &Context, access_token: String) -> Result<(), SilaError> {
139 self.backend
140 .logout(AccessToken::new(access_token))
141 .await
142 .map_err(|rejection| match rejection {
143 LogoutRejection::InvalidToken => generated::errors::InvalidAccessToken::raise(
144 "the sent access token is not valid".to_owned(),
145 ),
146 LogoutRejection::Unavailable(source) => unavailable(source.as_ref()),
147 })
148 }
149}
150
151fn whole_seconds(lifetime: std::time::Duration) -> Result<TokenLifetime, SilaError> {
156 if lifetime.subsec_nanos() != 0 {
157 return Err(SilaError::UndefinedExecutionError(
158 UndefinedExecutionError {
159 message: format!(
160 "the authentication backend granted a token lifetime of {lifetime:?}, and the \
161 standard TokenLifetime response carries whole seconds only"
162 ),
163 },
164 ));
165 }
166 let seconds = i64::try_from(lifetime.as_secs()).map_err(|_| {
167 SilaError::UndefinedExecutionError(UndefinedExecutionError {
168 message: format!(
169 "the authentication backend granted a token lifetime of {lifetime:?}, which does \
170 not fit the standard TokenLifetime response"
171 ),
172 })
173 })?;
174
175 TokenLifetime::new(seconds).map_err(|error| {
176 SilaError::UndefinedExecutionError(UndefinedExecutionError {
177 message: format!("the Login token lifetime could not be represented: {error}"),
178 })
179 })
180}