mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-12-16 04:09:03 +00:00
Merge 3b8af41c5f into 57bdab1550
This commit is contained in:
commit
3893fcd26a
144
src/api/admin.rs
144
src/api/admin.rs
@ -1,4 +1,9 @@
|
||||
use data_encoding::BASE64URL_NOPAD;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::RwLock;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env, sync::LazyLock};
|
||||
use url::Url;
|
||||
|
||||
use reqwest::Method;
|
||||
use rocket::{
|
||||
@ -63,6 +68,9 @@ pub fn routes() -> Vec<Route> {
|
||||
delete_config,
|
||||
backup_db,
|
||||
test_smtp,
|
||||
refresh_oauth2_token_endpoint,
|
||||
oauth2_authorize,
|
||||
oauth2_callback,
|
||||
users_overview,
|
||||
organizations_overview,
|
||||
delete_organization,
|
||||
@ -97,6 +105,9 @@ static CAN_BACKUP: LazyLock<bool> =
|
||||
#[cfg(not(sqlite))]
|
||||
static CAN_BACKUP: LazyLock<bool> = LazyLock::new(|| false);
|
||||
|
||||
// OAuth2 state storage for CSRF protection (state -> expiration timestamp)
|
||||
static OAUTH2_STATES: LazyLock<RwLock<HashMap<String, u64>>> = LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
#[get("/")]
|
||||
fn admin_disabled() -> &'static str {
|
||||
"The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
|
||||
@ -341,6 +352,139 @@ async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/test/oauth2")]
|
||||
async fn refresh_oauth2_token_endpoint(_token: AdminToken) -> EmptyResult {
|
||||
if CONFIG.smtp_oauth2_client_id().is_none() {
|
||||
err!("OAuth2 is not configured")
|
||||
}
|
||||
|
||||
mail::refresh_oauth2_token().await.map(|_| ())
|
||||
}
|
||||
|
||||
#[get("/oauth2/authorize")]
|
||||
fn oauth2_authorize(_token: AdminToken) -> Result<Redirect, Error> {
|
||||
// Check if OAuth2 is configured
|
||||
let client_id = CONFIG.smtp_oauth2_client_id().ok_or("OAuth2 Client ID not configured")?;
|
||||
let auth_url = CONFIG.smtp_oauth2_auth_url().ok_or("OAuth2 Authorization URL not configured")?;
|
||||
let scopes = CONFIG.smtp_oauth2_scopes();
|
||||
|
||||
// Generate a random state token for CSRF protection
|
||||
let state = crate::crypto::encode_random_bytes::<32>(&BASE64URL_NOPAD);
|
||||
|
||||
// Store state with expiration (10 minutes from now)
|
||||
let expiration = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 600;
|
||||
|
||||
OAUTH2_STATES.write().unwrap().insert(state.clone(), expiration);
|
||||
|
||||
// Clean up expired states
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
OAUTH2_STATES.write().unwrap().retain(|_, &mut exp| exp > now);
|
||||
|
||||
// Construct redirect URI
|
||||
let redirect_uri = format!("{}/admin/oauth2/callback", CONFIG.domain());
|
||||
|
||||
// Build authorization URL using url crate to ensure proper encoding
|
||||
let mut url = Url::parse(&auth_url).map_err(|e| Error::new("Invalid OAuth2 Authorization URL", e.to_string()))?;
|
||||
{
|
||||
let mut qp = url.query_pairs_mut();
|
||||
qp.append_pair("client_id", &client_id);
|
||||
qp.append_pair("redirect_uri", &redirect_uri);
|
||||
qp.append_pair("response_type", "code");
|
||||
qp.append_pair("scope", &scopes);
|
||||
qp.append_pair("state", &state);
|
||||
qp.append_pair("access_type", "offline");
|
||||
qp.append_pair("prompt", "consent");
|
||||
}
|
||||
|
||||
let auth_url = url.to_string();
|
||||
|
||||
Ok(Redirect::to(auth_url))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct OAuth2CallbackParams {
|
||||
code: Option<String>,
|
||||
state: Option<String>,
|
||||
error: Option<String>,
|
||||
error_description: Option<String>,
|
||||
}
|
||||
|
||||
#[get("/oauth2/callback?<params..>")]
|
||||
async fn oauth2_callback(params: OAuth2CallbackParams) -> 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());
|
||||
return Err(Error::new("OAuth2 Authorization Failed", format!("{}: {}", error, description)));
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
let code = params.code.ok_or("Authorization code not provided")?;
|
||||
let state = params.state.ok_or("State parameter not provided")?;
|
||||
|
||||
// Validate state token
|
||||
let valid_state = {
|
||||
let states = OAUTH2_STATES.read().unwrap();
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
states.get(&state).is_some_and(|&exp| exp > now)
|
||||
};
|
||||
|
||||
if !valid_state {
|
||||
return Err(Error::new("OAuth2 State Validation Failed", "Invalid or expired state token"));
|
||||
}
|
||||
|
||||
// Remove used state
|
||||
OAUTH2_STATES.write().unwrap().remove(&state);
|
||||
|
||||
// Exchange authorization code for tokens
|
||||
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 token_url = CONFIG.smtp_oauth2_token_url().ok_or("OAuth2 Token URL not configured")?;
|
||||
let redirect_uri = format!("{}/admin/oauth2/callback", CONFIG.domain());
|
||||
|
||||
let form_params = [
|
||||
("grant_type", "authorization_code"),
|
||||
("code", &code),
|
||||
("redirect_uri", &redirect_uri),
|
||||
("client_id", &client_id),
|
||||
("client_secret", &client_secret),
|
||||
];
|
||||
|
||||
let response = make_http_request(Method::POST, &token_url)?
|
||||
.form(&form_params)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| Error::new("OAuth2 Token Exchange Error", e.to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_else(|_| String::from("Unable to read response body"));
|
||||
return Err(Error::new("OAuth2 Token Exchange Failed", format!("HTTP {}: {}", status, body)));
|
||||
}
|
||||
|
||||
let token_response: Value =
|
||||
response.json().await.map_err(|e| Error::new("OAuth2 Token Parse Error", e.to_string()))?;
|
||||
|
||||
// Extract refresh_token from response
|
||||
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 = serde_json::from_value(json!({
|
||||
"smtp_oauth2_refresh_token": refresh_token
|
||||
}))
|
||||
.map_err(|e| Error::new("ConfigBuilder serialization error", e.to_string()))?;
|
||||
CONFIG.update_config_partial(config_builder).await?;
|
||||
|
||||
// Return success page via template
|
||||
let json = json!({
|
||||
"page_content": "admin/oauth2_success",
|
||||
"admin_url": admin_url(),
|
||||
"urlpath": CONFIG.domain_path(),
|
||||
});
|
||||
let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
|
||||
Ok(Html(text))
|
||||
}
|
||||
|
||||
#[get("/logout")]
|
||||
fn logout(cookies: &CookieJar<'_>) -> Redirect {
|
||||
cookies.remove(Cookie::build(COOKIE_NAME).path(admin_path()));
|
||||
|
||||
@ -887,6 +887,18 @@ make_config! {
|
||||
smtp_password: Pass, true, option;
|
||||
/// SMTP Auth mechanism |> Defaults for SSL is "Plain" and "Login" and nothing for Non-SSL connections. Possible values: ["Plain", "Login", "Xoauth2"]. Multiple options need to be separated by a comma ','.
|
||||
smtp_auth_mechanism: String, true, option;
|
||||
/// SMTP OAuth2 Client ID |> OAuth2 Client ID for XOAUTH2 authentication
|
||||
smtp_oauth2_client_id: String, true, option;
|
||||
/// SMTP OAuth2 Client Secret |> OAuth2 Client Secret for XOAUTH2 authentication
|
||||
smtp_oauth2_client_secret: Pass, true, option;
|
||||
/// SMTP OAuth2 Authorization URL |> OAuth2 Authorization Server URL
|
||||
smtp_oauth2_auth_url: String, true, option;
|
||||
/// SMTP OAuth2 Token URL |> OAuth2 Token Server URL for refreshing access tokens
|
||||
smtp_oauth2_token_url: String, true, option;
|
||||
/// SMTP OAuth2 Refresh Token |> OAuth2 Refresh Token for obtaining new access tokens
|
||||
smtp_oauth2_refresh_token: Pass, true, option;
|
||||
/// SMTP OAuth2 Scopes |> Comma-separated list of OAuth2 scopes
|
||||
smtp_oauth2_scopes: String, true, def, "https://mail.google.com/".to_string();
|
||||
/// SMTP connection timeout |> Number of seconds when to stop trying to connect to the SMTP server
|
||||
smtp_timeout: u64, true, def, 15;
|
||||
/// Server name sent during HELO |> By default this value should be the machine's hostname, but might need to be changed in case it trips some anti-spam filters
|
||||
@ -1151,8 +1163,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
err!("Both `SMTP_HOST` and `SMTP_FROM` need to be set for email support without `USE_SENDMAIL`")
|
||||
}
|
||||
|
||||
// Require both username and password for traditional auth, unless OAuth2 is configured
|
||||
if cfg.smtp_username.is_some() != cfg.smtp_password.is_some() {
|
||||
err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`")
|
||||
// Allow username without password if OAuth2 is configured
|
||||
if cfg.smtp_oauth2_client_id.is_none() {
|
||||
err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication without `USE_SENDMAIL`, unless using OAuth2")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1244,6 +1260,44 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||
err!("`AUTH_REQUEST_PURGE_SCHEDULE` is not a valid cron expression")
|
||||
}
|
||||
|
||||
// OAuth2 validation - triggered when SMTP Auth mechanism includes xoauth2
|
||||
let uses_xoauth2 = cfg.smtp_auth_mechanism.as_ref().map(|m| m.to_lowercase().contains("xoauth2")).unwrap_or(false);
|
||||
|
||||
if uses_xoauth2 {
|
||||
if cfg.smtp_oauth2_client_id.is_none() {
|
||||
err!("`SMTP_OAUTH2_CLIENT_ID` must be set when SMTP_AUTH_MECHANISM includes xoauth2");
|
||||
}
|
||||
if cfg.smtp_oauth2_client_secret.is_none() {
|
||||
err!("`SMTP_OAUTH2_CLIENT_SECRET` must be set when SMTP_AUTH_MECHANISM includes xoauth2");
|
||||
}
|
||||
if cfg.smtp_oauth2_auth_url.is_none() {
|
||||
err!("`SMTP_OAUTH2_AUTH_URL` must be set when SMTP_AUTH_MECHANISM includes xoauth2");
|
||||
}
|
||||
if cfg.smtp_oauth2_token_url.is_none() {
|
||||
err!("`SMTP_OAUTH2_TOKEN_URL` must be set when SMTP_AUTH_MECHANISM includes xoauth2");
|
||||
}
|
||||
if cfg.smtp_oauth2_scopes.is_empty() {
|
||||
err!("`SMTP_OAUTH2_SCOPES` must be set when SMTP_AUTH_MECHANISM includes xoauth2");
|
||||
}
|
||||
if cfg.smtp_username.is_none() {
|
||||
err!("`SMTP_USERNAME` must be set for OAuth2 authentication");
|
||||
}
|
||||
|
||||
// Validate that auth URL is a valid URL
|
||||
if let Some(ref auth_url) = cfg.smtp_oauth2_auth_url {
|
||||
if !auth_url.starts_with("http://") && !auth_url.starts_with("https://") {
|
||||
err!("`SMTP_OAUTH2_AUTH_URL` must be a valid URL starting with http:// or https://");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that token URL is a valid URL
|
||||
if let Some(ref token_url) = cfg.smtp_oauth2_token_url {
|
||||
if !token_url.starts_with("http://") && !token_url.starts_with("https://") {
|
||||
err!("`SMTP_OAUTH2_TOKEN_URL` must be a valid URL starting with http:// or https://");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !cfg.disable_admin_token {
|
||||
match cfg.admin_token.as_ref() {
|
||||
Some(t) if t.starts_with("$argon2") => {
|
||||
@ -1533,7 +1587,7 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
pub async fn update_config_partial(&self, other: ConfigBuilder) -> Result<(), Error> {
|
||||
let builder = {
|
||||
let usr = &self.inner.read().unwrap()._usr;
|
||||
let mut _overrides = Vec::new();
|
||||
@ -1793,6 +1847,7 @@ where
|
||||
reg!("admin/users");
|
||||
reg!("admin/organizations");
|
||||
reg!("admin/diagnostics");
|
||||
reg!("admin/oauth2_success");
|
||||
|
||||
reg!("404");
|
||||
|
||||
|
||||
@ -126,6 +126,12 @@ impl std::fmt::Debug for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(err: &str) -> Self {
|
||||
Error::new(err, "")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new<M: Into<String>, N: Into<String>>(usr_msg: M, log_msg: N) -> Self {
|
||||
(usr_msg, log_msg.into()).into()
|
||||
|
||||
127
src/mail.rs
127
src/mail.rs
@ -1,5 +1,8 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{env::consts::EXE_SUFFIX, str::FromStr};
|
||||
|
||||
use lettre::{
|
||||
@ -21,6 +24,87 @@ use crate::{
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
use crate::http_client::make_http_request;
|
||||
|
||||
// OAuth2 Token structures
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OAuth2Token {
|
||||
access_token: String,
|
||||
refresh_token: Option<String>,
|
||||
expires_at: Option<u64>,
|
||||
token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TokenRefreshResponse {
|
||||
access_token: String,
|
||||
refresh_token: Option<String>,
|
||||
expires_in: Option<u64>,
|
||||
token_type: String,
|
||||
}
|
||||
|
||||
pub async fn refresh_oauth2_token() -> Result<OAuth2Token, Error> {
|
||||
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 = [
|
||||
("grant_type", "refresh_token"),
|
||||
("refresh_token", &refresh_token),
|
||||
("client_id", &client_id),
|
||||
("client_secret", &client_secret),
|
||||
];
|
||||
|
||||
let response = make_http_request(reqwest::Method::POST, &token_url)?
|
||||
.form(&form_params)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| Error::new("OAuth2 Token Refresh Error", e.to_string()))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_else(|_| String::from("Unable to read response body"));
|
||||
return Err(Error::new("OAuth2 Token Refresh Failed", format!("HTTP {status}: {body}")));
|
||||
}
|
||||
|
||||
let token_response: TokenRefreshResponse =
|
||||
response.json().await.map_err(|e| Error::new("OAuth2 Token Parse Error", e.to_string()))?;
|
||||
|
||||
let expires_at = token_response
|
||||
.expires_in
|
||||
.map(|expires_in| SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + expires_in);
|
||||
|
||||
Ok(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,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_valid_oauth2_token() -> Result<OAuth2Token, Error> {
|
||||
static TOKEN_CACHE: LazyLock<RwLock<Option<OAuth2Token>>> = LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
let cached_token = TOKEN_CACHE.read().unwrap().clone();
|
||||
|
||||
if let Some(token) = cached_token {
|
||||
// Check if token is still valid (with 5 min buffer)
|
||||
if let Some(expires_at) = token.expires_at {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
|
||||
if now + 300 < expires_at {
|
||||
return Ok(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
let new_token = refresh_oauth2_token().await?;
|
||||
*TOKEN_CACHE.write().unwrap() = Some(new_token.clone());
|
||||
|
||||
Ok(new_token)
|
||||
}
|
||||
|
||||
fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> {
|
||||
if let Some(command) = CONFIG.sendmail_command() {
|
||||
AsyncSendmailTransport::new_with_command(command)
|
||||
@ -29,7 +113,7 @@ fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> {
|
||||
}
|
||||
}
|
||||
|
||||
fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
async fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
use std::time::Duration;
|
||||
let host = CONFIG.smtp_host().unwrap();
|
||||
|
||||
@ -57,8 +141,43 @@ fn smtp_transport() -> AsyncSmtpTransport<Tokio1Executor> {
|
||||
smtp_client
|
||||
};
|
||||
|
||||
let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password()) {
|
||||
(Some(user), Some(pass)) => smtp_client.credentials(Credentials::new(user, pass)),
|
||||
// Handle authentication - OAuth2 or traditional
|
||||
let smtp_client = match (CONFIG.smtp_username(), CONFIG.smtp_password(), CONFIG.smtp_oauth2_client_id()) {
|
||||
(Some(user), Some(pass), None) => {
|
||||
// Traditional authentication with username and password
|
||||
smtp_client.credentials(Credentials::new(user, pass))
|
||||
}
|
||||
(Some(user), None, Some(_)) => {
|
||||
// OAuth2 authentication
|
||||
match get_valid_oauth2_token().await {
|
||||
Ok(token) => {
|
||||
// Pass the access token directly as password - lettre's Xoauth2 mechanism
|
||||
// will format it correctly as: user={user}\x01auth=Bearer {token}\x01\x01
|
||||
smtp_client.credentials(Credentials::new(user, token.access_token))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error fetching OAuth2 token: {}", e);
|
||||
warn!("Failed to get OAuth2 token, SMTP transport may not work properly");
|
||||
smtp_client
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(user), Some(pass), Some(_)) => {
|
||||
// Both password and OAuth2 configured - prefer OAuth2
|
||||
warn!("Both SMTP_PASSWORD and SMTP_OAUTH2_CLIENT_ID are set. Using OAuth2 authentication.");
|
||||
match get_valid_oauth2_token().await {
|
||||
Ok(token) => {
|
||||
// Pass the access token directly as password - lettre's Xoauth2 mechanism
|
||||
// will format it correctly as: user={user}\x01auth=Bearer {token}\x01\x01
|
||||
smtp_client.credentials(Credentials::new(user, token.access_token))
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error fetching OAuth2 token: {}", e);
|
||||
warn!("Falling back to password authentication");
|
||||
smtp_client.credentials(Credentials::new(user, pass))
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => smtp_client,
|
||||
};
|
||||
|
||||
@ -671,7 +790,7 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match smtp_transport().send(email).await {
|
||||
match smtp_transport().await.send(email).await {
|
||||
Ok(_) => Ok(()),
|
||||
// Match some common errors and make them more user friendly
|
||||
Err(e) => {
|
||||
|
||||
35
src/static/scripts/admin_settings.js
vendored
35
src/static/scripts/admin_settings.js
vendored
@ -26,6 +26,33 @@ function smtpTest(event) {
|
||||
);
|
||||
}
|
||||
|
||||
function oauth2RefreshToken(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (formHasChanges(config_form)) {
|
||||
alert("Config has been changed but not yet saved.\nPlease save the changes first before refreshing the OAuth2 token.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_post(`${BASE_URL}/admin/test/oauth2`,
|
||||
"OAuth2 token refreshed successfully",
|
||||
"Error refreshing OAuth2 token",
|
||||
null, false
|
||||
);
|
||||
}
|
||||
|
||||
function oauth2Authorize(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (formHasChanges(config_form)) {
|
||||
alert("Config has been changed but not yet saved.\nPlease save the changes first before starting OAuth2 authorization.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Redirect to the OAuth2 authorization endpoint
|
||||
window.location.href = `${BASE_URL}/admin/oauth2/authorize`;
|
||||
}
|
||||
|
||||
function getFormData() {
|
||||
let data = {};
|
||||
|
||||
@ -225,6 +252,14 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
|
||||
if (btnSmtpTest) {
|
||||
btnSmtpTest.addEventListener("click", smtpTest);
|
||||
}
|
||||
const btnOAuth2Refresh = document.getElementById("oauth2RefreshTest");
|
||||
if (btnOAuth2Refresh) {
|
||||
btnOAuth2Refresh.addEventListener("click", oauth2RefreshToken);
|
||||
}
|
||||
const btnOAuth2Authorize = document.getElementById("oauth2Authorize");
|
||||
if (btnOAuth2Authorize) {
|
||||
btnOAuth2Authorize.addEventListener("click", oauth2Authorize);
|
||||
}
|
||||
|
||||
config_form.addEventListener("submit", saveConfig);
|
||||
|
||||
|
||||
10
src/static/templates/admin/oauth2_success.hbs
Normal file
10
src/static/templates/admin/oauth2_success.hbs
Normal file
@ -0,0 +1,10 @@
|
||||
<main class="container-xl">
|
||||
<div class="alert alert-success mt-5" role="alert">
|
||||
<h4 class="alert-heading">OAuth2 Authorization Successful</h4>
|
||||
<p>The refresh token has been saved to your configuration.</p>
|
||||
<hr>
|
||||
<p class="mb-0">
|
||||
<a href="{{admin_url}}" class="btn btn-primary">Return to Admin Settings</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
@ -50,6 +50,36 @@
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{#case group "smtp"}}
|
||||
<div class="row my-2 pt-3 border-top">
|
||||
<div class="col-sm-11">
|
||||
<div class="alert alert-info mb-0" role="alert">
|
||||
<h6 class="alert-heading"><i class="bi bi-info-circle"></i> OAuth2/XOAUTH2 Authentication</h6>
|
||||
<p class="mb-2 small">
|
||||
OAuth2 authentication allows secure email sending without storing your email password.
|
||||
Configure the OAuth2 fields below to use token-based authentication (recommended for Gmail and Microsoft 365).
|
||||
</p>
|
||||
<p class="mb-0 small">
|
||||
<strong>Note:</strong> If both password and OAuth2 credentials are configured, OAuth2 will be preferred with automatic fallback to password authentication.
|
||||
See the <a href="https://github.com/dani-garcia/vaultwarden/wiki/SMTP-Configuration#oauth2-authentication" target="_blank" rel="noopener noreferrer" class="alert-link">documentation</a> for detailed setup instructions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2 align-items-center pt-3 border-top" title="Start OAuth2 authorization flow to obtain refresh token">
|
||||
<label class="col-sm-3 col-form-label">Obtain OAuth2 Refresh Token</label>
|
||||
<div class="col-sm-8">
|
||||
<button type="button" class="btn btn-primary" id="oauth2Authorize">Start Authorization</button>
|
||||
<small class="form-text text-muted d-block mt-1">
|
||||
Click to authenticate with your email provider and obtain a refresh token.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2 align-items-center pt-3 border-top" title="Manually refresh OAuth2 token">
|
||||
<label class="col-sm-3 col-form-label">Refresh OAuth2 Token</label>
|
||||
<div class="col-sm-8">
|
||||
<button type="button" class="btn btn-outline-primary" id="oauth2RefreshTest">Refresh Token</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row my-2 align-items-center pt-3 border-top" title="Send a test email to given email address">
|
||||
<label for="smtp-test-email" class="col-sm-3 col-form-label">Test SMTP</label>
|
||||
<div class="col-sm-8 input-group">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user