user_sessions

This commit is contained in:
2025-11-01 22:45:46 +01:00
parent 1c5b920e13
commit 8efe2d19ba
10 changed files with 147 additions and 94 deletions
@@ -0,0 +1,26 @@
CREATE OR REPLACE FUNCTION shared.current_refresh_token_hash() RETURNS text AS $$
SELECT NULLIF(current_setting('papercrate.refresh_token_hash', true), '')
$$ LANGUAGE SQL STABLE;
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
USING (
tenant_id = shared.current_tenant_id()
OR (
shared.current_refresh_token_hash() IS NOT NULL
AND token_hash = shared.current_refresh_token_hash()
)
);
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
WITH CHECK (tenant_id = shared.current_tenant_id());
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
RENAME TO tenant_refresh_token_policy;
ALTER INDEX tenant.idx_user_sessions_user_id RENAME TO idx_refresh_tokens_user_id;
ALTER INDEX tenant.idx_user_sessions_token_hash RENAME TO idx_refresh_tokens_token_hash;
ALTER INDEX tenant.user_sessions_tenant_id_idx RENAME TO refresh_tokens_tenant_id_idx;
ALTER TABLE tenant.user_sessions RENAME TO refresh_tokens;
DROP FUNCTION IF EXISTS shared.current_user_session_hash();
@@ -0,0 +1,26 @@
ALTER TABLE tenant.refresh_tokens RENAME TO user_sessions;
ALTER INDEX tenant.idx_refresh_tokens_user_id RENAME TO idx_user_sessions_user_id;
ALTER INDEX tenant.idx_refresh_tokens_token_hash RENAME TO idx_user_sessions_token_hash;
ALTER INDEX tenant.refresh_tokens_tenant_id_idx RENAME TO user_sessions_tenant_id_idx;
ALTER POLICY tenant_refresh_token_policy ON tenant.user_sessions
RENAME TO tenant_user_session_policy;
CREATE OR REPLACE FUNCTION shared.current_user_session_hash() RETURNS text AS $$
SELECT NULLIF(current_setting('papercrate.user_session_hash', true), '')
$$ LANGUAGE SQL STABLE;
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
USING (
tenant_id = shared.current_tenant_id()
OR (
shared.current_user_session_hash() IS NOT NULL
AND token_hash = shared.current_user_session_hash()
)
);
ALTER POLICY tenant_user_session_policy ON tenant.user_sessions
WITH CHECK (tenant_id = shared.current_tenant_id());
DROP FUNCTION IF EXISTS shared.current_refresh_token_hash();
+4 -4
View File
@@ -596,9 +596,9 @@ pub struct NewDocumentCorrespondent {
} }
#[derive(Debug, Clone, Queryable, Identifiable, Associations)] #[derive(Debug, Clone, Queryable, Identifiable, Associations)]
#[diesel(table_name = refresh_tokens)] #[diesel(table_name = user_sessions)]
#[diesel(belongs_to(User))] #[diesel(belongs_to(User))]
pub struct RefreshToken { pub struct UserSession {
pub id: Uuid, pub id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub token_hash: String, pub token_hash: String,
@@ -611,8 +611,8 @@ pub struct RefreshToken {
} }
#[derive(Debug, Insertable)] #[derive(Debug, Insertable)]
#[diesel(table_name = refresh_tokens)] #[diesel(table_name = user_sessions)]
pub struct NewRefreshToken { pub struct NewUserSession {
pub id: Uuid, pub id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub token_hash: String, pub token_hash: String,
+52 -51
View File
@@ -27,24 +27,25 @@ use crate::{
}, },
error::{AppError, AppResult}, error::{AppError, AppResult},
models::{ models::{
ApiTokenCapability, MagicToken, MagicTokenKind, NewRefreshToken, NewUser, RefreshToken, ApiTokenCapability, MagicToken, MagicTokenKind, NewUser, NewUserSession, TenantStatus,
TenantStatus, User, User, UserSession,
}, },
schema::{ schema::{
magic_tokens::dsl as magic_dsl, refresh_tokens, tenants::dsl as tenant_dsl, magic_tokens::dsl as magic_dsl, tenants::dsl as tenant_dsl,
user_memberships::dsl as memberships_dsl, user_passkeys::dsl as passkey_dsl, users::dsl, user_memberships::dsl as memberships_dsl, user_passkeys::dsl as passkey_dsl, user_sessions,
users::dsl,
}, },
state::AppState, state::AppState,
tenants::{ tenants::{
apply_refresh_token_hash, apply_tenant_guc, apply_user_guc, clear_refresh_token_hash, apply_tenant_guc, apply_user_guc, apply_user_session_hash, clear_user_guc,
clear_user_guc, clear_user_session_hash,
}, },
}; };
use crate::schema::refresh_tokens::dsl as refresh_dsl; use crate::schema::user_sessions::dsl as session_dsl;
use webauthn_rs::prelude::RegisterPublicKeyCredential; use webauthn_rs::prelude::RegisterPublicKeyCredential;
const REFRESH_COOKIE_NAME: &str = "refresh_token"; const SESSION_COOKIE_NAME: &str = "refresh_token";
#[derive(Deserialize, ToSchema)] #[derive(Deserialize, ToSchema)]
pub struct LoginRequest { pub struct LoginRequest {
@@ -405,34 +406,34 @@ pub async fn refresh(
) -> AppResult<Response> { ) -> AppResult<Response> {
let cookies = jar.ok_or_else(AppError::unauthorized)?; let cookies = jar.ok_or_else(AppError::unauthorized)?;
let refresh_value = cookies let refresh_value = cookies
.get(REFRESH_COOKIE_NAME) .get(SESSION_COOKIE_NAME)
.ok_or_else(AppError::unauthorized)?; .ok_or_else(AppError::unauthorized)?;
let hashed = hash_refresh_token(refresh_value); let hashed = hash_session_token(refresh_value);
let mut conn = state.db_unscoped()?; let mut conn = state.db_unscoped()?;
let now = Utc::now(); let now = Utc::now();
let now_naive = now.naive_utc(); let now_naive = now.naive_utc();
apply_refresh_token_hash(&mut conn, &hashed)?; apply_user_session_hash(&mut conn, &hashed)?;
let token = match refresh_dsl::refresh_tokens let token = match session_dsl::user_sessions
.filter(refresh_dsl::token_hash.eq(&hashed)) .filter(session_dsl::token_hash.eq(&hashed))
.filter(refresh_dsl::revoked_at.is_null()) .filter(session_dsl::revoked_at.is_null())
.filter(refresh_dsl::expires_at.gt(now_naive)) .filter(session_dsl::expires_at.gt(now_naive))
.first::<RefreshToken>(&mut conn) .first::<UserSession>(&mut conn)
{ {
Ok(token) => token, Ok(token) => token,
Err(diesel::result::Error::NotFound) => return Err(AppError::unauthorized()), Err(diesel::result::Error::NotFound) => return Err(AppError::unauthorized()),
Err(err) => return Err(AppError::from(err)), Err(err) => return Err(AppError::from(err)),
}; };
clear_refresh_token_hash(&mut conn)?; clear_user_session_hash(&mut conn)?;
apply_tenant_guc(&mut conn, token.tenant_id)?; apply_tenant_guc(&mut conn, token.tenant_id)?;
clear_user_guc(&mut conn)?; clear_user_guc(&mut conn)?;
diesel::update(refresh_dsl::refresh_tokens.filter(refresh_dsl::id.eq(token.id))) diesel::update(session_dsl::user_sessions.filter(session_dsl::id.eq(token.id)))
.set(( .set((
refresh_dsl::revoked_at.eq(now_naive), session_dsl::revoked_at.eq(now_naive),
refresh_dsl::updated_at.eq(now_naive), session_dsl::updated_at.eq(now_naive),
)) ))
.execute(&mut conn)?; .execute(&mut conn)?;
@@ -518,17 +519,17 @@ pub async fn logout(
let mut rows_affected = 0; let mut rows_affected = 0;
if let Some(cookies) = jar { if let Some(cookies) = jar {
if let Some(value) = cookies.get(REFRESH_COOKIE_NAME) { if let Some(value) = cookies.get(SESSION_COOKIE_NAME) {
let hashed = hash_refresh_token(value); let hashed = hash_session_token(value);
rows_affected = diesel::update( rows_affected = diesel::update(
refresh_dsl::refresh_tokens session_dsl::user_sessions
.filter(refresh_dsl::token_hash.eq(hashed)) .filter(session_dsl::token_hash.eq(hashed))
.filter(refresh_dsl::user_id.eq(user.user_id)) .filter(session_dsl::user_id.eq(user.user_id))
.filter(refresh_dsl::revoked_at.is_null()), .filter(session_dsl::revoked_at.is_null()),
) )
.set(( .set((
refresh_dsl::revoked_at.eq(now), session_dsl::revoked_at.eq(now),
refresh_dsl::updated_at.eq(now), session_dsl::updated_at.eq(now),
)) ))
.execute(&mut conn) .execute(&mut conn)
.unwrap_or(0); .unwrap_or(0);
@@ -537,19 +538,19 @@ pub async fn logout(
if rows_affected == 0 { if rows_affected == 0 {
let _ = diesel::update( let _ = diesel::update(
refresh_dsl::refresh_tokens session_dsl::user_sessions
.filter(refresh_dsl::user_id.eq(user.user_id)) .filter(session_dsl::user_id.eq(user.user_id))
.filter(refresh_dsl::revoked_at.is_null()), .filter(session_dsl::revoked_at.is_null()),
) )
.set(( .set((
refresh_dsl::revoked_at.eq(now), session_dsl::revoked_at.eq(now),
refresh_dsl::updated_at.eq(now), session_dsl::updated_at.eq(now),
)) ))
.execute(&mut conn); .execute(&mut conn);
} }
let mut headers = HeaderMap::new(); let mut headers = HeaderMap::new();
headers.insert(SET_COOKIE, build_clear_refresh_cookie(&state)); headers.insert(SET_COOKIE, build_clear_session_cookie(&state));
Ok((headers, StatusCode::NO_CONTENT)) Ok((headers, StatusCode::NO_CONTENT))
} }
@@ -853,7 +854,7 @@ fn issue_session(
) -> AppResult<Response> { ) -> AppResult<Response> {
apply_tenant_guc(conn, tenant_id)?; apply_tenant_guc(conn, tenant_id)?;
clear_user_guc(conn)?; clear_user_guc(conn)?;
clear_refresh_token_hash(conn)?; clear_user_session_hash(conn)?;
let now = Utc::now(); let now = Utc::now();
let access_token = state let access_token = state
@@ -867,21 +868,21 @@ fn issue_session(
.first(conn) .first(conn)
.map_err(AppError::from)?; .map_err(AppError::from)?;
let refresh_value = generate_refresh_token(); let session_value = generate_session_token();
let refresh_hash = hash_refresh_token(&refresh_value); let session_hash = hash_session_token(&session_value);
let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days); let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days);
let new_refresh = NewRefreshToken { let new_session = NewUserSession {
id: Uuid::new_v4(), id: Uuid::new_v4(),
user_id: user.id, user_id: user.id,
token_hash: refresh_hash, token_hash: session_hash,
issued_at: now.naive_utc(), issued_at: now.naive_utc(),
expires_at: refresh_expires_at.naive_utc(), expires_at: refresh_expires_at.naive_utc(),
tenant_id, tenant_id,
}; };
diesel::insert_into(refresh_tokens::table) diesel::insert_into(user_sessions::table)
.values(&new_refresh) .values(&new_session)
.execute(conn)?; .execute(conn)?;
let mut response = Json(LoginResponse { let mut response = Json(LoginResponse {
@@ -897,36 +898,36 @@ fn issue_session(
response.headers_mut().insert( response.headers_mut().insert(
SET_COOKIE, SET_COOKIE,
build_refresh_cookie(state, &refresh_value, refresh_expires_at), build_session_cookie(state, &session_value, refresh_expires_at),
); );
Ok(response) Ok(response)
} }
fn hash_refresh_token(token: &str) -> String { fn hash_session_token(token: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(token.as_bytes()); hasher.update(token.as_bytes());
hex::encode(hasher.finalize()) hex::encode(hasher.finalize())
} }
fn hash_magic_token(token: &str) -> String { fn hash_magic_token(token: &str) -> String {
hash_refresh_token(token) hash_session_token(token)
} }
fn generate_refresh_token() -> String { fn generate_session_token() -> String {
let mut bytes = [0u8; 32]; let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes); OsRng.fill_bytes(&mut bytes);
hex::encode(bytes) hex::encode(bytes)
} }
fn build_refresh_cookie( fn build_session_cookie(
state: &AppState, state: &AppState,
token: &str, token: &str,
expires_at: chrono::DateTime<Utc>, expires_at: chrono::DateTime<Utc>,
) -> HeaderValue { ) -> HeaderValue {
let max_age = ChronoDuration::days(state.config.refresh_token_expiry_days).num_seconds(); let max_age = ChronoDuration::days(state.config.refresh_token_expiry_days).num_seconds();
let mut parts = vec![format!("{}={}", REFRESH_COOKIE_NAME, token)]; let mut parts = vec![format!("{}={}", SESSION_COOKIE_NAME, token)];
parts.push("Path=/".into()); parts.push("Path=/".into());
parts.push("HttpOnly".into()); parts.push("HttpOnly".into());
parts.push("SameSite=Strict".into()); parts.push("SameSite=Strict".into());
@@ -939,11 +940,11 @@ fn build_refresh_cookie(
parts.push(format!("Domain={}", domain)); parts.push(format!("Domain={}", domain));
} }
HeaderValue::from_str(&parts.join("; ")).expect("valid refresh cookie") HeaderValue::from_str(&parts.join("; ")).expect("valid session cookie")
} }
fn build_clear_refresh_cookie(state: &AppState) -> HeaderValue { fn build_clear_session_cookie(state: &AppState) -> HeaderValue {
let mut parts = vec![format!("{}=", REFRESH_COOKIE_NAME)]; let mut parts = vec![format!("{}=", SESSION_COOKIE_NAME)];
parts.push("Path=/".into()); parts.push("Path=/".into());
parts.push("HttpOnly".into()); parts.push("HttpOnly".into());
parts.push("SameSite=Strict".into()); parts.push("SameSite=Strict".into());
@@ -956,5 +957,5 @@ fn build_clear_refresh_cookie(state: &AppState) -> HeaderValue {
parts.push(format!("Domain={}", domain)); parts.push(format!("Domain={}", domain));
} }
HeaderValue::from_str(&parts.join("; ")).expect("valid refresh cookie") HeaderValue::from_str(&parts.join("; ")).expect("valid session cookie")
} }
+6 -6
View File
@@ -42,7 +42,7 @@ use crate::models::{
}; };
use crate::schema::{ use crate::schema::{
document_asset_objects, document_assets, document_correspondents, document_tags, document_asset_objects, document_assets, document_correspondents, document_tags,
document_versions, documents, folders, refresh_tokens::dsl as refresh_dsl, tags, document_versions, documents, folders, tags, user_sessions::dsl as session_dsl,
}; };
use crate::state::AppState; use crate::state::AppState;
use crate::utils::{ use crate::utils::{
@@ -1251,11 +1251,11 @@ pub async fn download_with_token(
let now = Utc::now().naive_utc(); let now = Utc::now().naive_utc();
let has_active_refresh: bool = select(exists( let has_active_refresh: bool = select(exists(
refresh_dsl::refresh_tokens session_dsl::user_sessions
.filter(refresh_dsl::user_id.eq(claims.user_id)) .filter(session_dsl::user_id.eq(claims.user_id))
.filter(refresh_dsl::tenant_id.eq(claims.tenant_id)) .filter(session_dsl::tenant_id.eq(claims.tenant_id))
.filter(refresh_dsl::revoked_at.is_null()) .filter(session_dsl::revoked_at.is_null())
.filter(refresh_dsl::expires_at.gt(now)), .filter(session_dsl::expires_at.gt(now)),
)) ))
.get_result(&mut conn)?; .get_result(&mut conn)?;
+4 -4
View File
@@ -155,7 +155,7 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
refresh_tokens (id) { user_sessions (id) {
id -> Uuid, id -> Uuid,
user_id -> Uuid, user_id -> Uuid,
token_hash -> Text, token_hash -> Text,
@@ -286,8 +286,8 @@ diesel::joinable!(documents -> folders (folder_id));
diesel::joinable!(documents -> tenants (tenant_id)); diesel::joinable!(documents -> tenants (tenant_id));
diesel::joinable!(folders -> tenants (tenant_id)); diesel::joinable!(folders -> tenants (tenant_id));
diesel::joinable!(jobs -> tenants (tenant_id)); diesel::joinable!(jobs -> tenants (tenant_id));
diesel::joinable!(refresh_tokens -> tenants (tenant_id)); diesel::joinable!(user_sessions -> tenants (tenant_id));
diesel::joinable!(refresh_tokens -> users (user_id)); diesel::joinable!(user_sessions -> users (user_id));
diesel::joinable!(tags -> tenants (tenant_id)); diesel::joinable!(tags -> tenants (tenant_id));
diesel::joinable!(user_memberships -> tenants (tenant_id)); diesel::joinable!(user_memberships -> tenants (tenant_id));
diesel::joinable!(user_memberships -> users (user_id)); diesel::joinable!(user_memberships -> users (user_id));
@@ -307,7 +307,7 @@ diesel::allow_tables_to_appear_in_same_query!(
folders, folders,
jobs, jobs,
magic_tokens, magic_tokens,
refresh_tokens, user_sessions,
tags, tags,
tenants, tenants,
user_memberships, user_memberships,
+5 -5
View File
@@ -148,7 +148,7 @@ pub fn clear_tenant_context(conn: &mut PgConnection) -> AppResult<()> {
"SELECT \ "SELECT \
set_config('papercrate.tenant_id', '', false), \ set_config('papercrate.tenant_id', '', false), \
set_config('papercrate.user_id', '', false), \ set_config('papercrate.user_id', '', false), \
set_config('papercrate.refresh_token_hash', '', false), \ set_config('papercrate.user_session_hash', '', false), \
set_config('papercrate.api_token_prefix', '', false)", set_config('papercrate.api_token_prefix', '', false)",
) )
.execute(conn) .execute(conn)
@@ -163,16 +163,16 @@ pub fn clear_user_guc(conn: &mut PgConnection) -> AppResult<()> {
.map_err(AppError::from) .map_err(AppError::from)
} }
pub fn apply_refresh_token_hash(conn: &mut PgConnection, hash: &str) -> AppResult<()> { pub fn apply_user_session_hash(conn: &mut PgConnection, hash: &str) -> AppResult<()> {
diesel::sql_query("SELECT set_config('papercrate.refresh_token_hash', $1, false)") diesel::sql_query("SELECT set_config('papercrate.user_session_hash', $1, false)")
.bind::<Text, _>(hash) .bind::<Text, _>(hash)
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
.map_err(AppError::from) .map_err(AppError::from)
} }
pub fn clear_refresh_token_hash(conn: &mut PgConnection) -> AppResult<()> { pub fn clear_user_session_hash(conn: &mut PgConnection) -> AppResult<()> {
diesel::sql_query("SELECT set_config('papercrate.refresh_token_hash', '', false)") diesel::sql_query("SELECT set_config('papercrate.user_session_hash', '', false)")
.execute(conn) .execute(conn)
.map(|_| ()) .map(|_| ())
.map_err(AppError::from) .map_err(AppError::from)
+11 -11
View File
@@ -9,9 +9,9 @@ use papercrate::auth::passkeys::{
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload, PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
RegistrationChallengeResponse, RegistrationChallengeResponse,
}; };
use papercrate::models::{NewRefreshToken, NewUserMembership, TenantStatus, UserPasskey}; use papercrate::models::{NewUserMembership, NewUserSession, TenantStatus, UserPasskey};
use papercrate::openapi::schemas::PasskeySummary; use papercrate::openapi::schemas::PasskeySummary;
use papercrate::schema::{refresh_tokens, tenants, user_memberships, users}; use papercrate::schema::{tenants, user_memberships, user_sessions, users};
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::RngCore; use rand::RngCore;
use serde::Deserialize; use serde::Deserialize;
@@ -598,21 +598,21 @@ async fn login_with_session(
.generate_token(user.id, tenant.id, &user.username) .generate_token(user.id, tenant.id, &user.username)
.map_err(|err| anyhow!(err))?; .map_err(|err| anyhow!(err))?;
let refresh_value = generate_refresh_token(); let session_value = generate_session_token();
let refresh_hash = hash_refresh_token(&refresh_value); let session_hash = hash_session_token(&session_value);
let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days); let refresh_expires_at = now + ChronoDuration::days(state.config.refresh_token_expiry_days);
let new_refresh = NewRefreshToken { let new_session = NewUserSession {
id: Uuid::new_v4(), id: Uuid::new_v4(),
user_id: user.id, user_id: user.id,
token_hash: refresh_hash, token_hash: session_hash,
issued_at: now.naive_utc(), issued_at: now.naive_utc(),
expires_at: refresh_expires_at.naive_utc(), expires_at: refresh_expires_at.naive_utc(),
tenant_id: tenant.id, tenant_id: tenant.id,
}; };
diesel::insert_into(refresh_tokens::table) diesel::insert_into(user_sessions::table)
.values(&new_refresh) .values(&new_session)
.execute(conn)?; .execute(conn)?;
let login = LoginResponse { let login = LoginResponse {
@@ -623,7 +623,7 @@ async fn login_with_session(
}, },
}; };
let cookie = format!("refresh_token={refresh_value}"); let cookie = format!("refresh_token={session_value}");
Ok((login, cookie)) Ok((login, cookie))
}) })
.await .await
@@ -643,13 +643,13 @@ fn extract_refresh_cookie(headers: &axum::http::HeaderMap) -> Result<String> {
Ok(cookie) Ok(cookie)
} }
fn generate_refresh_token() -> String { fn generate_session_token() -> String {
let mut bytes = [0u8; 32]; let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes); OsRng.fill_bytes(&mut bytes);
hex::encode(bytes) hex::encode(bytes)
} }
fn hash_refresh_token(value: &str) -> String { fn hash_session_token(value: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(value.as_bytes()); hasher.update(value.as_bytes());
hex::encode(hasher.finalize()) hex::encode(hasher.finalize())
+12 -12
View File
@@ -20,11 +20,11 @@ use papercrate::auth::jwt::JwtService;
use papercrate::config::AppConfig; use papercrate::config::AppConfig;
use papercrate::db::{self, PgPool}; use papercrate::db::{self, PgPool};
use papercrate::models::{ use papercrate::models::{
Job, NewRefreshToken, NewUser, NewUserMembership, NewUserPasskey, Tenant, TenantStatus, User, Job, NewUser, NewUserMembership, NewUserPasskey, NewUserSession, Tenant, TenantStatus, User,
UserMembership, UserMembership,
}; };
use papercrate::routes; use papercrate::routes;
use papercrate::schema::refresh_tokens::dsl as refresh_dsl; use papercrate::schema::user_sessions::dsl as session_dsl;
use papercrate::state::AppState; use papercrate::state::AppState;
use papercrate::storage::ObjectStorage; use papercrate::storage::ObjectStorage;
use rand::rngs::OsRng; use rand::rngs::OsRng;
@@ -359,25 +359,25 @@ impl TestApp {
.generate_token(user.id, tenant.id, &user.username) .generate_token(user.id, tenant.id, &user.username)
.map_err(|err| anyhow!(err))?; .map_err(|err| anyhow!(err))?;
let refresh_value = generate_refresh_token(); let session_value = generate_session_token();
let refresh_hash = hash_refresh_token(&refresh_value); let session_hash = hash_session_token(&session_value);
let refresh_expires_at = let refresh_expires_at =
now + ChronoDuration::days(state.config.refresh_token_expiry_days); now + ChronoDuration::days(state.config.refresh_token_expiry_days);
let new_refresh = NewRefreshToken { let new_session = NewUserSession {
id: Uuid::new_v4(), id: Uuid::new_v4(),
user_id: user.id, user_id: user.id,
token_hash: refresh_hash, token_hash: session_hash,
issued_at: now.naive_utc(), issued_at: now.naive_utc(),
expires_at: refresh_expires_at.naive_utc(), expires_at: refresh_expires_at.naive_utc(),
tenant_id: tenant.id, tenant_id: tenant.id,
}; };
diesel::insert_into(refresh_dsl::refresh_tokens) diesel::insert_into(session_dsl::user_sessions)
.values(&new_refresh) .values(&new_session)
.execute(conn)?; .execute(conn)?;
let cookie = format!("refresh_token={refresh_value}"); let cookie = format!("refresh_token={session_value}");
Ok((access_token, cookie, tenant.id)) Ok((access_token, cookie, tenant.id))
}) })
.await .await
@@ -787,7 +787,7 @@ fn truncate_all(conn: &mut PgConnection) -> Result<()> {
tenant.documents, \ tenant.documents, \
tenant.folders, \ tenant.folders, \
shared.jobs, \ shared.jobs, \
tenant.refresh_tokens, \ tenant.user_sessions, \
tenant.tags, \ tenant.tags, \
tenant.api_tokens, \ tenant.api_tokens, \
shared.webauthn_challenges, \ shared.webauthn_challenges, \
@@ -802,13 +802,13 @@ fn truncate_all(conn: &mut PgConnection) -> Result<()> {
Ok(()) Ok(())
} }
fn generate_refresh_token() -> String { fn generate_session_token() -> String {
let mut bytes = [0u8; 32]; let mut bytes = [0u8; 32];
OsRng.fill_bytes(&mut bytes); OsRng.fill_bytes(&mut bytes);
hex::encode(bytes) hex::encode(bytes)
} }
fn hash_refresh_token(value: &str) -> String { fn hash_session_token(value: &str) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(value.as_bytes()); hasher.update(value.as_bytes());
hex::encode(hasher.finalize()) hex::encode(hasher.finalize())
+1 -1
View File
@@ -75,7 +75,7 @@ objects under a single logical asset.
- `document_tags` and `document_correspondents` provide many-to-many - `document_tags` and `document_correspondents` provide many-to-many
relationships for categorisation. relationships for categorisation.
- `jobs` records background work (OCR, thumbnails, indexing) keyed by tenant. - `jobs` records background work (OCR, thumbnails, indexing) keyed by tenant.
- `webdav_tokens`, `refresh_tokens`, and `user_passkeys` live alongside but do - `api_tokens`, `user_sessions`, and `user_passkeys` live alongside but do
not alter the document schema directly. not alter the document schema directly.
## Lifecycle summary ## Lifecycle summary