session authentication guards
This commit is contained in:
parent
7be74e7b7e
commit
75ab376ebd
|
@ -1,5 +1,5 @@
|
|||
CREATE TABLE magic_links (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY UNIQUE,
|
||||
id VARCHAR(39) NOT NULL PRIMARY KEY UNIQUE,
|
||||
user_id SERIAL NOT NULL REFERENCES users(id),
|
||||
expires_on INTEGER NOT NULL
|
||||
);
|
|
@ -1,5 +1,5 @@
|
|||
CREATE TABLE session_tokens (
|
||||
id VARCHAR(36) NOT NULL PRIMARY KEY,
|
||||
id VARCHAR(39) NOT NULL PRIMARY KEY,
|
||||
user_id SERIAL NOT NULL REFERENCES users(id),
|
||||
expires_on INTEGER NOT NULL
|
||||
);
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE auth_tokens (
|
||||
id VARCHAR(39) NOT NULL PRIMARY KEY,
|
||||
user_id SERIAL NOT NULL REFERENCES users(id),
|
||||
session_token VARCHAR(39) NOT NULL REFERENCES session_tokens(id)
|
||||
);
|
|
@ -0,0 +1,124 @@
|
|||
use rocket::http::Status;
|
||||
use rocket::{Request, State};
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use sqlx::PgPool;
|
||||
use crate::tokens::{validate_auth_token, validate_session_token};
|
||||
|
||||
pub struct PartialUserInfo {
|
||||
pub user_id: i32,
|
||||
pub created_at: i64,
|
||||
pub email: String,
|
||||
pub hasTotp: bool
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AuthenticationError {
|
||||
MissingToken,
|
||||
InvalidToken,
|
||||
DatabaseError,
|
||||
RequiresTOTP
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for PartialUserInfo {
|
||||
type Error = AuthenticationError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let headers = req.headers();
|
||||
|
||||
// make sure the bearer token exists
|
||||
if let Some(authorization) = headers.get_one("Authorization") {
|
||||
// parse bearer token
|
||||
let components = authorization.split(' ').collect::<Vec<&str>>();
|
||||
|
||||
if components.len() != 2 || components.len() != 3 {
|
||||
return Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken));
|
||||
}
|
||||
|
||||
if components[0] != "Bearer" {
|
||||
return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken));
|
||||
}
|
||||
|
||||
if components.len() == 2 && components[1].starts_with("st-") {
|
||||
return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken));
|
||||
}
|
||||
|
||||
let st: String;
|
||||
let user_id: i64;
|
||||
let at: Option<String>;
|
||||
|
||||
match &components[1][..3] {
|
||||
"st-" => {
|
||||
// validate session token
|
||||
st = components[1].to_string();
|
||||
match validate_session_token(st.clone(), req.rocket().state().unwrap()).await {
|
||||
Ok(uid) => user_id = uid,
|
||||
Err(_) => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken))
|
||||
}
|
||||
},
|
||||
_ => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken))
|
||||
}
|
||||
|
||||
if components.len() == 3 {
|
||||
match &components[2][..3] {
|
||||
"at-" => {
|
||||
// validate auth token
|
||||
at = Some(components[2].to_string());
|
||||
match validate_auth_token(at.clone().unwrap().clone(), st.clone(), req.rocket().state().unwrap()).await {
|
||||
Ok(_) => (),
|
||||
Err(_) => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken))
|
||||
}
|
||||
},
|
||||
_ => return Outcome::Failure((Status::Unauthorized, AuthenticationError::InvalidToken))
|
||||
}
|
||||
} else {
|
||||
at = None;
|
||||
}
|
||||
|
||||
// this user is 100% valid and authenticated, fetch their info
|
||||
|
||||
let user = match sqlx::query!("SELECT * FROM users WHERE id = $1", user_id.clone() as i32).fetch_one(req.rocket().state().unwrap()).await {
|
||||
Ok(u) => u,
|
||||
Err(_) => return Outcome::Failure((Status::InternalServerError, AuthenticationError::DatabaseError))
|
||||
};
|
||||
|
||||
Outcome::Success(PartialUserInfo {
|
||||
user_id: user_id as i32,
|
||||
created_at: user.created_on as i64,
|
||||
email: user.email,
|
||||
hasTotp: at.is_some(),
|
||||
})
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, AuthenticationError::MissingToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TOTPAuthenticatedUserInfo {
|
||||
pub user_id: i32,
|
||||
pub created_at: i64,
|
||||
pub email: String,
|
||||
}
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for TOTPAuthenticatedUserInfo {
|
||||
type Error = AuthenticationError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let userinfo = PartialUserInfo::from_request(request).await;
|
||||
match userinfo {
|
||||
Outcome::Failure(e) => Outcome::Failure(e),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
Outcome::Success(s) => {
|
||||
if s.hasTotp {
|
||||
Outcome::Success(Self {
|
||||
user_id: s.user_id,
|
||||
created_at: s.created_at,
|
||||
email: s.email,
|
||||
})
|
||||
} else {
|
||||
Outcome::Failure((Status::Unauthorized, AuthenticationError::RequiresTOTP))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ pub mod db;
|
|||
pub mod config;
|
||||
pub mod tokens;
|
||||
pub mod routes;
|
||||
pub mod auth;
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!();
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ use std::time::UNIX_EPOCH;
|
|||
|
||||
// https://admin.defined.net/auth/magic-link?email=coredoescode%40gmail.com&token=ml-ckBsgw_5IdK5VYgseBYcoV_v_cQjtdq1re_RhDu_MKg
|
||||
pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFConfig) -> Result<(), Box<dyn Error>> {
|
||||
let otp = Uuid::new_v4().to_string();
|
||||
let otp = format!("ml-{}", Uuid::new_v4().to_string());
|
||||
let otp_url = config.base.join(&format!("/auth/magic-link?email={}&token={}", urlencoding::encode(&email.clone()), otp.clone())).unwrap();
|
||||
sqlx::query!("INSERT INTO magic_links (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", otp, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.magic_links_valid_for as i32).execute(db).await?;
|
||||
// TODO: send email
|
||||
|
@ -16,8 +16,22 @@ pub async fn send_magic_link(id: i64, email: String, db: &PgPool, config: &TFCon
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn generate_session_token(id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
|
||||
let token = Uuid::new_v4().to_string();
|
||||
sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
|
||||
pub async fn generate_session_token(user_id: i64, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
|
||||
let token = format!("st-{}", Uuid::new_v4());
|
||||
sqlx::query!("INSERT INTO session_tokens (id, user_id, expires_on) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, user_id as i32, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32 + config.session_tokens_valid_for as i32).execute(db).await?;
|
||||
Ok(token)
|
||||
}
|
||||
pub async fn validate_session_token(token: String, db: &PgPool) -> Result<i64, Box<dyn Error>> {
|
||||
Ok(sqlx::query!("SELECT user_id FROM session_tokens WHERE id = $1 AND expires_on < $2", token, SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i32).fetch_one(db).await?.user_id as i64)
|
||||
}
|
||||
|
||||
pub async fn generate_auth_token(user_id: i64, session_id: String, db: &PgPool, config: &TFConfig) -> Result<String, Box<dyn Error>> {
|
||||
let token = format!("at-{}", Uuid::new_v4());
|
||||
sqlx::query!("INSERT INTO auth_tokens (id, session_token, user_id) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING;", token, session_id, user_id as i32).execute(db).await?;
|
||||
Ok(token)
|
||||
}
|
||||
pub async fn validate_auth_token(token: String, session_id: String, db: &PgPool) -> Result<(), Box<dyn Error>> {
|
||||
validate_session_token(token.clone(), db).await?;
|
||||
sqlx::query!("SELECT * FROM auth_tokens WHERE id = $1 AND session_token = $2", token, session_id).fetch_one(db).await?;
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue