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}