Browse Source

Implement XOAuth2 model and database integration for SMTP OAuth2 tokens

pull/6388/head
Henning 2 weeks ago
parent
commit
405b50d044
  1. 1
      migrations/mysql/2025-12-26-143000_create_xoauth2/down.sql
  2. 4
      migrations/mysql/2025-12-26-143000_create_xoauth2/up.sql
  3. 1
      migrations/postgresql/2025-12-26-143000_create_xoauth2/down.sql
  4. 4
      migrations/postgresql/2025-12-26-143000_create_xoauth2/up.sql
  5. 1
      migrations/sqlite/2025-12-26-143000_create_xoauth2/down.sql
  6. 4
      migrations/sqlite/2025-12-26-143000_create_xoauth2/up.sql
  7. 14
      src/api/admin.rs
  8. 3
      src/config.rs
  9. 8
      src/db/mod.rs
  10. 2
      src/db/models/mod.rs
  11. 48
      src/db/models/xoauth2.rs
  12. 7
      src/db/schema.rs
  13. 22
      src/mail.rs
  14. 1
      src/main.rs

1
migrations/mysql/2025-12-26-143000_create_xoauth2/down.sql

@ -0,0 +1 @@
DROP TABLE xoauth2;

4
migrations/mysql/2025-12-26-143000_create_xoauth2/up.sql

@ -0,0 +1,4 @@
CREATE TABLE xoauth2 (
id VARCHAR(255) NOT NULL PRIMARY KEY,
refresh_token TEXT NOT NULL
);

1
migrations/postgresql/2025-12-26-143000_create_xoauth2/down.sql

@ -0,0 +1 @@
DROP TABLE xoauth2;

4
migrations/postgresql/2025-12-26-143000_create_xoauth2/up.sql

@ -0,0 +1,4 @@
CREATE TABLE xoauth2 (
id TEXT NOT NULL PRIMARY KEY,
refresh_token TEXT NOT NULL
);

1
migrations/sqlite/2025-12-26-143000_create_xoauth2/down.sql

@ -0,0 +1 @@
DROP TABLE xoauth2;

4
migrations/sqlite/2025-12-26-143000_create_xoauth2/up.sql

@ -0,0 +1,4 @@
CREATE TABLE xoauth2 (
id TEXT NOT NULL PRIMARY KEY,
refresh_token TEXT NOT NULL
);

14
src/api/admin.rs

@ -28,7 +28,7 @@ use crate::{
backup_sqlite, get_sql_server_version,
models::{
Attachment, Cipher, Collection, Device, Event, EventType, Group, Invitation, Membership, MembershipId,
MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId,
MembershipType, OrgPolicy, Organization, OrganizationId, SsoUser, TwoFactor, User, UserId, XOAuth2,
},
DbConn, DbConnType, ACTIVE_DB_TYPE,
},
@ -413,7 +413,7 @@ struct OAuth2CallbackParams {
}
#[get("/oauth2/callback?<params..>")]
async fn oauth2_callback(params: OAuth2CallbackParams) -> Result<Html<String>, Error> {
async fn oauth2_callback(params: OAuth2CallbackParams, conn: DbConn) -> Result<Html<String>, Error> {
// Check for errors from OAuth2 provider
if let Some(error) = params.error {
let description = params.error_description.unwrap_or_else(|| "Unknown error".to_string());
@ -472,14 +472,8 @@ async fn oauth2_callback(params: OAuth2CallbackParams) -> Result<Html<String>, E
let refresh_token =
token_response.get("refresh_token").and_then(|v| v.as_str()).ok_or("No refresh_token in response")?;
// Save refresh_token to configuration
let config_builder: ConfigBuilder = match serde_json::from_value(json!({
"smtp_oauth2_refresh_token": refresh_token
})) {
Ok(builder) => builder,
Err(e) => err!(format!("ConfigBuilder serialization error: {e}")),
};
CONFIG.update_config_partial(config_builder).await?;
// Save refresh_token to database
XOAuth2::new("smtp".to_string(), refresh_token.to_string()).save(&conn).await?;
// Return success page via template
let json = json!({

3
src/config.rs

@ -429,6 +429,9 @@ macro_rules! make_config {
"sso_authority",
"sso_callback_path",
"sso_client_id",
"smtp_oauth2_client_id",
"smtp_oauth2_auth_url",
"smtp_oauth2_token_url",
];
let cfg = {

8
src/db/mod.rs

@ -121,6 +121,14 @@ pub enum DbConnType {
}
pub static ACTIVE_DB_TYPE: OnceLock<DbConnType> = OnceLock::new();
pub static DB_POOL: OnceLock<DbPool> = OnceLock::new();
pub async fn get_conn() -> Result<DbConn, Error> {
match DB_POOL.get() {
Some(p) => p.get().await,
None => err!("Database pool not initialized"),
}
}
pub struct DbConn {
conn: Arc<Mutex<Option<PooledConnection<DbConnManager>>>>,

2
src/db/models/mod.rs

@ -16,6 +16,7 @@ mod two_factor;
mod two_factor_duo_context;
mod two_factor_incomplete;
mod user;
mod xoauth2;
pub use self::attachment::{Attachment, AttachmentId};
pub use self::auth_request::{AuthRequest, AuthRequestId};
@ -41,3 +42,4 @@ pub use self::two_factor::{TwoFactor, TwoFactorType};
pub use self::two_factor_duo_context::TwoFactorDuoContext;
pub use self::two_factor_incomplete::TwoFactorIncomplete;
pub use self::user::{Invitation, SsoUser, User, UserId, UserKdfType, UserStampException};
pub use self::xoauth2::XOAuth2;

48
src/db/models/xoauth2.rs

@ -0,0 +1,48 @@
use crate::db::DbConn;
use crate::api::EmptyResult;
use crate::error::MapResult;
use crate::db::schema::xoauth2;
use diesel::prelude::*;
#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = xoauth2)]
#[diesel(primary_key(id))]
pub struct XOAuth2 {
pub id: String,
pub refresh_token: String,
}
impl XOAuth2 {
pub fn new(id: String, refresh_token: String) -> Self {
Self { id, refresh_token }
}
pub async fn save(&self, conn: &DbConn) -> EmptyResult {
db_run! { conn:
sqlite, mysql {
diesel::replace_into(xoauth2::table)
.values(self)
.execute(conn)
.map_res("Error saving xoauth2")
}
postgresql {
diesel::insert_into(xoauth2::table)
.values(self)
.on_conflict(xoauth2::id)
.do_update()
.set(self)
.execute(conn)
.map_res("Error saving xoauth2")
}
}
}
pub async fn find_by_id(id: String, conn: &DbConn) -> Option<Self> {
db_run! { conn: {
xoauth2::table
.filter(xoauth2::id.eq(id))
.first::<Self>(conn)
.ok()
}}
}
}

7
src/db/schema.rs

@ -341,6 +341,13 @@ table! {
}
}
table! {
xoauth2 (id) {
id -> Text,
refresh_token -> Text,
}
}
joinable!(attachments -> ciphers (cipher_uuid));
joinable!(ciphers -> organizations (organization_uuid));
joinable!(ciphers -> users (user_uuid));

22
src/mail.rs

@ -19,7 +19,9 @@ use crate::{
encode_jwt, generate_delete_claims, generate_emergency_access_invite_claims, generate_invite_claims,
generate_verify_email_claims,
},
db::models::{Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId},
db::models::{
Device, DeviceType, EmergencyAccessId, MembershipId, OrganizationId, User, UserId, XOAuth2,
},
error::Error,
CONFIG,
};
@ -44,9 +46,15 @@ struct TokenRefreshResponse {
}
pub async fn refresh_oauth2_token() -> Result<OAuth2Token, Error> {
let conn = crate::db::get_conn().await?;
let refresh_token = if let Some(x) = XOAuth2::find_by_id("smtp".to_string(), &conn).await {
x.refresh_token
} else {
CONFIG.smtp_oauth2_refresh_token().ok_or("OAuth2 Refresh Token not configured")?
};
let client_id = CONFIG.smtp_oauth2_client_id().ok_or("OAuth2 Client ID not configured")?;
let client_secret = CONFIG.smtp_oauth2_client_secret().ok_or("OAuth2 Client Secret not configured")?;
let refresh_token = CONFIG.smtp_oauth2_refresh_token().ok_or("OAuth2 Refresh Token not configured")?;
let token_url = CONFIG.smtp_oauth2_token_url().ok_or("OAuth2 Token URL not configured")?;
let form_params = [
@ -76,12 +84,18 @@ pub async fn refresh_oauth2_token() -> Result<OAuth2Token, Error> {
.expires_in
.map(|expires_in| SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + expires_in);
Ok(OAuth2Token {
let new_token = OAuth2Token {
access_token: token_response.access_token,
refresh_token: token_response.refresh_token.or(Some(refresh_token)),
expires_at,
token_type: token_response.token_type,
})
};
if let Some(ref new_refresh) = new_token.refresh_token {
XOAuth2::new("smtp".to_string(), new_refresh.clone()).save(&conn).await?;
}
Ok(new_token)
}
async fn get_valid_oauth2_token() -> Result<OAuth2Token, Error> {

1
src/main.rs

@ -85,6 +85,7 @@ async fn main() -> Result<(), Error> {
create_dir(&CONFIG.tmp_folder(), "tmp folder");
let pool = create_db_pool().await;
db::DB_POOL.set(pool.clone()).ok();
schedule_jobs(pool.clone());
db::models::TwoFactor::migrate_u2f_to_webauthn(&pool.get().await.unwrap()).await.unwrap();
db::models::TwoFactor::migrate_credential_to_passkey(&pool.get().await.unwrap()).await.unwrap();

Loading…
Cancel
Save