Refactor password auth to use Bearer token

This commit is contained in:
Ben Grant 2023-05-14 02:10:22 +10:00
parent f46f456db0
commit 2da5ba107f
7 changed files with 113 additions and 52 deletions

27
backend/Cargo.lock generated
View file

@ -244,6 +244,7 @@ dependencies = [
"bitflags",
"bytes",
"futures-util",
"headers",
"http",
"http-body",
"hyper",
@ -599,6 +600,7 @@ name = "crabfit_backend"
version = "2.0.0"
dependencies = [
"axum",
"base64 0.21.0",
"bcrypt",
"chrono",
"common",
@ -1091,6 +1093,31 @@ dependencies = [
"hashbrown 0.12.3",
]
[[package]]
name = "headers"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
"base64 0.13.1",
"bitflags",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.3.3"

View file

@ -9,7 +9,7 @@ edition = "2021"
members = ["common", "adaptors/*"]
[dependencies]
axum = "0.6.18"
axum = { version = "0.6.18", features = ["headers"] }
serde = { version = "1.0.162", features = ["derive"] }
tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] }
common = { path = "common" }
@ -28,3 +28,4 @@ tower_governor = "0.0.4"
tower = "0.4.13"
utoipa = { version = "3.3.0", features = ["axum_extras", "preserve_order"] }
utoipa-swagger-ui = { version = "3.1.3", features = ["axum"] }
base64 = "0.21.0"

52
backend/src/docs.rs Normal file
View file

@ -0,0 +1,52 @@
use crate::payloads;
use crate::routes;
use utoipa::{
openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme},
Modify, OpenApi,
};
// OpenAPI documentation
#[derive(OpenApi)]
#[openapi(
info(title = "Crab Fit API"),
paths(
routes::get_stats::get_stats,
routes::create_event::create_event,
routes::get_event::get_event,
routes::get_people::get_people,
routes::get_person::get_person,
routes::update_person::update_person,
),
components(schemas(
payloads::StatsResponse,
payloads::EventResponse,
payloads::PersonResponse,
payloads::EventInput,
payloads::PersonInput,
)),
tags(
(name = "info"),
(name = "event"),
(name = "person"),
),
modifiers(&SecurityAddon),
)]
pub struct ApiDoc;
struct SecurityAddon;
// Add password auth spec
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
openapi.components.as_mut().unwrap().add_security_scheme(
"password",
SecurityScheme::Http(
HttpBuilder::new()
.scheme(HttpAuthScheme::Bearer)
.bearer_format("base64")
.build(),
),
);
}
}

View file

@ -16,6 +16,9 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer};
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::docs::ApiDoc;
mod docs;
mod errors;
mod payloads;
mod routes;
@ -33,35 +36,6 @@ async fn main() {
// Load env
dotenv::dotenv().ok();
#[derive(OpenApi)]
#[openapi(
info(title = "Crab Fit API"),
paths(
routes::get_stats::get_stats,
routes::create_event::create_event,
routes::get_event::get_event,
routes::get_people::get_people,
routes::get_person::get_person,
routes::update_person::update_person,
),
components(
schemas(
payloads::StatsResponse,
payloads::EventResponse,
payloads::PersonResponse,
payloads::EventInput,
payloads::GetPersonInput,
payloads::UpdatePersonInput,
),
),
tags(
(name = "info"),
(name = "event"),
(name = "person"),
),
)]
struct ApiDoc;
let shared_state = Arc::new(Mutex::new(ApiState {
adaptor: SqlAdaptor::new().await,
}));

View file

@ -70,12 +70,6 @@ impl From<Person> for PersonResponse {
}
#[derive(Deserialize, ToSchema)]
pub struct GetPersonInput {
pub password: Option<String>,
}
#[derive(Deserialize, ToSchema)]
pub struct UpdatePersonInput {
pub password: Option<String>,
pub struct PersonInput {
pub availability: Vec<String>,
}

View file

@ -1,12 +1,14 @@
use axum::{
extract::{self, Path},
Json,
headers::{authorization::Bearer, Authorization},
Json, TypedHeader,
};
use base64::{engine::general_purpose, Engine};
use common::{adaptor::Adaptor, person::Person};
use crate::{
errors::ApiError,
payloads::{ApiResult, GetPersonInput, PersonResponse},
payloads::{ApiResult, PersonResponse},
State,
};
@ -17,7 +19,7 @@ use crate::{
("event_id", description = "The ID of the event"),
("person_name", description = "The name of the person"),
),
request_body(content = GetPersonInput, description = "Person details"),
security((), ("password" = [])),
responses(
(status = 200, description = "Ok", body = PersonResponse),
(status = 401, description = "Incorrect password"),
@ -32,15 +34,12 @@ use crate::{
pub async fn get_person<A: Adaptor>(
extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>,
input: Option<Json<GetPersonInput>>,
bearer: Option<TypedHeader<Authorization<Bearer>>>,
) -> ApiResult<PersonResponse, A> {
let adaptor = &state.lock().await.adaptor;
// Get inputted password
let password = match input {
Some(Json(i)) => i.password,
None => None,
};
let password = parse_password(bearer);
let existing_people = adaptor
.get_people(event_id.clone())
@ -96,9 +95,20 @@ pub async fn get_person<A: Adaptor>(
}
}
pub fn parse_password(bearer: Option<TypedHeader<Authorization<Bearer>>>) -> Option<String> {
bearer.map(|TypedHeader(Authorization(b))| {
String::from_utf8(
general_purpose::STANDARD
.decode(b.token().trim())
.unwrap_or(vec![]),
)
.unwrap_or("".to_owned())
})
}
pub fn verify_password(person: &Person, raw: Option<String>) -> bool {
match &person.password_hash {
Some(hash) => bcrypt::verify(raw.unwrap_or(String::from("")), hash).unwrap_or(false),
Some(hash) => bcrypt::verify(raw.unwrap_or("".to_owned()), hash).unwrap_or(false),
// Specifically allow a user who doesn't have a password
// set to log in with or without any password input
None => true,

View file

@ -1,16 +1,17 @@
use axum::{
extract::{self, Path},
Json,
headers::{authorization::Bearer, Authorization},
Json, TypedHeader,
};
use common::{adaptor::Adaptor, person::Person};
use crate::{
errors::ApiError,
payloads::{ApiResult, PersonResponse, UpdatePersonInput},
payloads::{ApiResult, PersonInput, PersonResponse},
State,
};
use super::get_person::verify_password;
use super::get_person::{parse_password, verify_password};
#[utoipa::path(
patch,
@ -19,7 +20,8 @@ use super::get_person::verify_password;
("event_id", description = "The ID of the event"),
("person_name", description = "The name of the person"),
),
request_body(content = UpdatePersonInput, description = "Person details"),
security((), ("password" = [])),
request_body(content = PersonInput, description = "Person details"),
responses(
(status = 200, description = "Ok", body = PersonResponse),
(status = 401, description = "Incorrect password"),
@ -34,7 +36,8 @@ use super::get_person::verify_password;
pub async fn update_person<A: Adaptor>(
extract::State(state): State<A>,
Path((event_id, person_name)): Path<(String, String)>,
Json(input): Json<UpdatePersonInput>,
bearer: Option<TypedHeader<Authorization<Bearer>>>,
Json(input): Json<PersonInput>,
) -> ApiResult<PersonResponse, A> {
let adaptor = &state.lock().await.adaptor;
@ -56,7 +59,7 @@ pub async fn update_person<A: Adaptor>(
.ok_or(ApiError::NotFound)?;
// Verify password (if set)
if !verify_password(&existing_person, input.password) {
if !verify_password(&existing_person, parse_password(bearer)) {
return Err(ApiError::NotAuthorized);
}