Implement route to generate a new session
This commit is contained in:
parent
6057c8295d
commit
e6615454cd
16 changed files with 295 additions and 193 deletions
|
@ -1,11 +1,9 @@
|
||||||
DROP INDEX IF EXISTS status_name_uniq_idx;
|
DROP INDEX IF EXISTS status_name_uniq_idx;
|
||||||
DROP INDEX IF EXISTS user_auth_token_hash_uniq_idx;
|
|
||||||
DROP INDEX IF EXISTS user_username_uniq_idx;
|
DROP INDEX IF EXISTS user_username_uniq_idx;
|
||||||
DROP INDEX IF EXISTS user_email_uniq_idx;
|
DROP INDEX IF EXISTS user_email_uniq_idx;
|
||||||
DROP INDEX IF EXISTS permission_name_uniq_idx;
|
DROP INDEX IF EXISTS permission_name_uniq_idx;
|
||||||
|
|
||||||
DROP TABLE IF EXISTS public.user_permission;
|
DROP TABLE IF EXISTS public.user_permission;
|
||||||
DROP TABLE IF EXISTS public.permission;
|
DROP TABLE IF EXISTS public.permission;
|
||||||
DROP TABLE IF EXISTS public.user_auth_token;
|
|
||||||
DROP TABLE IF EXISTS public.user;
|
DROP TABLE IF EXISTS public.user;
|
||||||
DROP TABLE IF EXISTS public.status CASCADE;
|
DROP TABLE IF EXISTS public.status CASCADE;
|
||||||
|
|
|
@ -16,7 +16,7 @@ VALUES
|
||||||
('Active'),
|
('Active'),
|
||||||
('Unverified'),
|
('Unverified'),
|
||||||
('Removed'),
|
('Removed'),
|
||||||
('Quaranteened');
|
('Quarantined');
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS
|
CREATE TABLE IF NOT EXISTS
|
||||||
public.user (
|
public.user (
|
||||||
|
@ -33,17 +33,6 @@ CREATE TABLE IF NOT EXISTS
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_username_uniq_idx ON public.user(username);
|
CREATE UNIQUE INDEX IF NOT EXISTS user_username_uniq_idx ON public.user(username);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_email_uniq_idx ON public.user(email);
|
CREATE UNIQUE INDEX IF NOT EXISTS user_email_uniq_idx ON public.user(email);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS
|
|
||||||
public.user_auth_token (
|
|
||||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL REFERENCES public.user(id),
|
|
||||||
token_hash VARCHAR(256) NOT NULL,
|
|
||||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS user_auth_token_hash_uniq_idx ON public.user_auth_token(token_hash);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS
|
CREATE TABLE IF NOT EXISTS
|
||||||
public.permission (
|
public.permission (
|
||||||
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
use std::{
|
use std::fmt::Debug;
|
||||||
fmt::Debug,
|
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
use blake3::Hash;
|
use sqlx::prelude::*;
|
||||||
use serde::Serialize;
|
|
||||||
use sqlx::prelude::FromRow;
|
|
||||||
use time::OffsetDateTime;
|
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::models::AppError;
|
use crate::models::AppError;
|
||||||
|
@ -98,75 +92,3 @@ pub async fn verify_user(pool: &DbPool, user_id: i32) -> Result<(), AppError> {
|
||||||
})
|
})
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct NewUserAuthTokenEntity {
|
|
||||||
pub user_id: i32,
|
|
||||||
pub token_hash: Hash,
|
|
||||||
pub expires_at: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, FromRow)]
|
|
||||||
pub struct TmpUserAuthTokenEntity {
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub token_hash: String,
|
|
||||||
pub expires_at: OffsetDateTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub struct UserAuthTokenEntity {
|
|
||||||
pub id: i32,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub token_hash: String,
|
|
||||||
pub expires_at: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TmpUserAuthTokenEntity> for UserAuthTokenEntity {
|
|
||||||
fn from(other: TmpUserAuthTokenEntity) -> Self {
|
|
||||||
let TmpUserAuthTokenEntity {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
token_hash,
|
|
||||||
expires_at,
|
|
||||||
} = other;
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
user_id,
|
|
||||||
token_hash,
|
|
||||||
expires_at: UNIX_EPOCH
|
|
||||||
.checked_add(Duration::new(
|
|
||||||
expires_at.clone().unix_timestamp().try_into().unwrap(),
|
|
||||||
expires_at.nanosecond(),
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn insert_new_user_auth_token(
|
|
||||||
pool: &DbPool,
|
|
||||||
new_user_auth_token: NewUserAuthTokenEntity,
|
|
||||||
) -> Result<UserAuthTokenEntity, AppError> {
|
|
||||||
let NewUserAuthTokenEntity {
|
|
||||||
user_id,
|
|
||||||
token_hash,
|
|
||||||
expires_at,
|
|
||||||
} = new_user_auth_token.clone();
|
|
||||||
|
|
||||||
let token_hash = token_hash.to_string();
|
|
||||||
let expires_at = OffsetDateTime::from(expires_at);
|
|
||||||
|
|
||||||
sqlx::query_as::<_, TmpUserAuthTokenEntity>("INSERT INTO public.user_auth_token (user_id, token_hash, expires_at) VALUES ($1, $2, $3) RETURNING id, user_id, token_hash, expires_at;")
|
|
||||||
.bind(user_id)
|
|
||||||
.bind(token_hash)
|
|
||||||
.bind(expires_at)
|
|
||||||
.fetch_one(pool)
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
error!(?err, record = ?new_user_auth_token, "Cannot insert new user auth token record");
|
|
||||||
AppError::from(err)
|
|
||||||
})
|
|
||||||
.map(From::from)
|
|
||||||
}
|
|
||||||
|
|
|
@ -45,6 +45,10 @@ impl AppError {
|
||||||
Self::new(ErrorKind::InvalidToken)
|
Self::new(ErrorKind::InvalidToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn missing_authorization_token() -> Self {
|
||||||
|
Self::new(ErrorKind::MissingAuthorizationToken)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
pub fn missing_environment_variables(missing_vars: Vec<&'static str>) -> Self {
|
||||||
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
Self::new(ErrorKind::MissingEnvironmentVariables(missing_vars))
|
||||||
}
|
}
|
||||||
|
@ -129,6 +133,7 @@ impl Display for AppError {
|
||||||
ErrorKind::ExpiredToken => write!(f, "The provided token has expired"),
|
ErrorKind::ExpiredToken => write!(f, "The provided token has expired"),
|
||||||
ErrorKind::InvalidPassword => write!(f, "Invalid password"),
|
ErrorKind::InvalidPassword => write!(f, "Invalid password"),
|
||||||
ErrorKind::InvalidToken => write!(f, "The provided token is invalid"),
|
ErrorKind::InvalidToken => write!(f, "The provided token is invalid"),
|
||||||
|
ErrorKind::MissingAuthorizationToken => write!(f, "Missing authorization token"),
|
||||||
ErrorKind::MissingEnvironmentVariables(missing_vars) => write!(
|
ErrorKind::MissingEnvironmentVariables(missing_vars) => write!(
|
||||||
f,
|
f,
|
||||||
"Missing required environment variables: {}",
|
"Missing required environment variables: {}",
|
||||||
|
@ -163,6 +168,7 @@ enum ErrorKind {
|
||||||
InvalidToken,
|
InvalidToken,
|
||||||
MissingEnvironmentVariables(Vec<&'static str>),
|
MissingEnvironmentVariables(Vec<&'static str>),
|
||||||
MissingSessionField(&'static str),
|
MissingSessionField(&'static str),
|
||||||
|
MissingAuthorizationToken,
|
||||||
NoDbRecordFound,
|
NoDbRecordFound,
|
||||||
NoSessionFound,
|
NoSessionFound,
|
||||||
Sqlx(SqlxError),
|
Sqlx(SqlxError),
|
||||||
|
@ -180,7 +186,9 @@ impl IntoResponse for AppError {
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
ApiResponse::new_with_error(self).into_json_response(),
|
ApiResponse::new_with_error(self).into_json_response(),
|
||||||
),
|
),
|
||||||
&ErrorKind::ExpiredToken | &ErrorKind::NoSessionFound => (
|
&ErrorKind::ExpiredToken
|
||||||
|
| &ErrorKind::MissingAuthorizationToken
|
||||||
|
| &ErrorKind::NoSessionFound => (
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
ApiResponse::new_with_error(self).into_json_response(),
|
ApiResponse::new_with_error(self).into_json_response(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::{async_trait, extract::FromRequestParts};
|
||||||
|
use http::request::Parts;
|
||||||
use humantime::format_rfc3339;
|
use humantime::format_rfc3339;
|
||||||
use redis::ToRedisArgs;
|
use redis::ToRedisArgs;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
requests::AppState,
|
||||||
|
services::{
|
||||||
|
auth_token::{self, verify_token},
|
||||||
|
user_session,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::AppError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Session {
|
pub struct Session {
|
||||||
|
@ -28,12 +41,29 @@ impl Session {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Session {
|
#[async_trait]
|
||||||
fn default() -> Self {
|
impl FromRequestParts<AppState> for Session {
|
||||||
Self {
|
type Rejection = AppError;
|
||||||
user_id: Default::default(),
|
|
||||||
created_at: UNIX_EPOCH,
|
async fn from_request_parts<'a, 'b>(
|
||||||
expires_at: UNIX_EPOCH,
|
parts: &'a mut Parts,
|
||||||
}
|
state: &'b AppState,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
let cache_pool = state.cache_pool();
|
||||||
|
let token_key = state.env().token_key();
|
||||||
|
|
||||||
|
let trusted_token_str = auth_token::extract_token_string_from_http_headers(&parts.headers)?;
|
||||||
|
let trusted_token = verify_token(token_key, trusted_token_str, None)?;
|
||||||
|
|
||||||
|
let token_id = trusted_token
|
||||||
|
.payload_claims()
|
||||||
|
.and_then(|claims| claims.get_claim("kid"))
|
||||||
|
.ok_or(AppError::invalid_token())
|
||||||
|
.map(|value| value.as_str().unwrap())
|
||||||
|
.and_then(|token_id| Uuid::try_from(token_id).map_err(|_| AppError::invalid_token()))?;
|
||||||
|
|
||||||
|
user_session::get_user_session(cache_pool, token_id)
|
||||||
|
.await
|
||||||
|
.and_then(|session| session.ok_or(AppError::no_session_found()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,17 +11,14 @@ use pasetors::{keys::SymmetricKey, version4::V4};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{get_username_and_password_by_username, DbPool, UserIdAndHashedPasswordEntity},
|
||||||
get_username_and_password_by_username, insert_new_user_auth_token, DbPool,
|
|
||||||
NewUserAuthTokenEntity, UserIdAndHashedPasswordEntity,
|
|
||||||
},
|
|
||||||
models::{ApiResponse, AppError, Session},
|
models::{ApiResponse, AppError, Session},
|
||||||
requests::{
|
requests::{
|
||||||
auth::login::models::{AuthLoginResponse, AuthLoginTokenData},
|
auth::login::models::{AuthLoginResponse, AuthLoginTokenData},
|
||||||
AppState,
|
AppState,
|
||||||
},
|
},
|
||||||
services::{
|
services::{
|
||||||
auth_token::{generate_auth_token, generate_session_token},
|
auth_token::{generate_auth_token, generate_session_token, store_user_auth_token},
|
||||||
user_session, verify_password, CachePool,
|
user_session, verify_password, CachePool,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -55,34 +52,8 @@ async fn auth_login_request(
|
||||||
|
|
||||||
verify_password(password, hashed_password)?;
|
verify_password(password, hashed_password)?;
|
||||||
|
|
||||||
let (session_token, session_token_id, session_token_expiration) =
|
let ((session_token, session_token_expiration), (auth_token, auth_token_expiration)) =
|
||||||
generate_session_token(token_key, user_id);
|
generate_login_auth_and_session_tokens(cache_pool, token_key, user_id).await?;
|
||||||
|
|
||||||
let (auth_token, auth_token_id, auth_token_expiration) =
|
|
||||||
generate_auth_token(token_key, user_id);
|
|
||||||
|
|
||||||
insert_new_user_auth_token(
|
|
||||||
db_pool,
|
|
||||||
NewUserAuthTokenEntity {
|
|
||||||
user_id,
|
|
||||||
token_hash: blake3::hash(auth_token_id.as_bytes()),
|
|
||||||
expires_at: auth_token_expiration,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let session = Session {
|
|
||||||
user_id,
|
|
||||||
created_at: SystemTime::now(),
|
|
||||||
expires_at: auth_token_expiration,
|
|
||||||
};
|
|
||||||
|
|
||||||
let expiration = session_token_expiration
|
|
||||||
.duration_since(SystemTime::now())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
user_session::store_user_session(cache_pool, session_token_id, session, Some(expiration))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = AuthLoginResponse {
|
let response = AuthLoginResponse {
|
||||||
user_id,
|
user_id,
|
||||||
|
@ -102,3 +73,42 @@ async fn auth_login_request(
|
||||||
)
|
)
|
||||||
.into_response())
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn generate_login_auth_and_session_tokens(
|
||||||
|
cache_pool: &CachePool,
|
||||||
|
token_key: &SymmetricKey<V4>,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<((String, SystemTime), (String, SystemTime)), AppError> {
|
||||||
|
let (session_token, session_token_id, session_token_expiration) =
|
||||||
|
generate_session_token(token_key, user_id);
|
||||||
|
|
||||||
|
let (auth_token, _, auth_token_expiration) = generate_auth_token(token_key, user_id);
|
||||||
|
|
||||||
|
store_user_auth_token(
|
||||||
|
cache_pool,
|
||||||
|
user_id,
|
||||||
|
auth_token.as_str(),
|
||||||
|
auth_token_expiration
|
||||||
|
.duration_since(SystemTime::now())
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let session = Session {
|
||||||
|
user_id,
|
||||||
|
created_at: SystemTime::now(),
|
||||||
|
expires_at: auth_token_expiration,
|
||||||
|
};
|
||||||
|
|
||||||
|
let expiration = session_token_expiration
|
||||||
|
.duration_since(SystemTime::now())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
user_session::store_user_session(cache_pool, session_token_id, session, Some(expiration))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
(session_token, session_token_expiration),
|
||||||
|
(auth_token, auth_token_expiration),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
@ -1,13 +1,28 @@
|
||||||
use axum::{routing::post, Router};
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
use login::auth_login_post_handler;
|
use login::auth_login_post_handler;
|
||||||
|
use session::auth_session_get_handler;
|
||||||
|
|
||||||
use super::AppState;
|
use super::AppState;
|
||||||
|
|
||||||
mod login;
|
mod login;
|
||||||
|
mod session;
|
||||||
|
|
||||||
|
pub use login::generate_login_auth_and_session_tokens;
|
||||||
|
|
||||||
pub fn requests(state: AppState) -> Router {
|
pub fn requests(state: AppState) -> Router {
|
||||||
Router::new().route(
|
Router::new().nest(
|
||||||
"/auth/login",
|
"/auth",
|
||||||
post(auth_login_post_handler).with_state(state),
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/login",
|
||||||
|
post(auth_login_post_handler).with_state(state.clone()),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/session",
|
||||||
|
get(auth_session_get_handler).with_state(state.clone()),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
76
api/src/requests/auth/session/handler.rs
Normal file
76
api/src/requests/auth/session/handler.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
debug_handler,
|
||||||
|
extract::State,
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use http::{HeaderMap, StatusCode};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
models::{ApiResponse, AppError, Session},
|
||||||
|
requests::AppState,
|
||||||
|
services::{
|
||||||
|
auth_token::{self, generate_session_token, get_if_auth_token_exists, verify_token},
|
||||||
|
user_session,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::models::AuthSessionResponse;
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
pub async fn auth_session_get_handler(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let cache_pool = state.cache_pool();
|
||||||
|
let token_key = state.env().token_key();
|
||||||
|
|
||||||
|
let auth_token_str = auth_token::extract_token_string_from_http_headers(&headers)?;
|
||||||
|
let auth_token = verify_token(token_key, auth_token_str, None)?;
|
||||||
|
|
||||||
|
let user_id = auth_token
|
||||||
|
.payload_claims()
|
||||||
|
.and_then(|claims| claims.get_claim("sub"))
|
||||||
|
.and_then(|user_id| user_id.as_str())
|
||||||
|
.ok_or(AppError::invalid_token())
|
||||||
|
.and_then(|user_id| {
|
||||||
|
user_id
|
||||||
|
.parse::<i32>()
|
||||||
|
.map_err(|_| AppError::invalid_token())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let auth_token_exists =
|
||||||
|
get_if_auth_token_exists(cache_pool, user_id, auth_token_str.to_string().as_str()).await?;
|
||||||
|
|
||||||
|
if !auth_token_exists {
|
||||||
|
return Err(AppError::no_session_found());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (session_token, session_token_id, session_token_expiration) =
|
||||||
|
generate_session_token(token_key, user_id);
|
||||||
|
|
||||||
|
let expiration = session_token_expiration
|
||||||
|
.duration_since(SystemTime::now())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let new_session = Session {
|
||||||
|
user_id,
|
||||||
|
created_at: SystemTime::now(),
|
||||||
|
expires_at: session_token_expiration,
|
||||||
|
};
|
||||||
|
|
||||||
|
user_session::store_user_session(cache_pool, session_token_id, new_session, Some(expiration))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::CREATED,
|
||||||
|
ApiResponse::new(AuthSessionResponse {
|
||||||
|
token: session_token,
|
||||||
|
expiration: session_token_expiration,
|
||||||
|
})
|
||||||
|
.into_json_response(),
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
|
}
|
4
api/src/requests/auth/session/mod.rs
Normal file
4
api/src/requests/auth/session/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
mod handler;
|
||||||
|
mod models;
|
||||||
|
|
||||||
|
pub use handler::*;
|
3
api/src/requests/auth/session/models/mod.rs
Normal file
3
api/src/requests/auth/session/models/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
mod response;
|
||||||
|
|
||||||
|
pub use response::*;
|
12
api/src/requests/auth/session/models/response.rs
Normal file
12
api/src/requests/auth/session/models/response.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AuthSessionResponse {
|
||||||
|
pub token: String,
|
||||||
|
|
||||||
|
#[serde(serialize_with = "humantime_serde::serialize")]
|
||||||
|
pub expiration: SystemTime,
|
||||||
|
}
|
|
@ -111,13 +111,13 @@ async fn register_new_user_request(
|
||||||
});
|
});
|
||||||
|
|
||||||
UserRegistrationResponse {
|
UserRegistrationResponse {
|
||||||
id: user_id,
|
user_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
session_token: None,
|
session_token: None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
UserRegistrationResponse {
|
UserRegistrationResponse {
|
||||||
id: user_id,
|
user_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
session_token: Some(verification_token),
|
session_token: Some(verification_token),
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use serde_with::{serde_as, skip_serializing_none};
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct UserRegistrationResponse {
|
pub struct UserRegistrationResponse {
|
||||||
pub id: i32,
|
pub user_id: i32,
|
||||||
|
|
||||||
#[serde(serialize_with = "humantime_serde::serialize")]
|
#[serde(serialize_with = "humantime_serde::serialize")]
|
||||||
pub expires_at: SystemTime,
|
pub expires_at: SystemTime,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{str::FromStr, time::SystemTime};
|
use std::str::FromStr;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
debug_handler,
|
debug_handler,
|
||||||
|
@ -12,13 +12,13 @@ use tracing::{debug, error};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{insert_new_user_auth_token, verify_user, DbPool, NewUserAuthTokenEntity},
|
db::{verify_user, DbPool},
|
||||||
models::{ApiResponse, AppError, Session},
|
models::{ApiResponse, AppError},
|
||||||
requests::{user::verify::UserVerifyGetResponseTokenAndExpiration, AppState},
|
requests::{
|
||||||
services::{
|
auth::generate_login_auth_and_session_tokens,
|
||||||
auth_token::{generate_auth_token, generate_session_token, verify_token},
|
user::verify::UserVerifyGetResponseTokenAndExpiration, AppState,
|
||||||
user_session, CachePool,
|
|
||||||
},
|
},
|
||||||
|
services::{auth_token::verify_token, user_session, CachePool},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{UserVerifyGetParams, UserVerifyGetResponse};
|
use super::{UserVerifyGetParams, UserVerifyGetResponse};
|
||||||
|
@ -81,46 +81,14 @@ async fn verify_new_user_request(
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// TODO: Add new auth token to database, cache session access token
|
verify_user(db_pool, user_id)
|
||||||
let (session_token, session_token_id, session_token_expiration) =
|
.await
|
||||||
generate_session_token(token_key, user_id);
|
.inspect_err(|err| error!(?err))?;
|
||||||
let (auth_token, auth_token_id, auth_token_expiration) =
|
|
||||||
generate_auth_token(token_key, user_id);
|
|
||||||
|
|
||||||
let session = Session {
|
let ((session_token, session_token_expiration), (auth_token, auth_token_expiration)) =
|
||||||
user_id,
|
generate_login_auth_and_session_tokens(cache_pool, token_key, user_id).await?;
|
||||||
created_at: SystemTime::now(),
|
|
||||||
expires_at: session_token_expiration,
|
|
||||||
};
|
|
||||||
|
|
||||||
user_session::store_user_session(
|
let response = UserVerifyGetResponse {
|
||||||
cache_pool,
|
|
||||||
session_token_id,
|
|
||||||
session,
|
|
||||||
Some(
|
|
||||||
session_token_expiration
|
|
||||||
.duration_since(SystemTime::now())
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
insert_new_user_auth_token(
|
|
||||||
db_pool,
|
|
||||||
NewUserAuthTokenEntity {
|
|
||||||
user_id,
|
|
||||||
token_hash: blake3::hash(auth_token_id.as_bytes()),
|
|
||||||
expires_at: auth_token_expiration,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let response = verify_token(
|
|
||||||
token_key,
|
|
||||||
verification_token.as_str(),
|
|
||||||
Some(validation_rules),
|
|
||||||
)
|
|
||||||
.map(|_| UserVerifyGetResponse {
|
|
||||||
user_id,
|
user_id,
|
||||||
session: UserVerifyGetResponseTokenAndExpiration {
|
session: UserVerifyGetResponseTokenAndExpiration {
|
||||||
token: session_token,
|
token: session_token,
|
||||||
|
@ -130,11 +98,7 @@ async fn verify_new_user_request(
|
||||||
token: auth_token,
|
token: auth_token,
|
||||||
expires_at: auth_token_expiration,
|
expires_at: auth_token_expiration,
|
||||||
},
|
},
|
||||||
})?;
|
};
|
||||||
|
|
||||||
verify_user(db_pool, user_id)
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| error!(?err))?;
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use http::{header::AUTHORIZATION, HeaderMap};
|
||||||
|
use humantime::format_rfc3339_seconds;
|
||||||
use pasetors::{
|
use pasetors::{
|
||||||
claims::{Claims, ClaimsValidationRules},
|
claims::{Claims, ClaimsValidationRules},
|
||||||
errors::{ClaimValidationError, Error as TokenError},
|
errors::{ClaimValidationError, Error as TokenError},
|
||||||
|
@ -15,10 +17,40 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::AppError;
|
use crate::models::AppError;
|
||||||
|
|
||||||
|
use super::{cache, CachePool};
|
||||||
|
|
||||||
|
static AUTH_TOKEN_CACHE_KEY_PREFIX: &'static str = "debt_pirate:auth:";
|
||||||
static ONE_DAY: Duration = Duration::from_secs(86_400);
|
static ONE_DAY: Duration = Duration::from_secs(86_400);
|
||||||
static ONE_HOUR: Duration = Duration::from_secs(3_600);
|
static ONE_HOUR: Duration = Duration::from_secs(3_600);
|
||||||
static FIFTEEN_MINUTES: Duration = Duration::from_secs(900);
|
static FIFTEEN_MINUTES: Duration = Duration::from_secs(900);
|
||||||
|
|
||||||
|
pub async fn store_user_auth_token(
|
||||||
|
cache_pool: &CachePool,
|
||||||
|
user_id: i32,
|
||||||
|
auth_token: &str,
|
||||||
|
expiration: Duration,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let key = make_key(user_id, auth_token);
|
||||||
|
let expires_at = SystemTime::now() + expiration;
|
||||||
|
|
||||||
|
cache::store_scalar(
|
||||||
|
cache_pool,
|
||||||
|
key.as_str(),
|
||||||
|
format_rfc3339_seconds(expires_at).to_string(),
|
||||||
|
Some(expiration),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_if_auth_token_exists(
|
||||||
|
cache_pool: &CachePool,
|
||||||
|
user_id: i32,
|
||||||
|
auth_token: &str,
|
||||||
|
) -> Result<bool, AppError> {
|
||||||
|
let key = make_key(user_id, auth_token);
|
||||||
|
cache::exists(cache_pool, key.as_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
pub fn verify_token(
|
pub fn verify_token(
|
||||||
key: &SymmetricKey<V4>,
|
key: &SymmetricKey<V4>,
|
||||||
token: &str,
|
token: &str,
|
||||||
|
@ -68,6 +100,22 @@ pub fn generate_new_user_token(key: &SymmetricKey<V4>, user_id: i32) -> (String,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_token_string_from_http_headers<'a>(
|
||||||
|
headers: &'a HeaderMap,
|
||||||
|
) -> Result<&'a str, AppError> {
|
||||||
|
headers
|
||||||
|
.get(AUTHORIZATION)
|
||||||
|
.ok_or(AppError::missing_authorization_token())
|
||||||
|
.map(|value| {
|
||||||
|
value
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.trim_start_matches("Bearer")
|
||||||
|
.trim_start_matches("bearer")
|
||||||
|
.trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn generate_token(
|
fn generate_token(
|
||||||
key: &SymmetricKey<V4>,
|
key: &SymmetricKey<V4>,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
|
@ -131,6 +179,11 @@ fn map_token_error(err: TokenError) -> AppError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn make_key(user_id: i32, token: &str) -> String {
|
||||||
|
let token_hash = blake3::hash(token.as_bytes());
|
||||||
|
format!("{AUTH_TOKEN_CACHE_KEY_PREFIX}{user_id}:{token_hash}")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
|
|
|
@ -67,6 +67,24 @@ pub async fn _get_object<K: FromRedisValue + Eq + Hash>(
|
||||||
Ok(hash_fields)
|
Ok(hash_fields)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn store_scalar<'a, V: ToRedisArgs + Send + Sync + 'a>(
|
||||||
|
cache_pool: &CachePool,
|
||||||
|
key: &str,
|
||||||
|
scalar: V,
|
||||||
|
expiration: Option<Duration>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let mut conn = get_connection_from_pool(cache_pool).await?;
|
||||||
|
let _: () = conn.set(key, scalar).await?;
|
||||||
|
|
||||||
|
if let Some(expiration) = expiration {
|
||||||
|
let _: () = conn
|
||||||
|
.expire(key, expiration.as_secs().try_into().unwrap())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn exists(cache_pool: &CachePool, key: &str) -> Result<bool, AppError> {
|
pub async fn exists(cache_pool: &CachePool, key: &str) -> Result<bool, AppError> {
|
||||||
let mut conn = get_connection_from_pool(cache_pool).await?;
|
let mut conn = get_connection_from_pool(cache_pool).await?;
|
||||||
conn.exists(key).await.map_err(Into::into)
|
conn.exists(key).await.map_err(Into::into)
|
||||||
|
|
Loading…
Add table
Reference in a new issue