Moved auth code to gateway

This commit is contained in:
Jef Roosens 2021-11-09 10:42:16 +01:00
parent 4c67a43553
commit 26e2f5f3b1
Signed by: Jef Roosens
GPG key ID: 955C0660072F691F
12 changed files with 2473 additions and 2 deletions

118
src/auth/jwt.rs Normal file
View file

@ -0,0 +1,118 @@
use chrono::Utc;
use diesel::PgConnection;
use hmac::{Hmac, NewMac};
use jwt::SignWithKey;
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::{
db,
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(
conn: &PgConnection,
jwt: &RbJwtConf,
user: &db::User,
) -> 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: user.id,
username: user.username.clone(),
admin: user.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();
// Store refresh token in database
db::tokens::create(
conn,
&db::NewRefreshToken {
token: refresh_token.to_vec(),
user_id: user.id,
expires_at: refresh_expire,
},
)?;
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)
}

68
src/auth/mod.rs Normal file
View file

@ -0,0 +1,68 @@
use rocket::{serde::json::Json, State};
use serde::Deserialize;
use self::{
jwt::{generate_jwt_token, JWTResponse},
pass::verify_user,
};
use crate::{errors::RbResult, guards::User, RbConfig, RbDbConn};
pub mod jwt;
pub mod pass;
#[derive(Deserialize)]
pub struct Credentials
{
username: String,
password: String,
}
#[post("/login")]
pub async fn already_logged_in(_user: User) -> String
{
String::from("You're already logged in!")
}
#[post("/login", data = "<credentials>", rank = 2)]
pub async fn login(
conn: RbDbConn,
conf: &State<RbConfig>,
credentials: Json<Credentials>,
) -> RbResult<Json<JWTResponse>>
{
let credentials = credentials.into_inner();
let jwt = conf.jwt.clone();
// Get the user, if credentials are valid
let user = conn
.run(move |c| verify_user(c, &credentials.username, &credentials.password))
.await?;
Ok(Json(
conn.run(move |c| generate_jwt_token(c, &jwt, &user))
.await?,
))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshTokenRequest
{
pub refresh_token: String,
}
#[post("/refresh", data = "<refresh_token_request>")]
pub async fn refresh_token(
conn: RbDbConn,
conf: &State<RbConfig>,
refresh_token_request: Json<RefreshTokenRequest>,
) -> RbResult<Json<JWTResponse>>
{
let refresh_token = refresh_token_request.into_inner().refresh_token;
let jwt = conf.jwt.clone();
Ok(Json(
conn.run(move |c| crate::auth::jwt::refresh_token(c, &jwt, &refresh_token))
.await?,
))
}

36
src/auth/pass.rs Normal file
View file

@ -0,0 +1,36 @@
use argon2::verify_encoded;
use diesel::PgConnection;
use rand::{thread_rng, Rng};
use crate::{
db,
errors::{RbError, RbResult},
};
pub fn verify_user(conn: &PgConnection, username: &str, password: &str) -> RbResult<db::User>
{
// TODO handle non-"NotFound" Diesel errors accordingely
let user = db::users::find_by_username(conn, username).map_err(|_| RbError::AuthUnknownUser)?;
// Check if a user is blocked
if user.blocked {
return Err(RbError::AuthBlockedUser);
}
match verify_encoded(user.password.as_str(), password.as_bytes()) {
Ok(true) => Ok(user),
_ => Err(RbError::AuthInvalidPassword),
}
}
pub fn hash_password(password: &str) -> RbResult<String>
{
// Generate a random salt
let mut salt = [0u8; 64];
thread_rng().fill(&mut salt[..]);
// Encode the actual password
let config = argon2::Config::default();
argon2::hash_encoded(password.as_bytes(), &salt, &config)
.map_err(|_| RbError::Custom("Couldn't hash password."))
}

101
src/main.rs Normal file
View file

@ -0,0 +1,101 @@
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate diesel;
use figment::{
providers::{Env, Format, Yaml},
Figment,
};
use rocket::{
fairing::AdHoc,
http::Status,
serde::json::{json, Value},
Build, Orbit, Request, Rocket,
};
use rocket_sync_db_pools::database;
use serde::{Deserialize, Serialize};
pub mod auth;
// pub mod db;
// pub mod errors;
// pub mod guards;
pub(crate) mod schema;
#[database("postgres_rb")]
pub struct RbDbConn(diesel::PgConnection);
#[catch(default)]
fn default_catcher(status: Status, _: &Request) -> Value
{
json!({"status": status.code, "message": ""})
}
embed_migrations!();
async fn run_db_migrations(rocket: Rocket<Build>) -> Result<Rocket<Build>, Rocket<Build>>
{
let conn = RbDbConn::get_one(&rocket)
.await
.expect("database connection");
conn.run(|c| match embedded_migrations::run(c) {
Ok(()) => Ok(rocket),
Err(_) => Err(rocket),
})
.await
}
async fn create_admin_user<'a>(rocket: &'a Rocket<Orbit>)
{
let config = rocket.state::<RbConfig>().expect("RbConfig instance");
let admin_user = config.admin_user.clone();
let admin_pass = config.admin_pass.clone();
let conn = RbDbConn::get_one(&rocket)
.await
.expect("database connection");
conn.run(move |c| {
admin::create_admin_user(c, &admin_user, &admin_pass).expect("failed to create admin user")
})
.await;
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct RbJwtConf
{
key: String,
refresh_token_size: usize,
refresh_token_expire: i64,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct RbConfig
{
admin_user: String,
admin_pass: String,
jwt: RbJwtConf,
}
#[launch]
fn rocket() -> _
{
let figment = Figment::from(rocket::config::Config::default())
.merge(Yaml::file("Rb.yaml").nested())
.merge(Env::prefixed("RB_").global());
rocket::custom(figment)
.attach(RbDbConn::fairing())
.attach(AdHoc::try_on_ignite(
"Run database migrations",
run_db_migrations,
))
// .attach(AdHoc::try_on_ignite("Create admin user", create_admin_user))
.attach(AdHoc::config::<RbConfig>())
.register("/", catchers![default_catcher])
.mount(
"/api/auth",
routes![auth::already_logged_in, auth::login, auth::refresh_token,],
)
}

49
src/schema.rs Normal file
View file

@ -0,0 +1,49 @@
table! {
posts (id) {
id -> Uuid,
section_id -> Uuid,
title -> Nullable<Varchar>,
publish_date -> Date,
content -> Text,
}
}
table! {
refresh_tokens (token) {
token -> Bytea,
user_id -> Uuid,
expires_at -> Timestamp,
last_used_at -> Nullable<Timestamp>,
}
}
table! {
sections (id) {
id -> Uuid,
title -> Varchar,
shortname -> Varchar,
description -> Nullable<Text>,
is_default -> Bool,
has_titles -> Bool,
}
}
table! {
users (id) {
id -> Uuid,
username -> Varchar,
password -> Text,
blocked -> Bool,
admin -> Bool,
}
}
joinable!(posts -> sections (section_id));
joinable!(refresh_tokens -> users (user_id));
allow_tables_to_appear_in_same_query!(
posts,
refresh_tokens,
sections,
users,
);