From c2973477d11f4112329d6be372c04d7295b5d396 Mon Sep 17 00:00:00 2001 From: nils <48135649+Nilstrieb@users.noreply.github.com> Date: Fri, 16 Jul 2021 15:05:38 +0200 Subject: [PATCH] may not actually really kind of works --- .env | 3 +- Cargo.lock | 180 ++++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 2 +- src/actions.rs | 13 ++-- src/auth.rs | 82 ++++++++++++++++++++++ src/errors.rs | 41 +++++++++++ src/handlers.rs | 35 +++++++--- src/main.rs | 16 +++-- 8 files changed, 335 insertions(+), 37 deletions(-) create mode 100644 src/auth.rs diff --git a/.env b/.env index f33bd02..7e4676a 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -DATABASE_URL=postgres://postgres:hugo123@localhost/karldbauth \ No newline at end of file +DATABASE_URL=postgres://postgres:hugo123@localhost/karldbauth +JWT_SECRET=halloichbineinsecretundsosehrsicherlmao34567 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index ec4bc99..80f10d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,19 +333,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alcoholic_jwt" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d27226e01414833a0443a67862f5aaf1cf3536b39b11f8b7d30d0e31b29d38db" -dependencies = [ - "base64 0.10.1", - "openssl", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "async-trait" version = "0.1.50" @@ -422,6 +409,18 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bitflags" version = "1.2.1" @@ -448,6 +447,12 @@ dependencies = [ "libc", ] +[[package]] +name = "bumpalo" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" + [[package]] name = "byteorder" version = "1.4.3" @@ -1176,6 +1181,29 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "js-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32" +dependencies = [ + "base64 0.12.3", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "karlauth" version = "0.1.0" @@ -1184,12 +1212,12 @@ dependencies = [ "actix-service", "actix-web", "actix-web-httpauth", - "alcoholic_jwt", "chrono", "derive_more", "diesel", "dotenv", "futures 0.3.15", + "jsonwebtoken", "r2d2", "reqwest", "serde", @@ -1397,6 +1425,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.1", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1525,6 +1564,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1963,6 +2013,21 @@ dependencies = [ "quick-error", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rustc-demangle" version = "0.1.20" @@ -2144,6 +2209,17 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", +] + [[package]] name = "slab" version = "0.4.3" @@ -2176,6 +2252,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "string" version = "0.2.1" @@ -2568,6 +2650,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "1.7.2" @@ -2641,6 +2729,70 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasm-bindgen" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" + +[[package]] +name = "web-sys" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "widestring" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 6555c15..db59e08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,6 @@ serde = "1.0" serde_derive = "1.0" serde_json = "1.0" actix-service = "1.0.1" -alcoholic_jwt = "1.0.0" +jsonwebtoken = "7.2.0" reqwest = "0.9.22" actix-rt = "1.0.0" \ No newline at end of file diff --git a/src/actions.rs b/src/actions.rs index c05b756..8d53019 100644 --- a/src/actions.rs +++ b/src/actions.rs @@ -8,17 +8,16 @@ use diesel::{delete, insert_into}; type DbResult = Result; -pub fn get_all_users(db: &Pool) -> DbResult { +pub fn get_all_users(db: &Pool) -> DbResult> { use super::schema::users::dsl::*; let conn = db.get().unwrap(); - let items = users.load::(&conn)?; - Ok(items) + users.load::(&conn) } -pub fn get_user_by_id(db: &Pool, id: i32) -> DbResult { +pub fn get_user_by_id(db: &Pool, user_id: i32) -> DbResult { use super::schema::users::dsl::*; let conn = db.get().unwrap(); - users.find(id).get_result::(&conn) + users.find(user_id).get_result::(&conn) } pub fn add_user(db: &Pool, user: InputUser) -> DbResult { @@ -33,8 +32,8 @@ pub fn add_user(db: &Pool, user: InputUser) -> DbResult { insert_into(users).values(&new_user).get_result(&conn) } -pub fn delete_user(db: &Pool, id: i32) -> DbResult { +pub fn delete_user(db: &Pool, user_id: i32) -> DbResult { use super::schema::users::dsl::*; let conn = db.get().unwrap(); - delete(users.find(id)).execute(&conn) + delete(users.find(user_id)).execute(&conn) } diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..3f233aa --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,82 @@ +use crate::errors::ServiceError; +use crate::models::User; +use actix_web::dev::{Payload, ServiceRequest}; +use actix_web_httpauth::extractors::bearer::{BearerAuth, Config}; +use actix_web_httpauth::extractors::AuthenticationError; +use chrono::Utc; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Role { + None, + ReadAll, + WriteAll, + Admin, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Claims { + exp: usize, + uid: i32, + role: Role, +} + +pub async fn validator( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result { + let config = req + .app_data::() + .map(|data| data.get_ref().clone()) + .unwrap_or(Default::default()); + + match validate_token(credentials.token()) { + Ok(claims) => { + //req.extensions_mut().insert(claims); + Ok(req) + } + Err(err) => Err(AuthenticationError::from(config).into()), + } +} + +fn validate_token(token: &str) -> Result { + let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET env var"); + + let decoded = jsonwebtoken::decode::( + &token, + &DecodingKey::from_secret(secret.as_bytes()), + &Validation::new(Algorithm::HS512), + ) + .map_err(|_| ServiceError::JWTokenError)? + .claims; + + if decoded.exp > Utc::now().timestamp() as usize { + Err(ServiceError::TokenExpiredError) + } else { + Ok(decoded) + } +} + +pub fn create_jwt(user: &User) -> Result { + let expiration = Utc::now() + .checked_add_signed(chrono::Duration::weeks(10)) + .expect("valid timestamp") + .timestamp(); + + let claims = Claims { + exp: expiration as usize, + uid: user.id, + role: Role::ReadAll, + }; + + let secret = std::env::var("JWT_SECRET").expect("JWT_SECRET env var"); + + let header = Header::new(Algorithm::HS512); + jsonwebtoken::encode( + &header, + &claims, + &EncodingKey::from_secret(secret.as_bytes()), + ) + .map_err(|_| ServiceError::JWTCreationError) +} diff --git a/src/errors.rs b/src/errors.rs index e69de29..2e940ec 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -0,0 +1,41 @@ +use actix_web::{error::ResponseError, HttpResponse}; +use derive_more::Display; + +#[derive(Debug, Display)] +pub enum ServiceError { + #[display(fmt = "Internal Server Error")] + InternalServerError, + + #[display(fmt = "BadRequest: {}", _0)] + BadRequest(String), + + #[display(fmt = "JWT Creation Error")] + JWTCreationError, + + #[display(fmt = "JWT Error")] + JWTokenError, + + #[display(fmt = "No Permission Error")] + NoPermissionError, + + #[display(fmt = "Token Expired Error")] + TokenExpiredError, +} + +// impl ResponseError trait allows to convert our errors into http responses with appropriate data +impl ResponseError for ServiceError { + fn error_response(&self) -> HttpResponse { + match self { + ServiceError::InternalServerError => { + HttpResponse::InternalServerError().json("Internal Server Error, Please try later") + } + ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), + ServiceError::JWTCreationError => { + HttpResponse::InternalServerError().json("Could not fetch JWKS") + } + ServiceError::JWTokenError => HttpResponse::BadRequest().json("Invalid JWT"), + ServiceError::NoPermissionError => HttpResponse::Unauthorized().json("No permissions"), + ServiceError::TokenExpiredError => HttpResponse::Unauthorized().json("Token expired"), + } + } +} diff --git a/src/handlers.rs b/src/handlers.rs index 310fb9e..0222c13 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,10 +1,9 @@ use super::actions; -use super::models::{NewUser, User}; use super::Pool; -use actix_web::{web, HttpResponse, Responder}; -use diesel::dsl::{delete, insert_into}; +use crate::auth::create_jwt; +use crate::models::User; +use actix_web::{web, HttpResponse}; use serde::{Deserialize, Serialize}; -use std::vec::Vec; type HttpResult = Result; @@ -15,13 +14,21 @@ pub struct InputUser { pub email: String, } +#[derive(Debug, Serialize, Deserialize)] +struct UserWithToken { + pub user: User, + pub token: String, +} + +/// handler for `GET /users` pub async fn get_users(db: web::Data) -> HttpResult { Ok(web::block(move || actions::get_all_users(&db)) .await - .map(|user| user.into()) + .map(|users| HttpResponse::Ok().json(users)) .map_err(|_| HttpResponse::InternalServerError())?) } +/// handler for `GET /users/{id}` pub async fn get_user_by_id(db: web::Data, user_id: web::Path) -> HttpResult { Ok(web::block(move || actions::get_user_by_id(&db, *user_id)) .await @@ -29,15 +36,23 @@ pub async fn get_user_by_id(db: web::Data, user_id: web::Path) -> Htt .map_err(|_| HttpResponse::InternalServerError())?) } +/// handler for `POST /users` pub async fn add_user(db: web::Data, item: web::Json) -> HttpResult { - Ok(web::block(move || actions::add_user(&db, *item)) - .await - .map(|user| user.into()) - .map_err(|_| HttpResponse::InternalServerError())?) + Ok( + web::block(move || actions::add_user(&db, item.into_inner())) + .await + .map_err(|_| HttpResponse::InternalServerError()) + .map(|user| { + HttpResponse::Ok().json(UserWithToken { + token: create_jwt(&user).expect("Could not create JWT"), + user, + }) + })?, + ) } /// handler for `DELETE /users/{id}` -pub async fn delete_user(db: web::Data, user_id: web::Path) -> impl Responder { +pub async fn delete_user(db: web::Data, user_id: web::Path) -> HttpResult { Ok(web::block(move || actions::delete_user(&db, *user_id)) .await .map(|count| HttpResponse::Ok().body(format!("Deleted {} user.", count))) diff --git a/src/main.rs b/src/main.rs index 9a75144..3ad257d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ #[macro_use] extern crate diesel; -use actix_web::{dev::ServiceRequest, web, App, Error, HttpServer}; +use crate::auth::validator; +use actix_web::{web, App, Error, HttpServer}; +use actix_web_httpauth::middleware::HttpAuthentication; use diesel::prelude::*; use diesel::r2d2::{self, ConnectionManager}; mod actions; +mod auth; mod errors; mod handlers; mod models; @@ -25,12 +28,17 @@ async fn main() -> std::io::Result<()> { .expect("Failed to create pool."); HttpServer::new(move || { + let auth_middleware = HttpAuthentication::bearer(validator); App::new() .data(pool.clone()) - .route("/users", web::get().to(handlers::get_users)) - .route("/users/{id}", web::get().to(handlers::get_user_by_id)) .route("/users", web::post().to(handlers::add_user)) - .route("/users/{id}", web::delete().to(handlers::delete_user)) + .service( + web::scope("/users") + .wrap(auth_middleware) + .route("", web::get().to(handlers::get_users)) + .route("/{id}", web::get().to(handlers::get_user_by_id)) + .route("/{id}", web::delete().to(handlers::delete_user)), + ) }) .bind("127.0.0.1:8080")? .run()