Add authentication #1
|
@ -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();
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue