axum_auth/
auth_basic.rs

1//! Implementation of http basic authentication
2//!
3//! See [AuthBasic] for the most commonly-used data structure
4
5use crate::{get_header, Rejection, ERR_DECODE, ERR_DEFAULT, ERR_WRONG_BASIC};
6use axum_core::extract::FromRequestParts;
7use base64::{engine::general_purpose, Engine};
8use http::{request::Parts, StatusCode};
9
10/// Basic authentication extractor, containing an identifier as well as an optional password
11///
12/// This is enabled via the `auth-basic` feature
13///
14/// # Example
15///
16/// Though this structure can be used like any other axum extractor, we recommend this pattern:
17///
18/// ```no_run
19/// use axum_auth::AuthBasic;
20///
21/// /// Takes basic auth details and shows a message
22/// async fn handler(AuthBasic((id, password)): AuthBasic) -> String {
23///     if let Some(password) = password {
24///         format!("User '{}' with password '{}'", id, password)
25///     } else {
26///         format!("User '{}' without password", id)
27///     }
28/// }
29/// ```
30///
31/// # Errors
32///
33/// There are a few errors which this extractor can make. By default, all invalid responses are `400 BAD REQUEST` with one of these messages:
34///
35/// - \`Authorization\` header could not be decoded – The header couldn't be decoded, probably missing a colon
36/// - \`Authorization\` header must be for basic authentication – Someone tried to use bearer auth instead of basic auth
37/// - \`Authorization\` header is missing – The header was required but it wasn't found
38/// - \`Authorization\` header contains invalid characters – The header couldn't be processed because of invalid characters
39#[derive(Debug, PartialEq, Eq, Clone)]
40pub struct AuthBasic(pub (String, Option<String>));
41
42impl<B> FromRequestParts<B> for AuthBasic
43where
44    B: Send + Sync,
45{
46    type Rejection = Rejection;
47
48    async fn from_request_parts(parts: &mut Parts, _: &B) -> Result<Self, Self::Rejection> {
49        Self::decode_request_parts(parts)
50    }
51}
52
53impl AuthBasicCustom for AuthBasic {
54    const ERROR_CODE: StatusCode = ERR_DEFAULT;
55    const ERROR_OVERWRITE: Option<&'static str> = None;
56
57    fn from_header(contents: (String, Option<String>)) -> Self {
58        Self(contents)
59    }
60}
61
62/// Custom extractor trait for basic auth allowing you to implement custom responses
63///
64/// This is enabled via the `auth-basic` feature
65///
66/// # Usage
67///
68/// To create your own basic auth extractor using this create, you have to:
69///
70/// 1. Make the extractor struct, something like `struct Example((String, Option<String>));`
71/// 2. Implement [FromRequestParts] that links to step 3, copy and paste this from the example below
72/// 3. Implement [AuthBasicCustom] to generate your extractor with your custom options, see the example below
73///
74/// Once you've completed these steps, you should have a new extractor which is just as easy to use as [AuthBasic] but has all of your custom configuration options inside of it!
75///
76/// # Example
77///
78/// This is what a typical custom extractor should look like in full, copy-paste this and edit it:
79///
80/// ```rust
81/// use axum_auth::{AuthBasicCustom, Rejection};
82/// use http::{request::Parts, StatusCode};
83/// use axum::extract::FromRequestParts;
84///
85/// /// Your custom basic auth returning a fun 418 for errors
86/// struct MyCustomBasicAuth((String, Option<String>));
87///
88/// // This is where you define your custom options:
89/// impl AuthBasicCustom for MyCustomBasicAuth {
90///     const ERROR_CODE: StatusCode = StatusCode::IM_A_TEAPOT; // <-- define custom status code here
91///     const ERROR_OVERWRITE: Option<&'static str> = None; // <-- define overwriting message here
92///
93///     fn from_header(contents: (String, Option<String>)) -> Self {
94///         Self(contents)
95///     }
96/// }
97///
98/// // This is just boilerplate for now, copy and paste this:
99/// impl<B> FromRequestParts<B> for MyCustomBasicAuth
100/// where
101///     B: Send + Sync,
102/// {
103///     type Rejection = Rejection;
104///
105///     async fn from_request_parts(parts: &mut Parts, _: &B) -> Result<Self, Self::Rejection> {
106///         Self::decode_request_parts(parts)
107///     }
108/// }
109/// ```
110///
111/// Some notes about this example for some more insight:
112///
113/// - There's no reason for the [FromRequestParts] to ever change out of this pattern unless you're doing something special
114/// - It's recommended to use the `struct BasicExample((String, Option<String>));` pattern because it makes using it from routes easy
115pub trait AuthBasicCustom: Sized {
116    /// Error code to use instead of the typical `400 BAD REQUEST` error
117    const ERROR_CODE: StatusCode;
118
119    /// Message to overwrite all default ones with if required, leave as [None] ideally
120    const ERROR_OVERWRITE: Option<&'static str>;
121
122    /// Converts provided header contents to new instance of self; you need to implement this
123    ///
124    /// # Example
125    ///
126    /// With the typical `struct BasicExample((String, Option<String>));` pattern of structures, this can be implemented like so:
127    ///
128    /// ```rust
129    /// use axum_auth::AuthBasicCustom;
130    /// use http::StatusCode;
131    ///
132    /// struct BasicExample((String, Option<String>));
133    ///
134    /// impl AuthBasicCustom for BasicExample {
135    ///     const ERROR_CODE: StatusCode = StatusCode::BAD_REQUEST;
136    ///     const ERROR_OVERWRITE: Option<&'static str> = None;
137    ///
138    ///     fn from_header(contents: (String, Option<String>)) -> Self {
139    ///         Self(contents)
140    ///     }
141    /// }
142    /// ```
143    ///
144    /// All this method does is let you put the automatically contents of the header into your resulting structure.
145    fn from_header(contents: (String, Option<String>)) -> Self;
146
147    /// Decodes bearer token content into new instance of self from axum body parts; this is automatically implemented
148    fn decode_request_parts(req: &mut Parts) -> Result<Self, Rejection> {
149        // Get authorization header
150        let authorization = get_header(req, Self::ERROR_CODE)?;
151
152        // Check that its well-formed basic auth then decode and return
153        let split = authorization.split_once(' ');
154        match split {
155            Some((name, contents)) if name == "Basic" => {
156                let decoded = decode(contents, (Self::ERROR_CODE, ERR_DECODE))?;
157                Ok(Self::from_header(decoded))
158            }
159            _ => Err((Self::ERROR_CODE, ERR_WRONG_BASIC)),
160        }
161    }
162}
163
164/// Decodes the two parts of basic auth using the colon
165fn decode(input: &str, err: Rejection) -> Result<(String, Option<String>), Rejection> {
166    // Decode from base64 into a string
167    let decoded = general_purpose::STANDARD.decode(input).map_err(|_| err)?;
168    let decoded = String::from_utf8(decoded).map_err(|_| err)?;
169
170    // Return depending on if password is present
171    Ok(if let Some((id, password)) = decoded.split_once(':') {
172        (id.to_string(), Some(password.to_string()))
173    } else {
174        (decoded.to_string(), None)
175    })
176}