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;
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/<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
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<()> {
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)
}

View File

@ -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<UserInfo, ()> {
// LocalUser::get_user_by_uuid()
Outcome::Forward(())
fn from_request(request: &'a Request<'r>) -> Outcome<UserInfo, UserError> {
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::<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 {
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<UserInfo, ()> {
unimplemented!()
pub fn get_user_by_uuid(conn: &DbConn, request_user_id: String) -> Result<UserInfo, UserError> {
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> {
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_bytes()))
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
}
}