Add authentication #1

Merged
hannaeko merged 11 commits from feature/auth into master 2021-04-03 17:54:16 +02:00
3 changed files with 90 additions and 12 deletions
Showing only changes of commit 5e08f2c2c8 - Show all commits

View File

@ -20,6 +20,7 @@ mod auth;
mod schema; mod schema;
use models::errors::ErrorResponse; use models::errors::ErrorResponse;
use models::users::UserInfo;
use auth::routes::*; use auth::routes::*;
@ -27,7 +28,12 @@ use auth::routes::*;
pub struct DbConn(diesel::SqliteConnection); pub struct DbConn(diesel::SqliteConnection);
#[get("/zones/<zone>/records")] #[get("/zones/<zone>/records")]
fn get_zone_records(client: State<SyncClient<TcpClientConnection>>, zone: String) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> { fn get_zone_records(
client: State<SyncClient<TcpClientConnection>>,
_user_info: UserInfo,
zone: String
) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> {
// TODO: Implement FromParam for Name // TODO: Implement FromParam for Name
let name = Name::from_utf8(&zone).unwrap(); let name = Name::from_utf8(&zone).unwrap();

View File

@ -54,8 +54,12 @@ impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> {
impl From<UserError> for ErrorResponse<()> { impl From<UserError> for ErrorResponse<()> {
fn from(e: UserError) -> Self { fn from(e: UserError) -> Self {
match e { match e {
UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Incorrect password or username.".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::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::DbError(e) => make_500(e),
UserError::PasswordError(e) => make_500(e) UserError::PasswordError(e) => make_500(e)
} }

View File

@ -2,17 +2,29 @@ use uuid::Uuid;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use diesel_derive_enum::DbEnum; use diesel_derive_enum::DbEnum;
use rocket::request::{FromRequest, Request, Outcome}; use rocket::request::{FromRequest, Request, Outcome, State};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use rocket::http::Status;
use chrono::serde::ts_seconds; use chrono::serde::ts_seconds;
use chrono::prelude::{DateTime, Utc}; use chrono::prelude::{DateTime, Utc};
use chrono::Duration; use chrono::Duration;
// TODO: Maybe just use argon2 crate directly // TODO: Maybe just use argon2 crate directly
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; 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::schema::*;
use crate::DbConn; use crate::DbConn;
use crate::config::Config;
const BEARER: &'static str = "Bearer ";
const AUTH_HEADER: &'static str = "Authentication";
#[derive(Debug, DbEnum, Deserialize)] #[derive(Debug, DbEnum, Deserialize)]
@ -82,11 +94,44 @@ pub struct UserInfo {
} }
impl<'a, 'r> FromRequest<'a, 'r> for UserInfo { impl<'a, 'r> FromRequest<'a, 'r> for UserInfo {
type Error = (); type Error = UserError;
fn from_request(request: &'a Request<'r>) -> Outcome<UserInfo, ()> { fn from_request(request: &'a Request<'r>) -> Outcome<UserInfo, UserError> {
// LocalUser::get_user_by_uuid() let auth_header = match request.headers().get_one(AUTH_HEADER) {
Outcome::Forward(()) 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::<State<Config>>().unwrap();
let conn = request.guard::<DbConn>().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 { pub enum UserError {
NotFound, NotFound,
UserExists, UserExists,
BadToken,
ExpiredToken,
MalformedHeader,
PermissionDenied,
DbError(DieselError), DbError(DieselError),
PasswordError(HasherError), PasswordError(HasherError),
} }
@ -180,8 +229,19 @@ impl LocalUser {
}) })
} }
pub fn get_user_by_uuid(user_id: Uuid) -> Result<UserInfo, ()> { pub fn get_user_by_uuid(conn: &DbConn, request_user_id: String) -> Result<UserInfo, UserError> {
unimplemented!() 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<AuthClaims> {
decode::<AuthClaims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&Validation::new(JwtAlgorithm::HS256)
).and_then(|data| Ok(data.claims))
}
pub fn encode(self, secret: &str) -> JwtResult<String> { pub fn encode(self, secret: &str) -> JwtResult<String> {
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_bytes())) encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
} }
} }