mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-04-29 07:13:40 -06:00
Implement XOAuth2 model and database integration for SMTP OAuth2 tokens
This commit is contained in:
parent
418e4454c9
commit
405b50d044
@ -0,0 +1 @@
|
||||
DROP TABLE xoauth2;
|
||||
4
migrations/mysql/2025-12-26-143000_create_xoauth2/up.sql
Normal file
4
migrations/mysql/2025-12-26-143000_create_xoauth2/up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE xoauth2 (
|
||||
id VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
refresh_token TEXT NOT NULL
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE xoauth2;
|
||||
@ -0,0 +1,4 @@
|
||||
CREATE TABLE xoauth2 (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
refresh_token TEXT NOT NULL
|
||||
);
|
||||
@ -0,0 +1 @@
|
||||
DROP TABLE xoauth2;
|
||||
@ -0,0 +1,4 @@
|
||||
CREATE TABLE xoauth2 (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
refresh_token TEXT NOT NULL
|
||||
);
|
||||
@ -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!({
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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>>>>,
|
||||
|
||||
@ -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
Normal file
48
src/db/models/xoauth2.rs
Normal file
@ -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()
|
||||
}}
|
||||
}
|
||||
}
|
||||
@ -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
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> {
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user