Skip to content

Commit

Permalink
backend: add bearer authorization header (#656)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnghia authored Jan 12, 2025
1 parent 4ecdb5f commit d51c299
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 36 deletions.
19 changes: 14 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions nghe-backend/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
71 changes: 58 additions & 13 deletions nghe-backend/src/http/extract/auth/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,8 +19,21 @@ pub struct Header<R> {
pub user: users::Authenticated,
}

pub type BearerAuthorization = headers::Authorization<headers::authorization::Bearer>;
pub type BaiscAuthorization = headers::Authorization<headers::authorization::Basic>;

impl Authentication for BearerAuthorization {
async fn authenticated(&self, database: &Database) -> Result<users::Authenticated, Error> {
auth::ApiKey::from(
self.token()
.parse::<Uuid>()
.map_err(|_| error::Kind::InvalidBearerAuthorizationFormat)?,
)
.authenticated(database)
.await
}
}

impl username::Authentication for BaiscAuthorization {
fn username(&self) -> &str {
self.username()
Expand All @@ -38,11 +53,14 @@ where
type Rejection = Error;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let header = parts
.headers
.typed_get::<BaiscAuthorization>()
.ok_or_else(|| error::Kind::MissingAuthenticationHeader)?;
Ok(Self { _request: PhantomData, user: header.login::<S, R>(state).await? })
let user = if let Some(header) = parts.headers.typed_get::<BearerAuthorization>() {
header.login::<S, R>(state).await?
} else if let Some(header) = parts.headers.typed_get::<BaiscAuthorization>() {
header.login::<S, R>(state).await?
} else {
return error::Kind::MissingAuthenticationHeader.into();
};
Ok(Self { _request: PhantomData, user })
}
}

Expand All @@ -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::<String>();
Expand All @@ -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::<Uuid>().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::<Request>::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(
Expand Down
38 changes: 20 additions & 18 deletions nghe-backend/src/test/mock_impl/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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::<String>(),
})
.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())
}

Expand All @@ -52,13 +67,9 @@ impl<'a> Mock<'a> {
&self,
use_token: Option<bool>,
) -> auth::Form<'static, 'static, 'static, 'static> {
let username = self.username();
let password = self.password();
let client = Faker.fake::<String>();

if let Some(use_token) = use_token {
let username = username.into();
let client = client.into();
let username = self.username().into();
let client = Faker.fake::<String>().into();
if use_token {
let salt: String = Faker.fake();
let token = auth::username::Token::new(self.password(), &salt);
Expand All @@ -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()
}
}
}

0 comments on commit d51c299

Please sign in to comment.