From 5e08f2c2c85ca4ffcf2e9b22bb711277f3d38eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Sat, 27 Mar 2021 15:36:57 -0400 Subject: [PATCH] basic authentification check --- src/main.rs | 8 ++++- src/models/errors.rs | 8 +++-- src/models/users.rs | 86 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 90 insertions(+), 12 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6550319..f78f5fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ mod auth; mod schema; use models::errors::ErrorResponse; +use models::users::UserInfo; use auth::routes::*; @@ -27,7 +28,12 @@ use auth::routes::*; pub struct DbConn(diesel::SqliteConnection); #[get("/zones//records")] -fn get_zone_records(client: State>, zone: String) -> Result>, ErrorResponse<()>> { +fn get_zone_records( + client: State>, + _user_info: UserInfo, + zone: String +) -> Result>, ErrorResponse<()>> { + // TODO: Implement FromParam for Name let name = Name::from_utf8(&zone).unwrap(); diff --git a/src/models/errors.rs b/src/models/errors.rs index c8892f7..9a7d56b 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -54,8 +54,12 @@ impl<'r, T: Serialize> Responder<'r> for ErrorResponse { impl From for ErrorResponse<()> { fn from(e: UserError) -> Self { match e { - UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Incorrect password or username.".into()), - UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists.".into()), + UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Provided credentials or token do not match any existing user".into()), + UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists".into()), + UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()), + UserError::ExpiredToken => ErrorResponse::new(Status::Unauthorized, "The provided token has expired".into()), + UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()), + UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()), UserError::DbError(e) => make_500(e), UserError::PasswordError(e) => make_500(e) } diff --git a/src/models/users.rs b/src/models/users.rs index be1a6b7..49432d0 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -2,17 +2,29 @@ use uuid::Uuid; use diesel::prelude::*; use diesel::result::Error as DieselError; use diesel_derive_enum::DbEnum; -use rocket::request::{FromRequest, Request, Outcome}; +use rocket::request::{FromRequest, Request, Outcome, State}; use serde::{Serialize, Deserialize}; +use rocket::http::Status; use chrono::serde::ts_seconds; use chrono::prelude::{DateTime, Utc}; use chrono::Duration; // TODO: Maybe just use argon2 crate directly use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; -use jsonwebtoken::{encode, Header, EncodingKey, errors::Result as JwtResult}; +use jsonwebtoken::{ + encode, decode, + Header, Validation, + Algorithm as JwtAlgorithm, EncodingKey, DecodingKey, + errors::Result as JwtResult, + errors::ErrorKind as JwtErrorKind +}; use crate::schema::*; use crate::DbConn; +use crate::config::Config; + + +const BEARER: &'static str = "Bearer "; +const AUTH_HEADER: &'static str = "Authentication"; #[derive(Debug, DbEnum, Deserialize)] @@ -82,11 +94,44 @@ pub struct UserInfo { } impl<'a, 'r> FromRequest<'a, 'r> for UserInfo { - type Error = (); + type Error = UserError; - fn from_request(request: &'a Request<'r>) -> Outcome { - // LocalUser::get_user_by_uuid() - Outcome::Forward(()) + fn from_request(request: &'a Request<'r>) -> Outcome { + let auth_header = match request.headers().get_one(AUTH_HEADER) { + None => return Outcome::Forward(()), + Some(auth_header) => auth_header, + }; + + let token = if auth_header.starts_with(BEARER) { + auth_header.trim_start_matches(BEARER) + } else { + return Outcome::Failure((Status::BadRequest, UserError::MalformedHeader)) + }; + + // TODO: Better error handling + let config = request.guard::>().unwrap(); + let conn = request.guard::().unwrap(); + + let token_data = AuthClaims::decode( + token, &config.web_app.secret + ).map_err(|e| match e.into_kind() { + JwtErrorKind::ExpiredSignature => (Status::Unauthorized, UserError::ExpiredToken), + _ => (Status::BadRequest, UserError::BadToken), + }); + + let token_data = match token_data { + Err(e) => return Outcome::Failure(e), + Ok(data) => data + }; + + let user_id = token_data.sub; + let user_info = match LocalUser::get_user_by_uuid(&conn, user_id) { + Err(UserError::NotFound) => return Outcome::Failure((Status::NotFound, UserError::NotFound)), + Err(e) => return Outcome::Failure((Status::InternalServerError, e)), + Ok(d) => d, + }; + + return Outcome::Success(user_info) } } @@ -94,6 +139,10 @@ impl<'a, 'r> FromRequest<'a, 'r> for UserInfo { pub enum UserError { NotFound, UserExists, + BadToken, + ExpiredToken, + MalformedHeader, + PermissionDenied, DbError(DieselError), PasswordError(HasherError), } @@ -180,8 +229,19 @@ impl LocalUser { }) } - pub fn get_user_by_uuid(user_id: Uuid) -> Result { - unimplemented!() + pub fn get_user_by_uuid(conn: &DbConn, request_user_id: String) -> Result { + use crate::schema::localuser::dsl::*; + use crate::schema::user::dsl::*; + + let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser) + .filter(id.eq(request_user_id)) + .get_result(&**conn)?; + + Ok(UserInfo { + id: client_user.id, + role: client_user.role, + username: client_localuser.username, + }) } } @@ -200,7 +260,15 @@ impl AuthClaims { } } + pub fn decode(token: &str, secret: &str) -> JwtResult { + decode::( + token, + &DecodingKey::from_secret(secret.as_ref()), + &Validation::new(JwtAlgorithm::HS256) + ).and_then(|data| Ok(data.claims)) + } + pub fn encode(self, secret: &str) -> JwtResult { - encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_bytes())) + encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref())) } }