user_sessions
This commit is contained in:
@@ -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();
|
||||||
@@ -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
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user