use std::time::Duration; use chrono::{NaiveDateTime, Utc}; use diesel::{ deserialize::FromSql, expression::AsExpression, prelude::*, serialize::{Output, ToSql}, sql_types::Text, }; use crate::{ api::EmptyResult, db::{DbConn, DbPool, schema::sso_auth}, error::MapResult, sso::{OIDCCode, OIDCCodeChallenge, OIDCIdentifier, OIDCState, SSO_AUTH_EXPIRATION}, }; #[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)] #[diesel(sql_type = Text)] pub struct OIDCCodeResponseError { pub error: String, pub error_description: Option, } impl_FromToSqlText!(OIDCCodeResponseError); #[derive(AsExpression, Clone, Debug, Serialize, Deserialize, FromSqlRow)] #[diesel(sql_type = Text)] pub struct OIDCAuthenticatedUser { pub refresh_token: Option, pub access_token: String, pub expires_in: Option, pub identifier: OIDCIdentifier, pub email: String, pub email_verified: Option, pub user_name: Option, } impl_FromToSqlText!(OIDCAuthenticatedUser); #[derive(Identifiable, Queryable, Insertable, AsChangeset, Selectable)] #[diesel(table_name = sso_auth)] #[diesel(treat_none_as_null = true)] #[diesel(primary_key(state))] pub struct SsoAuth { pub state: OIDCState, pub client_challenge: OIDCCodeChallenge, pub nonce: String, pub redirect_uri: String, pub code_response: Option, pub code_response_error: Option, pub auth_response: Option, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub binding_hash: Option, } /// Local methods impl SsoAuth { pub fn new( state: OIDCState, client_challenge: OIDCCodeChallenge, nonce: String, redirect_uri: String, binding_hash: Option, ) -> Self { let now = Utc::now().naive_utc(); SsoAuth { state, client_challenge, nonce, redirect_uri, created_at: now, updated_at: now, code_response: None, code_response_error: None, auth_response: None, binding_hash, } } } /// Database methods impl SsoAuth { pub async fn save(&self, conn: &DbConn) -> EmptyResult { db_run! { conn: mysql { diesel::insert_into(sso_auth::table) .values(self) .on_conflict(diesel::dsl::DuplicatedKeys) .do_update() .set(self) .execute(conn) .map_res("Error saving SSO auth") } postgresql, sqlite { diesel::insert_into(sso_auth::table) .values(self) .on_conflict(sso_auth::state) .do_update() .set(self) .execute(conn) .map_res("Error saving SSO auth") } } } pub async fn find(state: &OIDCState, conn: &DbConn) -> Option { let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; conn.run(move |conn| { sso_auth::table .filter(sso_auth::state.eq(state)) .filter(sso_auth::created_at.ge(oldest)) .first::(conn) .ok() }) .await } pub async fn find_by_code(code: &OIDCCode, conn: &DbConn) -> Option { let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; db_run! { conn: { sso_auth::table .filter(sso_auth::code_response.eq(code)) .filter(sso_auth::created_at.ge(oldest)) .first::(conn) .ok() }} } pub async fn delete(self, conn: &DbConn) -> EmptyResult { conn.run(move |conn| { diesel::delete(sso_auth::table.filter(sso_auth::state.eq(self.state))) .execute(conn) .map_res("Error deleting sso_auth") }) .await } pub async fn delete_expired(pool: DbPool) -> EmptyResult { debug!("Purging expired sso_auth"); if let Ok(conn) = pool.get().await { let oldest = Utc::now().naive_utc() - *SSO_AUTH_EXPIRATION; conn.run(move |conn| { diesel::delete(sso_auth::table.filter(sso_auth::created_at.lt(oldest))) .execute(conn) .map_res("Error deleting expired SSO nonce") }) .await } else { err!("Failed to get DB connection while purging expired sso_auth") } } }