mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-17 15:31:38 +02:00
Admin tokens are cryptographically distinct from user tokens: a dedicated ADMIN_JWT_SECRET, a distinct audience (smirk-admin) plus an explicit scope, and the centralized strict Validation (HS256 pinned, zero leeway, aud required) — Validation::default() is never used on this path. Access TTL 15 min (carries a jti recorded in the session row, so a soft-revoked admin's still-valid token is rejected at the next call), refresh TTL 8 h (binds a session id). - core::admin_session: AdminSessionManager (mint/verify access+refresh) with the four required regressions + scope check: user token fails admin decode; wrong/absent aud fails; HS512 (alg confusion) fails; expired fails at zero leeway; non-admin scope fails. - core::session::strict_validation made pub(crate) and shared (no drift). - db::admin_sessions: create, find-active-by-jti (guard), find-active-by-id + rotate-jti (refresh), revoke (logout), revoke-all-for-key (key cascade), and a dead-session sweep. Lookups return only live (unrevoked, unexpired) rows. - NewAdminSession model; 2 L1 tests (lookup/rotate/logout, key->session cascade).
119 lines
3.2 KiB
Rust
119 lines
3.2 KiB
Rust
//! L1 integration: the admin_sessions lifecycle against real Postgres —
|
|
//! create, lookup-by-jti, jti rotation on refresh, logout, and key-cascade
|
|
//! revocation.
|
|
|
|
mod common;
|
|
|
|
use chrono::{Duration, Utc};
|
|
use smirk_backend_core::models::db::{NewAdminKey, NewAdminSession};
|
|
use uuid::Uuid;
|
|
|
|
const SECRET: &str = "admin-key-integrity-secret-0123456789ab";
|
|
|
|
async fn make_key(db: &smirk_backend_core::infra::db::Database) -> (Uuid, String) {
|
|
let pk = format!("{:0>64}", Uuid::new_v4().simple());
|
|
let key = db
|
|
.create_admin_key(
|
|
NewAdminKey {
|
|
pubkey: pk.clone(),
|
|
label: None,
|
|
scope: "admin".into(),
|
|
created_by_kind: "cli".into(),
|
|
activation_deadline: None,
|
|
},
|
|
SECRET,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
(key.id, pk)
|
|
}
|
|
|
|
fn new_session(id: Uuid, key_id: Uuid, pk: &str, jti: &str) -> NewAdminSession {
|
|
NewAdminSession {
|
|
id,
|
|
admin_key_id: key_id,
|
|
pubkey: pk.to_string(),
|
|
refresh_token_hash: format!("hash-{jti}"),
|
|
access_jti: jti.to_string(),
|
|
device_info: None,
|
|
ip_address: None,
|
|
expires_at: Utc::now() + Duration::hours(8),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn create_lookup_rotate_and_logout() {
|
|
let app = require_app!();
|
|
let db = &app.state.db;
|
|
let (key_id, pk) = make_key(db).await;
|
|
let sid = Uuid::new_v4();
|
|
|
|
db.create_admin_session(new_session(sid, key_id, &pk, "jti-1"))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Guard lookup by the access jti.
|
|
let found = db
|
|
.find_active_admin_session_by_jti("jti-1")
|
|
.await
|
|
.unwrap()
|
|
.expect("live session by jti");
|
|
assert_eq!(found.id, sid);
|
|
|
|
// Refresh rotates the jti: the old one stops matching, the new one matches.
|
|
db.rotate_admin_session_jti(sid, "jti-2")
|
|
.await
|
|
.unwrap()
|
|
.expect("rotated");
|
|
assert!(db
|
|
.find_active_admin_session_by_jti("jti-1")
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
assert!(db
|
|
.find_active_admin_session_by_jti("jti-2")
|
|
.await
|
|
.unwrap()
|
|
.is_some());
|
|
|
|
// Logout revokes the session.
|
|
assert!(db.revoke_admin_session(sid).await.unwrap());
|
|
assert!(db
|
|
.find_active_admin_session_by_jti("jti-2")
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
// Re-revoke is a no-op.
|
|
assert!(!db.revoke_admin_session(sid).await.unwrap());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn key_revocation_cascades_to_sessions() {
|
|
let app = require_app!();
|
|
let db = &app.state.db;
|
|
let (key_id, pk) = make_key(db).await;
|
|
|
|
let s1 = Uuid::new_v4();
|
|
let s2 = Uuid::new_v4();
|
|
db.create_admin_session(new_session(s1, key_id, &pk, "c-1"))
|
|
.await
|
|
.unwrap();
|
|
db.create_admin_session(new_session(s2, key_id, &pk, "c-2"))
|
|
.await
|
|
.unwrap();
|
|
|
|
// Cascade revokes both live sessions for the key.
|
|
let n = db.revoke_admin_sessions_for_key(key_id).await.unwrap();
|
|
assert_eq!(n, 2);
|
|
assert!(db
|
|
.find_active_admin_session_by_id(s1)
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
assert!(db
|
|
.find_active_admin_session_by_id(s2)
|
|
.await
|
|
.unwrap()
|
|
.is_none());
|
|
}
|