hermod_api/services/
auth.rs1use actix_web::http::HeaderMap;
4use actix_web::HttpRequest;
5use anyhow::Context;
6use argon2::{Argon2, PasswordHash, PasswordVerifier};
7use sqlx::PgPool;
8
9use crate::{db::User, handlers::ApplicationError};
10
11pub async fn validate_request_with_basic_auth(
14 request: HttpRequest,
15 pool: &PgPool,
16) -> Result<User, AuthenticationError> {
17 let credentials =
18 extract_from_headers(request.headers()).map_err(|_| AuthenticationError::InvalidHeaders)?;
19 let user = validate_credentials(credentials, pool).await?;
20 Ok(user)
21}
22
23#[tracing::instrument(name = "services::auth::validate_credentials", skip(credentials, pool), fields(
24 username=%credentials.username,
25))]
26async fn validate_credentials(
27 credentials: Credentials,
28 pool: &PgPool,
29) -> Result<User, AuthenticationError> {
30 let mut user = None;
31 let mut expected_password_hash = "$argon2id$v=19$m=15000,t=2,p=1$\
32 gZiV/M1gPc22ElAH/Jh1Hw$\
33 CWOrkoo7oJBQ/iyh7uJ0LO2aLEfrHwTWllSAxT0zRno"
34 .to_string();
35
36 if let Some((stored_user, stored_password_hash)) =
37 get_stored_credentials(&credentials.username, pool)
38 .await
39 .map_err(AuthenticationError::UnexpectedError)?
40 {
41 user = Some(stored_user);
42 expected_password_hash = stored_password_hash;
43 }
44
45 actix_web::rt::task::spawn_blocking(move || {
46 verify_password_hash(expected_password_hash, credentials.password)
47 })
48 .await
49 .context("Failed to spawn blocking task.")
50 .map_err(AuthenticationError::UnexpectedError)??;
51
52 user.ok_or(AuthenticationError::InvalidCredentials)
53}
54
55async fn get_stored_credentials(
56 username: &str,
57 pool: &PgPool,
58) -> Result<Option<(User, String)>, anyhow::Error> {
59 let username = username.to_lowercase();
60 let row = sqlx::query_as!(
61 User,
62 r#"
63 SELECT *
64 FROM account
65 WHERE username = $1
66 "#,
67 username,
68 )
69 .fetch_optional(pool)
70 .await
71 .context("Failed to performed a query to retrieve stored credentials.")?
72 .map(|row| (row.clone(), row.password));
73 Ok(row)
74}
75
76fn verify_password_hash(
77 expected_password_hash: String,
78 password_candidate: String,
79) -> Result<(), AuthenticationError> {
80 let expected_password_hash = PasswordHash::new(&expected_password_hash)
81 .context("Failed to parse hash in PHC string format.")
82 .map_err(AuthenticationError::UnexpectedError)?;
83
84 Argon2::default()
85 .verify_password(password_candidate.as_bytes(), &expected_password_hash)
86 .context("Invalid password.")
87 .map_err(|_| AuthenticationError::InvalidCredentials)
88}
89
90fn extract_from_headers(headers: &HeaderMap) -> Result<Credentials, anyhow::Error> {
91 let header_value = headers
92 .get("Authorization")
93 .context("The 'Authorization' header was missing.")?
94 .to_str()
95 .context("The 'Authorization' header was not a valid UTF-8 string.")?;
96 let base64_encoded_segment = header_value
97 .strip_prefix("Basic ")
98 .context("The authorization scheme was not 'Basic'.")?;
99 let decoded_bytes = base64::decode_config(base64_encoded_segment, base64::STANDARD)
100 .context("Failed to base64-decode 'Basic' credentials.")?;
101 let decoded_credentials = String::from_utf8(decoded_bytes)
102 .context("The decoded credential string is not valid UTF-8.")?;
103
104 let mut credentials = decoded_credentials.splitn(2, ':');
105 let username = credentials
106 .next()
107 .ok_or_else(|| anyhow::anyhow!("A username must be provided in 'Basic' auth."))?
108 .to_string();
109 let password = credentials
110 .next()
111 .ok_or_else(|| anyhow::anyhow!("A password must be provided in 'Basic' auth."))?
112 .to_string();
113
114 Ok(Credentials { username, password })
115}
116
117struct Credentials {
118 username: String,
119 password: String,
120}
121
122#[derive(thiserror::Error)]
124pub enum AuthenticationError {
125 #[error(transparent)]
126 UnexpectedError(#[from] anyhow::Error),
127 #[error("Invalid headers.")]
128 InvalidHeaders,
129 #[error("Invalid credentials.")]
130 InvalidCredentials,
131 #[error("Not logged in.")]
132 Unauthorized,
133}
134
135impl std::fmt::Debug for AuthenticationError {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 crate::services::error::error_chain_fmt(self, f)
138 }
139}
140
141impl From<AuthenticationError> for ApplicationError {
142 fn from(e: AuthenticationError) -> Self {
143 Self::AuthError(e)
144 }
145}