use crate::dal::unified::DAL;
use crate::error::ValidationError;
#[cfg(feature = "postgres")]
use chrono::{DateTime, Utc};
#[cfg(feature = "postgres")]
use diesel::prelude::*;
#[cfg(feature = "postgres")]
use uuid::Uuid;
pub fn encrypt_token(plaintext: &[u8], enc_key: &[u8]) -> Result<Vec<u8>, ValidationError> {
crate::crypto::encrypt_private_key(plaintext, enc_key)
.map_err(|e| ValidationError::ConnectionPool(format!("refresh-token encryption: {e}")))
}
pub fn decrypt_token(ciphertext: &[u8], enc_key: &[u8]) -> Result<Vec<u8>, ValidationError> {
crate::crypto::decrypt_private_key(ciphertext, enc_key)
.map_err(|e| ValidationError::ConnectionPool(format!("refresh-token decryption: {e}")))
}
#[derive(Debug, Clone)]
pub struct RefreshSession {
pub provider: String,
pub refresh_token: Vec<u8>,
pub expires_at: Option<DateTime<Utc>>,
}
#[cfg(feature = "postgres")]
#[derive(Queryable)]
#[diesel(table_name = crate::database::schema::postgres::oidc_sessions)]
struct OidcSessionRow {
#[allow(dead_code)]
pub id: Uuid,
#[allow(dead_code)]
pub key_id: Uuid,
pub provider: String,
pub refresh_enc: Vec<u8>,
#[allow(dead_code)]
pub created_at: chrono::NaiveDateTime,
pub expires_at: Option<chrono::NaiveDateTime>,
}
#[cfg(feature = "postgres")]
#[derive(Insertable)]
#[diesel(table_name = crate::database::schema::postgres::oidc_sessions)]
struct NewOidcSession {
pub id: Uuid,
pub key_id: Uuid,
pub provider: String,
pub refresh_enc: Vec<u8>,
pub expires_at: Option<chrono::NaiveDateTime>,
}
pub struct OidcSessionDAL<'a> {
dal: &'a DAL,
}
impl<'a> OidcSessionDAL<'a> {
pub fn new(dal: &'a DAL) -> Self {
Self { dal }
}
#[cfg(feature = "postgres")]
pub async fn create(
&self,
key_id: Uuid,
provider: &str,
refresh_token: &[u8],
enc_key: &[u8],
expires_at: Option<DateTime<Utc>>,
) -> Result<(), ValidationError> {
let refresh_enc = encrypt_token(refresh_token, enc_key)?;
let row = NewOidcSession {
id: Uuid::new_v4(),
key_id,
provider: provider.to_string(),
refresh_enc,
expires_at: expires_at.map(|t| t.naive_utc()),
};
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
conn.interact(move |conn| {
diesel::insert_into(crate::database::schema::postgres::oidc_sessions::table)
.values(&row)
.execute(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(())
}
#[cfg(feature = "postgres")]
pub async fn get(
&self,
key_id: Uuid,
enc_key: &[u8],
) -> Result<Option<RefreshSession>, ValidationError> {
use crate::database::schema::postgres::oidc_sessions;
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let row: Option<OidcSessionRow> = conn
.interact(move |conn| {
oidc_sessions::table
.filter(oidc_sessions::key_id.eq(key_id))
.first(conn)
.optional()
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
match row {
None => Ok(None),
Some(r) => {
let refresh_token = decrypt_token(&r.refresh_enc, enc_key)?;
Ok(Some(RefreshSession {
provider: r.provider,
refresh_token,
expires_at: r.expires_at.map(|t| t.and_utc()),
}))
}
}
}
#[cfg(feature = "postgres")]
pub async fn delete(&self, key_id: Uuid) -> Result<bool, ValidationError> {
use crate::database::schema::postgres::oidc_sessions;
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let n: usize = conn
.interact(move |conn| {
diesel::delete(oidc_sessions::table.filter(oidc_sessions::key_id.eq(key_id)))
.execute(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(n > 0)
}
#[cfg(feature = "postgres")]
pub async fn sweep_expired(&self) -> Result<usize, ValidationError> {
use crate::database::schema::postgres::oidc_sessions;
let now = Utc::now().naive_utc();
let conn = self
.dal
.database
.get_postgres_connection()
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))?;
let n: usize = conn
.interact(move |conn| {
diesel::delete(oidc_sessions::table.filter(oidc_sessions::expires_at.lt(now)))
.execute(conn)
})
.await
.map_err(|e| ValidationError::ConnectionPool(e.to_string()))??;
Ok(n)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_encrypt_decrypt_roundtrip() {
let key = [0x11u8; 32];
let token = b"refresh-token-abc.def.ghi";
let enc = encrypt_token(token, &key).expect("encrypt");
assert_ne!(&enc[..], &token[..], "must not store plaintext");
let dec = decrypt_token(&enc, &key).expect("decrypt");
assert_eq!(&dec, token);
}
#[test]
fn wrong_key_fails_decryption() {
let token = b"secret";
let enc = encrypt_token(token, &[0x11u8; 32]).unwrap();
assert!(decrypt_token(&enc, &[0x22u8; 32]).is_err());
}
#[test]
fn bad_key_length_errors() {
assert!(encrypt_token(b"x", &[0u8; 16]).is_err());
}
}