Started moving stuff to common library

pull/2/head
Jef Roosens 2021-11-09 11:09:41 +01:00
parent b4a5ea4c0e
commit b423a57a9b
Signed by: Jef Roosens
GPG Key ID: 955C0660072F691F
7 changed files with 342 additions and 0 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

24
Cargo.toml 100644
View File

@ -0,0 +1,24 @@
[package]
name = "rb-common"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Backend web framework
rocket = { version = "0.5.0-rc.1", features = [ "json", "uuid" ] }
# Used to (de)serialize JSON
serde = { version = "1.0.127", features = [ "derive" ] }
rand = "0.8.4"
uuid = { version = "0.8.2", features = ["serde"] }
# Authentification
jwt = "0.14.0"
hmac = "*"
sha2 = "*"
# Timestamps for JWT tokens
chrono = { version = "*", features = [ "serde" ] }
# Encoding of refresh tokens
base64 = "0.13.0"
# Reading in configuration files
figment = { version = "*", features = [ "yaml" ] }

103
src/auth/jwt.rs 100644
View File

@ -0,0 +1,103 @@
use chrono::Utc;
use hmac::{Hmac, NewMac};
use jwt::SignWithKey;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::{
errors::{RbError, RbResult},
RbJwtConf,
};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JWTResponse {
token: String,
refresh_token: String,
}
#[derive(Serialize, Deserialize)]
pub struct Claims {
pub id: uuid::Uuid,
pub username: String,
pub admin: bool,
pub exp: i64,
}
pub fn generate_jwt_token(
jwt: &RbJwtConf,
id: uuid::Uuid,
username: String,
is_admin: bool,
) -> RbResult<JWTResponse> {
let key: Hmac<Sha256> = Hmac::new_from_slice(jwt.key.as_bytes())
.map_err(|_| RbError::Custom("Couldn't create Hmac key."))?;
let current_time = Utc::now();
// Create the claims
let claims = Claims {
id,
username: username.clone(),
admin: is_admin,
exp: current_time.timestamp() + jwt.refresh_token_expire,
};
// Sign the claims into a new token
let token = claims
.sign_with_key(&key)
.map_err(|_| RbError::Custom("Couldn't sign JWT."))?;
// Generate a random refresh token
let mut refresh_token = vec![0u8; jwt.refresh_token_size];
thread_rng().fill(&mut refresh_token[..]);
let refresh_expire =
(current_time + chrono::Duration::seconds(jwt.refresh_token_expire)).naive_utc();
Ok(JWTResponse {
token,
refresh_token: base64::encode(refresh_token),
})
}
pub fn refresh_token(
conn: &PgConnection,
jwt: &RbJwtConf,
refresh_token: &str,
) -> RbResult<JWTResponse> {
let token_bytes =
base64::decode(refresh_token).map_err(|_| RbError::AuthInvalidRefreshToken)?;
// First, we request the token from the database to see if it's really a valid token
let (token_entry, user) =
db::tokens::find_with_user(conn, &token_bytes).ok_or(RbError::AuthInvalidRefreshToken)?;
// If we see that the token has already been used before, we block the user.
if token_entry.last_used_at.is_some() {
// If we fail to block the user, the end user must know
if let Err(err) = db::users::block(conn, token_entry.user_id) {
return Err(err);
}
return Err(RbError::AuthDuplicateRefreshToken);
}
// Then we check if the user is blocked
if user.blocked {
return Err(RbError::AuthBlockedUser);
}
// Now we check if the token has already expired
let cur_time = Utc::now().naive_utc();
if token_entry.expires_at < cur_time {
return Err(RbError::AuthTokenExpired);
}
// We update the last_used_at value for the refresh token
db::tokens::update_last_used_at(conn, &token_entry.token, cur_time)?;
generate_jwt_token(conn, jwt, &user)
}

1
src/auth/mod.rs 100644
View File

@ -0,0 +1 @@
pub mod jwt;

94
src/errors.rs 100644
View File

@ -0,0 +1,94 @@
use rocket::{
http::Status,
request::Request,
response::{self, Responder},
serde::json::json,
};
#[derive(Debug)]
pub enum RbError
{
AuthUnknownUser,
AuthBlockedUser,
AuthInvalidPassword,
AuthUnauthorized,
AuthTokenExpired,
AuthRefreshTokenExpired,
AuthInvalidRefreshToken,
AuthDuplicateRefreshToken,
AuthMissingHeader,
// UM = User Management
UMDuplicateUser,
UMUnknownUser,
DbError(&'static str),
Custom(&'static str),
}
impl RbError
{
pub fn status(&self) -> Status
{
// Every entry gets its own line for easy editing later when needed
match self {
RbError::AuthUnknownUser => Status::NotFound,
RbError::AuthBlockedUser => Status::Forbidden,
RbError::AuthInvalidPassword => Status::Unauthorized,
RbError::AuthUnauthorized => Status::Unauthorized,
RbError::AuthTokenExpired => Status::Unauthorized,
RbError::AuthRefreshTokenExpired => Status::Unauthorized,
RbError::AuthInvalidRefreshToken => Status::Unauthorized,
RbError::AuthDuplicateRefreshToken => Status::Unauthorized,
RbError::AuthMissingHeader => Status::BadRequest,
RbError::UMDuplicateUser => Status::Conflict,
RbError::Custom(_) => Status::InternalServerError,
_ => Status::InternalServerError,
}
}
pub fn message(&self) -> &'static str
{
match self {
RbError::AuthUnknownUser => "This user doesn't exist.",
RbError::AuthBlockedUser => "This user is blocked.",
RbError::AuthInvalidPassword => "Invalid credentials.",
RbError::AuthUnauthorized => "You are not authorized to access this resource.",
RbError::AuthTokenExpired => "This token is not valid anymore.",
RbError::AuthRefreshTokenExpired => "This refresh token is not valid anymore.",
RbError::AuthInvalidRefreshToken => "This refresh token is not valid.",
RbError::AuthDuplicateRefreshToken => {
"This refresh token has already been used. The user has been blocked."
},
RbError::AuthMissingHeader => "Missing Authorization header.",
RbError::UMDuplicateUser => "This user already exists.",
RbError::Custom(message) => message,
_ => "",
}
}
}
impl<'r> Responder<'r, 'static> for RbError
{
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static>
{
let status = self.status();
let content = json!({
"status": status.code,
"message": self.message(),
});
// TODO add status to response
content.respond_to(req)
}
}
/// Type alias for results that can return an RbError
pub type RbResult<T> = std::result::Result<T, RbError>;
/// Type alias for optional results that can fail & return an RbError
pub type RbOption<T> = RbResult<Option<T>>;

115
src/guards.rs 100644
View File

@ -0,0 +1,115 @@
use hmac::{Hmac, NewMac};
use jwt::VerifyWithKey;
use rocket::{
http::Status,
outcome::try_outcome,
request::{FromRequest, Outcome, Request},
State,
};
use sha2::Sha256;
use crate::{auth::jwt::Claims, errors::RbError, RbConfig};
/// Extracts an "Authorization: Bearer" string from the headers.
pub struct Bearer<'a>(&'a str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Bearer<'r>
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
// If the header isn't present, just forward to the next route
let header = match req.headers().get_one("Authorization") {
None => return Outcome::Forward(()),
Some(val) => val,
};
if header.starts_with("Bearer ") {
match header.get(7..) {
Some(s) => Outcome::Success(Self(s)),
None => Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized)),
}
} else {
Outcome::Forward(())
}
}
}
/// Verifies the provided JWT is valid.
pub struct Jwt(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Jwt
{
type Error = RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let bearer = try_outcome!(req.guard::<Bearer>().await).0;
let config = try_outcome!(req.guard::<&State<RbConfig>>().await.map_failure(|_| (
Status::InternalServerError,
RbError::Custom("Couldn't get config guard.")
)));
let key: Hmac<Sha256> = match Hmac::new_from_slice(&config.jwt.key.as_bytes()) {
Ok(key) => key,
Err(_) => {
return Outcome::Failure((
Status::InternalServerError,
Self::Error::Custom("Failed to do Hmac thing."),
))
},
};
// Verify token using key
match bearer.verify_with_key(&key) {
Ok(claims) => Outcome::Success(Self(claims)),
Err(_) => {
return Outcome::Failure((Status::Unauthorized, Self::Error::AuthUnauthorized))
},
}
}
}
/// Verifies the JWT has not expired.
pub struct User(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for User
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let claims = try_outcome!(req.guard::<Jwt>().await).0;
// Verify key hasn't yet expired
if chrono::Utc::now().timestamp() > claims.exp {
Outcome::Failure((Status::Forbidden, Self::Error::AuthTokenExpired))
} else {
Outcome::Success(Self(claims))
}
}
}
/// Verifies the JWT belongs to an admin.
pub struct Admin(Claims);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Admin
{
type Error = crate::errors::RbError;
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error>
{
let user = try_outcome!(req.guard::<User>().await).0;
if user.admin {
Outcome::Success(Self(user))
} else {
Outcome::Failure((Status::Unauthorized, RbError::AuthUnauthorized))
}
}
}

3
src/lib.rs 100644
View File

@ -0,0 +1,3 @@
pub mod auth;
pub mod errors;
pub mod guards;