Skip to main content

ruma_common/api/
auth_scheme.rs

1//! The `AuthScheme` trait used to specify the authentication scheme used by endpoints and the types
2//! that implement it.
3
4#![allow(clippy::exhaustive_structs)]
5
6use as_variant::as_variant;
7use http::{HeaderMap, header};
8use serde::Deserialize;
9
10/// Trait implemented by types representing an authentication scheme used by an endpoint.
11pub trait AuthScheme: Sized {
12    /// The input necessary to generate the authentication.
13    type Input<'a>;
14
15    /// The error type returned from [`add_authentication()`](Self::add_authentication).
16    type AddAuthenticationError: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;
17
18    /// The authentication data that can be extracted from a request.
19    type Output;
20
21    /// The error type returned from [`extract_authentication()`](Self::extract_authentication).
22    type ExtractAuthenticationError: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;
23
24    /// Add this authentication scheme to the given outgoing request, if necessary.
25    ///
26    /// Returns an error if the endpoint requires authentication but the input doesn't provide it,
27    /// or if the input fails to serialize to the proper format.
28    fn add_authentication<T: AsRef<[u8]>>(
29        request: &mut http::Request<T>,
30        input: Self::Input<'_>,
31    ) -> Result<(), Self::AddAuthenticationError>;
32
33    /// Extract the data of this authentication scheme from the given incoming request.
34    ///
35    /// Returns an error if the endpoint requires authentication but the request doesn't provide it,
36    /// or if the output fails to deserialize to the proper format.
37    fn extract_authentication<T>(
38        request: &http::Request<T>,
39    ) -> Result<Self::Output, Self::ExtractAuthenticationError>;
40}
41
42/// A marker trait indicating that endpoints which use this auth scheme
43/// require clients authenticated using the [OAuth 2.0 API] to request
44/// certain scopes. See [`Metadata::required_client_scopes()`] for more information.
45///
46/// [OAuth 2.0 API]: https://spec.matrix.org/v1.19/client-server-api/#oauth-20-api
47/// [`Metadata::required_client_scopes()`]: super::Metadata::required_client_scopes
48pub trait ClientScopedAuthScheme: AuthScheme {}
49
50/// No authentication is performed.
51#[derive(Debug, Clone, Copy, Default)]
52pub struct NoAuthentication;
53
54impl AuthScheme for NoAuthentication {
55    type Input<'a> = ();
56    type AddAuthenticationError = std::convert::Infallible;
57    type Output = ();
58    type ExtractAuthenticationError = std::convert::Infallible;
59
60    fn add_authentication<T: AsRef<[u8]>>(
61        _request: &mut http::Request<T>,
62        _input: (),
63    ) -> Result<(), Self::AddAuthenticationError> {
64        Ok(())
65    }
66
67    /// Since this endpoint doesn't expect any authentication, this is a noop.
68    fn extract_authentication<T>(
69        _request: &http::Request<T>,
70    ) -> Result<(), Self::ExtractAuthenticationError> {
71        Ok(())
72    }
73}
74
75/// No authentication is performed on an API that usually relies on access tokens.
76///
77/// Contrary to [`NoAuthentication`], this type accepts a [`SendAccessToken`] as input to be able to
78/// send it regardless of whether it is required.
79#[derive(Debug, Clone, Copy, Default)]
80pub struct NoAccessToken;
81
82impl AuthScheme for NoAccessToken {
83    type Input<'a> = SendAccessToken<'a>;
84    type AddAuthenticationError = header::InvalidHeaderValue;
85    type Output = ();
86    type ExtractAuthenticationError = std::convert::Infallible;
87
88    fn add_authentication<T: AsRef<[u8]>>(
89        request: &mut http::Request<T>,
90        access_token: SendAccessToken<'_>,
91    ) -> Result<(), Self::AddAuthenticationError> {
92        if let Some(access_token) = access_token.get_not_required_for_endpoint() {
93            add_access_token_as_authorization_header(request.headers_mut(), access_token)?;
94        }
95
96        Ok(())
97    }
98
99    /// Since this endpoint doesn't expect any authentication, this is a noop.
100    fn extract_authentication<T>(
101        _request: &http::Request<T>,
102    ) -> Result<(), Self::ExtractAuthenticationError> {
103        Ok(())
104    }
105}
106
107/// Authentication is performed by including an access token in the `Authentication` http
108/// header, or an `access_token` query parameter.
109///
110/// Using the query parameter is deprecated since Matrix 1.11.
111#[derive(Debug, Clone, Copy, Default)]
112pub struct AccessToken;
113
114impl AuthScheme for AccessToken {
115    type Input<'a> = SendAccessToken<'a>;
116    type AddAuthenticationError = AddRequiredTokenError;
117    /// The access token.
118    type Output = String;
119    type ExtractAuthenticationError = ExtractTokenError;
120
121    fn add_authentication<T>(
122        request: &mut http::Request<T>,
123        access_token: SendAccessToken<'_>,
124    ) -> Result<(), Self::AddAuthenticationError> {
125        let token = access_token
126            .get_required_for_endpoint()
127            .ok_or(AddRequiredTokenError::MissingAccessToken)?;
128        Ok(add_access_token_as_authorization_header(request.headers_mut(), token)?)
129    }
130
131    fn extract_authentication<T>(
132        request: &http::Request<T>,
133    ) -> Result<String, Self::ExtractAuthenticationError> {
134        extract_bearer_or_query_token(request)?.ok_or(ExtractTokenError::MissingAccessToken)
135    }
136}
137
138impl ClientScopedAuthScheme for AccessToken {}
139
140/// Authentication is optional, and it is performed by including an access token in the
141/// `Authentication` http header, or an `access_token` query parameter.
142///
143/// Using the query parameter is deprecated since Matrix 1.11.
144#[derive(Debug, Clone, Copy, Default)]
145pub struct AccessTokenOptional;
146
147impl AuthScheme for AccessTokenOptional {
148    type Input<'a> = SendAccessToken<'a>;
149    type AddAuthenticationError = header::InvalidHeaderValue;
150    /// The access token, if any.
151    type Output = Option<String>;
152    type ExtractAuthenticationError = ExtractTokenError;
153
154    fn add_authentication<T: AsRef<[u8]>>(
155        request: &mut http::Request<T>,
156        access_token: SendAccessToken<'_>,
157    ) -> Result<(), Self::AddAuthenticationError> {
158        if let Some(access_token) = access_token.get_required_for_endpoint() {
159            add_access_token_as_authorization_header(request.headers_mut(), access_token)?;
160        }
161
162        Ok(())
163    }
164
165    fn extract_authentication<T>(
166        request: &http::Request<T>,
167    ) -> Result<Option<String>, Self::ExtractAuthenticationError> {
168        extract_bearer_or_query_token(request)
169    }
170}
171
172impl ClientScopedAuthScheme for AccessTokenOptional {}
173
174/// Authentication is required, and can only be performed for appservices, by including an
175/// appservice access token in the `Authentication` http header, or `access_token` query
176/// parameter.
177///
178/// Using the query parameter is deprecated since Matrix 1.11.
179#[derive(Debug, Clone, Copy, Default)]
180pub struct AppserviceToken;
181
182impl AuthScheme for AppserviceToken {
183    type Input<'a> = SendAccessToken<'a>;
184    type AddAuthenticationError = AddRequiredTokenError;
185    /// The appservice token.
186    type Output = String;
187    type ExtractAuthenticationError = ExtractTokenError;
188
189    fn add_authentication<T: AsRef<[u8]>>(
190        request: &mut http::Request<T>,
191        access_token: SendAccessToken<'_>,
192    ) -> Result<(), Self::AddAuthenticationError> {
193        let token = access_token
194            .get_required_for_appservice()
195            .ok_or(AddRequiredTokenError::MissingAccessToken)?;
196        Ok(add_access_token_as_authorization_header(request.headers_mut(), token)?)
197    }
198
199    fn extract_authentication<T>(
200        request: &http::Request<T>,
201    ) -> Result<String, Self::ExtractAuthenticationError> {
202        extract_bearer_or_query_token(request)?.ok_or(ExtractTokenError::MissingAccessToken)
203    }
204}
205
206/// No authentication is performed for clients, but it can be performed for appservices, by
207/// including an appservice access token in the `Authentication` http header, or an
208/// `access_token` query parameter.
209///
210/// Using the query parameter is deprecated since Matrix 1.11.
211#[derive(Debug, Clone, Copy, Default)]
212pub struct AppserviceTokenOptional;
213
214impl AuthScheme for AppserviceTokenOptional {
215    type Input<'a> = SendAccessToken<'a>;
216    type AddAuthenticationError = header::InvalidHeaderValue;
217    /// The appservice token, if any.
218    type Output = Option<String>;
219    type ExtractAuthenticationError = ExtractTokenError;
220
221    fn add_authentication<T: AsRef<[u8]>>(
222        request: &mut http::Request<T>,
223        access_token: SendAccessToken<'_>,
224    ) -> Result<(), Self::AddAuthenticationError> {
225        if let Some(access_token) = access_token.get_required_for_appservice() {
226            add_access_token_as_authorization_header(request.headers_mut(), access_token)?;
227        }
228
229        Ok(())
230    }
231
232    fn extract_authentication<T>(
233        request: &http::Request<T>,
234    ) -> Result<Option<String>, Self::ExtractAuthenticationError> {
235        extract_bearer_or_query_token(request)
236    }
237}
238
239/// Add the given access token as an `Authorization` HTTP header to the given map.
240pub fn add_access_token_as_authorization_header(
241    headers: &mut HeaderMap,
242    token: &str,
243) -> Result<(), header::InvalidHeaderValue> {
244    headers.insert(header::AUTHORIZATION, format!("Bearer {token}").try_into()?);
245    Ok(())
246}
247
248/// Extract the access token from the `Authorization` HTTP header or the query string of the given
249/// request.
250pub fn extract_bearer_or_query_token<T>(
251    request: &http::Request<T>,
252) -> Result<Option<String>, ExtractTokenError> {
253    if let Some(token) = extract_bearer_token_from_authorization_header(request.headers())? {
254        return Ok(Some(token));
255    }
256
257    if let Some(query) = request.uri().query() {
258        Ok(extract_access_token_from_query(query)?)
259    } else {
260        Ok(None)
261    }
262}
263
264/// Extract the value of the `Authorization` HTTP header with a `Bearer` scheme.
265fn extract_bearer_token_from_authorization_header(
266    headers: &HeaderMap,
267) -> Result<Option<String>, ExtractTokenError> {
268    const EXPECTED_START: &str = "bearer ";
269
270    let Some(value) = headers.get(header::AUTHORIZATION) else {
271        return Ok(None);
272    };
273
274    let value = value.to_str()?;
275
276    if value.len() < EXPECTED_START.len()
277        || !value[..EXPECTED_START.len()].eq_ignore_ascii_case(EXPECTED_START)
278    {
279        return Err(ExtractTokenError::InvalidAuthorizationScheme);
280    }
281
282    Ok(Some(value[EXPECTED_START.len()..].to_owned()))
283}
284
285/// Extract the `access_token` from the given query string.
286fn extract_access_token_from_query(
287    query: &str,
288) -> Result<Option<String>, serde_html_form::de::Error> {
289    #[derive(Deserialize)]
290    struct AccessTokenDeHelper {
291        access_token: Option<String>,
292    }
293
294    serde_html_form::from_str::<AccessTokenDeHelper>(query).map(|helper| helper.access_token)
295}
296
297/// An enum to control whether an access token should be added to outgoing requests
298#[derive(Clone, Copy, Debug)]
299#[allow(clippy::exhaustive_enums)]
300pub enum SendAccessToken<'a> {
301    /// Add the given access token to the request only if the `METADATA` on the request requires
302    /// it.
303    IfRequired(&'a str),
304
305    /// Always add the access token.
306    Always(&'a str),
307
308    /// Add the given appservice token to the request only if the `METADATA` on the request
309    /// requires it.
310    Appservice(&'a str),
311
312    /// Don't add an access token.
313    ///
314    /// This will lead to an error if the request endpoint requires authentication
315    None,
316}
317
318impl<'a> SendAccessToken<'a> {
319    /// Get the access token for an endpoint that requires one.
320    ///
321    /// Returns `Some(_)` if `self` contains an access token.
322    pub fn get_required_for_endpoint(self) -> Option<&'a str> {
323        as_variant!(self, Self::IfRequired | Self::Appservice | Self::Always)
324    }
325
326    /// Get the access token for an endpoint that should not require one.
327    ///
328    /// Returns `Some(_)` only if `self` is `SendAccessToken::Always(_)`.
329    pub fn get_not_required_for_endpoint(self) -> Option<&'a str> {
330        as_variant!(self, Self::Always)
331    }
332
333    /// Gets the access token for an endpoint that requires one for appservices.
334    ///
335    /// Returns `Some(_)` if `self` is either `SendAccessToken::Appservice(_)`
336    /// or `SendAccessToken::Always(_)`
337    pub fn get_required_for_appservice(self) -> Option<&'a str> {
338        as_variant!(self, Self::Appservice | Self::Always)
339    }
340}
341
342/// An error that can occur when adding an [`AuthScheme`] that requires an access token.
343#[derive(Debug, thiserror::Error)]
344#[non_exhaustive]
345pub enum AddRequiredTokenError {
346    /// No access token was provided, but the endpoint requires one.
347    #[error("no access token provided, but this endpoint requires one")]
348    MissingAccessToken,
349
350    /// Failed to convert the authentication to a header value.
351    #[error(transparent)]
352    IntoHeader(#[from] header::InvalidHeaderValue),
353}
354
355/// An error that can occur when extracting an [`AuthScheme`] that expects an access token.
356#[derive(Debug, thiserror::Error)]
357#[non_exhaustive]
358pub enum ExtractTokenError {
359    /// No access token was found, but the endpoint requires one.
360    #[error("no access token found, but this endpoint requires one")]
361    MissingAccessToken,
362
363    /// Failed to convert the header value to a UTF-8 string.
364    #[error(transparent)]
365    FromHeader(#[from] header::ToStrError),
366
367    /// The scheme of the Authorization HTTP header is invalid.
368    #[error("invalid authorization header scheme")]
369    InvalidAuthorizationScheme,
370
371    /// Failed to deserialize the query string.
372    #[error("failed to deserialize query string: {0}")]
373    FromQuery(#[from] serde_html_form::de::Error),
374}