From 711c5a88fb517f50d2e03e10f4e3125a9fb5b5c1 Mon Sep 17 00:00:00 2001 From: Mitchell Marino Date: Wed, 12 Apr 2023 22:41:02 -0500 Subject: [PATCH] added auth to create_event endpoint --- Cargo.toml | 1 + Dockerfile | 1 + src/events.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++------ src/jwt.rs | 43 +++++++++++++++++++++++++++++++++++--- src/main.rs | 13 ++++++++---- src/user.rs | 38 +++++++++++++++++++++------------- 6 files changed, 126 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f158bc3..f0ea626 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] tokio = { version = "1.27", features = ["full"] } axum = "0.6" +axum-auth = { version = "0.4", features = ["auth-bearer"]} sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-rustls", "all-types"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/Dockerfile b/Dockerfile index 901ee96..e9a7702 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,5 +8,6 @@ RUN cargo install --path . FROM debian:buster-slim COPY --from=build /usr/local/cargo/bin/school_app_api /usr/local/bin/school_app_api +ENV DATABASE_URL "postgres://school_app_api_user:school_app_api_pass@school_app_db/school_app_api" CMD ["school_app_api"] diff --git a/src/events.rs b/src/events.rs index c648d4e..8fa281d 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,4 +1,11 @@ -use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use axum_auth::AuthBearer; +use jsonwebtoken::{Algorithm, Validation}; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::{ @@ -6,7 +13,7 @@ use sqlx::{ types::{chrono::NaiveDateTime, BigDecimal}, }; -use crate::AppState; +use crate::{jwt::Claims, user::Role, AppState}; #[derive( sqlx::Type, Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, @@ -38,7 +45,6 @@ pub struct NewEventRequestEntry { place: Option, #[serde(with = "serialize_big_decimal")] price: BigDecimal, - created_by: Option, } #[derive(Clone, Serialize, Debug)] @@ -136,13 +142,20 @@ LIMIT } pub async fn create_event( + AuthBearer(token): AuthBearer, State(app_state): State, Json(event_post_query): Json, ) -> impl IntoResponse { + let token_data = match handle_token(token, &app_state, Role::Teacher) { + Ok(value) => value, + Err(value) => return value, + }; + let result = query!( r#" INSERT INTO events (title, description, time_start, time_end, event_type, points, place, price, created_by) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9 ) + RETURNING id "#, event_post_query.title, event_post_query.description, @@ -152,11 +165,11 @@ pub async fn create_event( event_post_query.points, event_post_query.place, event_post_query.price, - event_post_query.created_by, - ).execute(&app_state.db_pool).await; + token_data.id, + ).fetch_one(&app_state.db_pool).await; match result { - Ok(_) => (StatusCode::OK, "").into_response(), + Ok(record) => (StatusCode::OK, Json(json!({ "data": record.id }))).into_response(), Err(err) => ( StatusCode::BAD_REQUEST, Json(json!({ @@ -166,3 +179,35 @@ pub async fn create_event( .into_response(), } } + +fn handle_token( + token: String, + app_state: &AppState, + minimum_role: Role, +) -> Result { + let token_result = jsonwebtoken::decode::( + &token, + &app_state.jwt_decode, + &Validation::new(Algorithm::HS256), + ); + let token_data = match token_result { + Ok(token_data) => token_data.claims, + Err(err) => { + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({ "error": format!("Failed to parse JWT: {}", err) })), + ) + .into_response()) + } + }; + if token_data.role < minimum_role { + return Err(( + StatusCode::UNAUTHORIZED, + Json(json!({ + "error": format!("You must be at least a {:?} to do this", minimum_role) + })), + ) + .into_response()); + } + Ok(token_data) +} diff --git a/src/jwt.rs b/src/jwt.rs index 4f14dc4..650d931 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -1,13 +1,50 @@ -use jsonwebtoken::EncodingKey; +use std::time::Duration; + +use jsonwebtoken::{DecodingKey, EncodingKey}; use serde::{Deserialize, Serialize}; +use sqlx::types::time::OffsetDateTime; + +use crate::user::Role; + +pub const JWT_LIFETIME: Duration = Duration::from_secs(60 * 60 * 24 * 7); #[derive(Clone, Serialize, Deserialize, Debug, Hash)] pub struct Claims { + #[serde(with = "jwt_numeric_date")] + pub exp: OffsetDateTime, pub id: i32, pub username: String, + pub role: Role, } -pub fn get_jwt_secret() -> EncodingKey { +pub fn get_jwt_keys() -> (EncodingKey, DecodingKey) { let secret = std::env::var("SAAPI_JWT_SECRET").unwrap_or("Sdb\\y5PP`,hmG+98".into()); - EncodingKey::from_secret(secret.as_bytes()) + ( + EncodingKey::from_secret(secret.as_bytes()), + DecodingKey::from_secret(secret.as_bytes()), + ) +} + +mod jwt_numeric_date { + //! Custom serialization of OffsetDateTime to conform with the JWT spec (RFC 7519 section 2, "Numeric Date") + use serde::{self, Deserialize, Deserializer, Serializer}; + use sqlx::types::time::OffsetDateTime; + + /// Serializes an OffsetDateTime to a Unix timestamp (milliseconds since 1970/1/1T00:00:00T) + pub fn serialize(date: &OffsetDateTime, serializer: S) -> Result + where + S: Serializer, + { + let timestamp = date.unix_timestamp(); + serializer.serialize_i64(timestamp) + } + + /// Attempts to deserialize an i64 and use as a Unix timestamp + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + OffsetDateTime::from_unix_timestamp(i64::deserialize(deserializer)?) + .map_err(|_| serde::de::Error::custom("invalid Unix timestamp value")) + } } diff --git a/src/main.rs b/src/main.rs index 7ab5c0b..cf5f2f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use axum::{ routing::{get, post}, Router, }; -use jsonwebtoken::EncodingKey; +use jsonwebtoken::{DecodingKey, EncodingKey}; use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use std::net::SocketAddr; @@ -15,7 +15,8 @@ use user::{signin, signup}; #[derive(Clone)] pub struct AppState { pub db_pool: PgPool, - pub jwt_key: EncodingKey, + pub jwt_encode: EncodingKey, + pub jwt_decode: DecodingKey, } #[tokio::main] @@ -27,7 +28,7 @@ async fn main() { let db_url = std::env::var("DATABASE_URL") .expect("Set `DATABASE_URL` to the url of the postgres database."); - let jwt_key = jwt::get_jwt_secret(); + let (jwt_encode, jwt_decode) = jwt::get_jwt_keys(); let db_pool = PgPoolOptions::new() .max_connections(50) @@ -42,7 +43,11 @@ async fn main() { .route("/user/signin", post(signin)) .route("/event", post(events::create_event)) .route("/event/preview", get(events::get_events_preview)) - .with_state(AppState { db_pool, jwt_key }); + .with_state(AppState { + db_pool, + jwt_encode, + jwt_decode, + }); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` diff --git a/src/user.rs b/src/user.rs index 280d894..25da2e8 100644 --- a/src/user.rs +++ b/src/user.rs @@ -2,8 +2,12 @@ use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; use jsonwebtoken::Header; use serde::{Deserialize, Serialize}; use serde_json::json; +use sqlx::types::time::OffsetDateTime; -use crate::{jwt::Claims, AppState}; +use crate::{ + jwt::{Claims, JWT_LIFETIME}, + AppState, +}; #[derive(Clone, Debug, Deserialize)] pub struct Signup { @@ -88,7 +92,10 @@ pub async fn signin( let result = sqlx::query!( r#" - SELECT id, username + SELECT + id, + username, + role AS "role!: Role" FROM users WHERE username = $1 AND password = $2 "#, @@ -101,21 +108,24 @@ pub async fn signin( match result { Ok(Some(user)) => { let claims = Claims { + exp: OffsetDateTime::now_utc() + JWT_LIFETIME, id: user.id, username: user.username, + role: user.role, }; - let token = match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_key) - { - Ok(token) => token, - Err(err) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ - "error": format!("Unknown error signing in when creating JWT: {}", err) - })), - ) - } - }; + let token = + match jsonwebtoken::encode(&Header::default(), &claims, &app_state.jwt_encode) { + Ok(token) => token, + Err(err) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": + format!("Unknown error signing in when creating JWT: {}", err) + })), + ) + } + }; return (StatusCode::OK, Json(json!({ "data": token }))); }