diff --git a/Cargo.lock b/Cargo.lock index 19df2a2f..460b0905 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1010,7 +1010,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ - "convert_case", + "convert_case 0.6.0", "nom", "pathdiff", "serde", @@ -1069,6 +1069,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58edcceb030fad49b986afe5783b651123c0ee44a61e22802f91a9d5d1ae1900" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -2684,7 +2693,7 @@ checksum = "20bcb2afa03e0614c64eec4a95ec2986fd3c59358daa0f50006e081bc1bd1067" dependencies = [ "attribute-derive", "cfg-if", - "convert_case", + "convert_case 0.6.0", "html-escape", "itertools 0.13.0", "leptos_hot_reload", @@ -3166,7 +3175,7 @@ version = "0.9.10" dependencies = [ "bon", "concat-string", - "convert_case", + "convert_case 0.7.0", "deluxe", "proc-macro2", "quote", @@ -3794,7 +3803,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d86e4f08f361b05d11422398cef4bc4cf356f2fdd2f06a96646b0e9cd902226" dependencies = [ - "convert_case", + "convert_case 0.6.0", "proc-macro-error2", "proc-macro2", "quote", @@ -4393,7 +4402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee7723bef57b4353cd9939e280d3b5b2ebe45b4a4630c9e9e97a6fa4b84e8b1c" dependencies = [ "const_format", - "convert_case", + "convert_case 0.6.0", "proc-macro2", "quote", "syn", diff --git a/nghe-backend/src/error/mod.rs b/nghe-backend/src/error/mod.rs index 89f05249..e0744ae2 100644 --- a/nghe-backend/src/error/mod.rs +++ b/nghe-backend/src/error/mod.rs @@ -46,6 +46,10 @@ pub enum Kind { #[into(StatusCode| StatusCode::BAD_REQUEST)] #[into(OpensubsonicCode| OpensubsonicCode::RequiredParameterIsMissing)] MissingAuthenticationHeader, + #[error("Invalid bearer authorization format")] + #[into(StatusCode| StatusCode::BAD_REQUEST)] + #[into(OpensubsonicCode| OpensubsonicCode::RequiredParameterIsMissing)] + InvalidBearerAuthorizationFormat, #[error("Wrong username or password")] #[into(StatusCode| StatusCode::UNAUTHORIZED)] #[into(OpensubsonicCode| OpensubsonicCode::WrongUsernameOrPassword)] diff --git a/nghe-backend/src/http/extract/auth/header.rs b/nghe-backend/src/http/extract/auth/header.rs index a34596d6..109ec25b 100644 --- a/nghe-backend/src/http/extract/auth/header.rs +++ b/nghe-backend/src/http/extract/auth/header.rs @@ -3,6 +3,8 @@ use std::marker::PhantomData; use axum::extract::{FromRef, FromRequestParts}; use axum::http::request::Parts; use axum_extra::headers::{self, HeaderMapExt}; +use nghe_api::auth; +use uuid::Uuid; use super::{Authentication, Authorization, username}; use crate::database::Database; @@ -17,8 +19,21 @@ pub struct Header { pub user: users::Authenticated, } +pub type BearerAuthorization = headers::Authorization; pub type BaiscAuthorization = headers::Authorization; +impl Authentication for BearerAuthorization { + async fn authenticated(&self, database: &Database) -> Result { + auth::ApiKey::from( + self.token() + .parse::() + .map_err(|_| error::Kind::InvalidBearerAuthorizationFormat)?, + ) + .authenticated(database) + .await + } +} + impl username::Authentication for BaiscAuthorization { fn username(&self) -> &str { self.username() @@ -38,11 +53,14 @@ where type Rejection = Error; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let header = parts - .headers - .typed_get::() - .ok_or_else(|| error::Kind::MissingAuthenticationHeader)?; - Ok(Self { _request: PhantomData, user: header.login::(state).await? }) + let user = if let Some(header) = parts.headers.typed_get::() { + header.login::(state).await? + } else if let Some(header) = parts.headers.typed_get::() { + header.login::(state).await? + } else { + return error::Kind::MissingAuthenticationHeader.into(); + }; + Ok(Self { _request: PhantomData, user }) } } @@ -51,14 +69,22 @@ where mod tests { use axum::http; use axum_extra::headers::HeaderMapExt; - use fake::Fake; use fake::faker::internet::en::{Password, Username}; + use fake::{Fake, Faker}; use rstest::rstest; use super::username::Authentication; use super::*; use crate::test::{Mock, mock}; + struct Request; + + impl Authorization for Request { + fn authorized(_: crate::orm::users::Role) -> bool { + true + } + } + #[rstest] fn test_authenticated(#[values(true, false)] ok: bool) { let username = Username().fake::(); @@ -72,17 +98,36 @@ mod tests { #[rstest] #[tokio::test] - async fn test_from_request_parts(#[future(awt)] mock: Mock, #[values(true, false)] ok: bool) { - struct Request; + async fn test_from_request_parts_bearer( + #[future(awt)] mock: Mock, + #[values(true, false)] ok: bool, + ) { + let user = mock.user(0).await; + let auth = user.auth_bearer().await; + + let mut http_request = http::Request::builder().body(()).unwrap(); + http_request.headers_mut().typed_insert(if ok { + auth + } else { + BearerAuthorization::bearer(&Faker.fake::().to_string()).unwrap() + }); + let mut parts = http_request.into_parts().0; - impl Authorization for Request { - fn authorized(_: crate::orm::users::Role) -> bool { - true - } + let header = Header::::from_request_parts(&mut parts, mock.state()).await; + assert_eq!(header.is_ok(), ok); + if ok { + assert_eq!(header.unwrap().user.id, user.id()); } + } + #[rstest] + #[tokio::test] + async fn test_from_request_parts_basic( + #[future(awt)] mock: Mock, + #[values(true, false)] ok: bool, + ) { let user = mock.user(0).await; - let auth = user.auth_header(); + let auth = user.auth_basic(); let mut http_request = http::Request::builder().body(()).unwrap(); http_request.headers_mut().typed_insert(BaiscAuthorization::basic( diff --git a/nghe-backend/src/test/mock_impl/user.rs b/nghe-backend/src/test/mock_impl/user.rs index 2909a781..95b06e1f 100644 --- a/nghe-backend/src/test/mock_impl/user.rs +++ b/nghe-backend/src/test/mock_impl/user.rs @@ -5,7 +5,7 @@ use image::EncodableLayout; use nghe_api::auth; use uuid::Uuid; -use crate::http::extract::auth::header::BaiscAuthorization; +use crate::http::extract::auth::header::{BaiscAuthorization, BearerAuthorization}; use crate::orm::users; use crate::route::key; @@ -41,7 +41,22 @@ impl<'a> Mock<'a> { .unwrap() } - pub fn auth_header(&self) -> BaiscAuthorization { + pub async fn api_key(&self) -> auth::ApiKey { + key::create::handler(self.mock.database(), key::create::Request { + username: self.username(), + password: self.password(), + client: Faker.fake::(), + }) + .await + .unwrap() + .api_key + } + + pub async fn auth_bearer(&self) -> BearerAuthorization { + BearerAuthorization::bearer(&self.api_key().await.api_key.to_string()).unwrap() + } + + pub fn auth_basic(&self) -> BaiscAuthorization { BaiscAuthorization::basic(&self.username(), &self.password()) } @@ -52,13 +67,9 @@ impl<'a> Mock<'a> { &self, use_token: Option, ) -> auth::Form<'static, 'static, 'static, 'static> { - let username = self.username(); - let password = self.password(); - let client = Faker.fake::(); - if let Some(use_token) = use_token { - let username = username.into(); - let client = client.into(); + let username = self.username().into(); + let client = Faker.fake::().into(); if use_token { let salt: String = Faker.fake(); let token = auth::username::Token::new(self.password(), &salt); @@ -72,16 +83,7 @@ impl<'a> Mock<'a> { auth::Username { username, client, auth: self.password().into() }.into() } } else { - key::create::handler(self.mock.database(), key::create::Request { - username, - password, - client, - }) - .await - .unwrap() - .api_key - .api_key - .into() + self.api_key().await.api_key.into() } } }