Refactor password auth to use Bearer token
This commit is contained in:
parent
f46f456db0
commit
2da5ba107f
27
backend/Cargo.lock
generated
27
backend/Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
52
backend/src/docs.rs
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue