Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-09-28 21:53:55 +02:00
bind Duo oauth flow to device id, drop redundant device type handling
Dieser Commit ist enthalten in:
Ursprung
fde54f3b18
Commit
f817c15f3a
3 geänderte Dateien mit 38 neuen und 64 gelöschten Zeilen
|
@ -1,12 +1,14 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use reqwest::{header, StatusCode};
|
use reqwest::{header, StatusCode};
|
||||||
|
use ring::digest::{digest, Digest, SHA512_256};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
|
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
|
||||||
auth::ClientType,
|
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::{
|
db::{models::{
|
||||||
EventType,
|
EventType,
|
||||||
|
@ -423,10 +425,13 @@ fn make_callback_url(client_name: &str) -> Result<String, Error> {
|
||||||
|
|
||||||
// Pre-redirect first stage of the Duo WebSDKv4 authentication flow.
|
// Pre-redirect first stage of the Duo WebSDKv4 authentication flow.
|
||||||
// Returns the "AuthUrl" that should be returned to clients for MFA.
|
// Returns the "AuthUrl" that should be returned to clients for MFA.
|
||||||
pub async fn get_duo_auth_url(email: &str, client_type: &ClientType, conn: &mut DbConn) -> Result<String, Error> {
|
pub async fn get_duo_auth_url(email: &str,
|
||||||
|
client_id: &String,
|
||||||
|
device_identifier: &String,
|
||||||
|
conn: &mut DbConn) -> Result<String, Error> {
|
||||||
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||||
|
|
||||||
let callback_url = match make_callback_url(client_type.as_str()) {
|
let callback_url = match make_callback_url(client_id.as_str()) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(e) => err!(format!("{}", e)),
|
Err(e) => err!(format!("{}", e)),
|
||||||
};
|
};
|
||||||
|
@ -439,11 +444,16 @@ pub async fn get_duo_auth_url(email: &str, client_type: &ClientType, conn: &mut
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate random OAuth2 state and OIDC Nonce
|
// Generate random OAuth2 state and OIDC Nonce
|
||||||
let state = generate_state();
|
let state: String = generate_state();
|
||||||
let nonce = generate_state();
|
let nonce: String = generate_state();
|
||||||
|
|
||||||
|
// Bind the nonce to the device that's currently authing by hashing the nonce and device id
|
||||||
|
// and sending that as the OIDC nonce.
|
||||||
|
let d: Digest = digest(&SHA512_256, format!("{nonce}{device_identifier}").as_bytes());
|
||||||
|
let hash: String = HEXLOWER.encode(d.as_ref());
|
||||||
|
|
||||||
match TwoFactorDuoContext::save(state.as_str(), email, nonce.as_str(), CTX_VALIDITY_SECS, conn).await {
|
match TwoFactorDuoContext::save(state.as_str(), email, nonce.as_str(), CTX_VALIDITY_SECS, conn).await {
|
||||||
Ok(()) => client.make_authz_req_url(email, state, nonce),
|
Ok(()) => client.make_authz_req_url(email, state, hash),
|
||||||
Err(e) => err!(format!("Error storing Duo authentication context: {}", e))
|
Err(e) => err!(format!("Error storing Duo authentication context: {}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -453,12 +463,10 @@ pub async fn get_duo_auth_url(email: &str, client_type: &ClientType, conn: &mut
|
||||||
pub async fn validate_duo_login(
|
pub async fn validate_duo_login(
|
||||||
email: &str,
|
email: &str,
|
||||||
two_factor_token: &str,
|
two_factor_token: &str,
|
||||||
client_type: &ClientType,
|
client_id: &String,
|
||||||
|
device_identifier: &String,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> EmptyResult {
|
) -> EmptyResult {
|
||||||
// TODO: The OIDC nonce should somehow be bound to a specific authentication attempt.
|
|
||||||
// e.g. hashed and in an httponly cookie.
|
|
||||||
// This may not be possible given the way that BW clients redirect users for final auth.
|
|
||||||
let email = &email.to_lowercase();
|
let email = &email.to_lowercase();
|
||||||
|
|
||||||
let split: Vec<&str> = two_factor_token.split('|').collect();
|
let split: Vec<&str> = two_factor_token.split('|').collect();
|
||||||
|
@ -506,7 +514,7 @@ pub async fn validate_duo_login(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let callback_url = match make_callback_url(client_type.as_str()) {
|
let callback_url = match make_callback_url(client_id.as_str()) {
|
||||||
Ok(url) => url,
|
Ok(url) => url,
|
||||||
Err(e) => err!(format!("{}", e)),
|
Err(e) => err!(format!("{}", e)),
|
||||||
};
|
};
|
||||||
|
@ -518,7 +526,10 @@ pub async fn validate_duo_login(
|
||||||
Err(e) => err!(format!("{}", e)),
|
Err(e) => err!(format!("{}", e)),
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.exchange_authz_code_for_result(code, email, ctx.nonce.as_str()).await {
|
let d: Digest = digest(&SHA512_256, format!("{}{}", ctx.nonce, device_identifier).as_bytes());
|
||||||
|
let hash: String = HEXLOWER.encode(d.as_ref());
|
||||||
|
|
||||||
|
match client.exchange_authz_code_for_result(code, email, hash.as_str()).await {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
err!(
|
err!(
|
||||||
|
|
|
@ -17,7 +17,7 @@ use crate::{
|
||||||
push::register_push_device,
|
push::register_push_device,
|
||||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
},
|
},
|
||||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, ClientType},
|
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util, CONFIG,
|
||||||
|
@ -48,7 +48,7 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
|
||||||
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
|
||||||
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip, client_header.client_type).await
|
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
"client_credentials" => {
|
"client_credentials" => {
|
||||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
|
@ -140,7 +140,6 @@ async fn _password_login(
|
||||||
user_uuid: &mut Option<String>,
|
user_uuid: &mut Option<String>,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_type: ClientType,
|
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
// Validate scope
|
// Validate scope
|
||||||
let scope = data.scope.as_ref().unwrap();
|
let scope = data.scope.as_ref().unwrap();
|
||||||
|
@ -251,7 +250,7 @@ async fn _password_login(
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, &client_type, conn).await?;
|
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
||||||
|
@ -486,7 +485,6 @@ async fn twofactor_auth(
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
client_type: &ClientType,
|
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||||
|
@ -504,7 +502,7 @@ async fn twofactor_auth(
|
||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, client_type, conn).await?, "2FA token not provided"),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, &data, conn).await?, "2FA token not provided"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
|
@ -528,7 +526,11 @@ async fn twofactor_auth(
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
// OIDC based flow
|
// OIDC based flow
|
||||||
duo_oidc::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, client_type, conn).await?
|
duo_oidc::validate_duo_login(data.username.as_ref().unwrap().trim(),
|
||||||
|
twofactor_code,
|
||||||
|
data.client_id.as_ref().unwrap(),
|
||||||
|
data.device_identifier.as_ref().unwrap(),
|
||||||
|
conn).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -543,7 +545,7 @@ async fn twofactor_auth(
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
err_json!(
|
err_json!(
|
||||||
_json_err_twofactor(&twofactor_ids, &user.uuid, client_type, conn).await?,
|
_json_err_twofactor(&twofactor_ids, &user.uuid, &data, conn).await?,
|
||||||
"2FA Remember token not provided"
|
"2FA Remember token not provided"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -571,7 +573,7 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||||
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, client_type: &ClientType, conn: &mut DbConn) -> ApiResult<Value> {
|
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, data: &ConnectData, conn: &mut DbConn) -> ApiResult<Value> {
|
||||||
let mut result = json!({
|
let mut result = json!({
|
||||||
"error" : "invalid_grant",
|
"error" : "invalid_grant",
|
||||||
"error_description" : "Two factor required.",
|
"error_description" : "Two factor required.",
|
||||||
|
@ -607,7 +609,10 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, client_type: &C
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
// OIDC based flow
|
// OIDC based flow
|
||||||
let auth_url = duo_oidc::get_duo_auth_url(&email, client_type, conn).await?;
|
let auth_url = duo_oidc::get_duo_auth_url(&email,
|
||||||
|
data.client_id.as_ref().unwrap(),
|
||||||
|
data.device_identifier.as_ref().unwrap(),
|
||||||
|
conn).await?;
|
||||||
|
|
||||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||||
"AuthUrl": auth_url,
|
"AuthUrl": auth_url,
|
||||||
|
|
42
src/auth.rs
42
src/auth.rs
|
@ -404,47 +404,9 @@ impl<'r> FromRequest<'r> for Host {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ClientType {
|
|
||||||
Unspecified = 0,
|
|
||||||
Web = 1,
|
|
||||||
Browser = 2,
|
|
||||||
Desktop = 3,
|
|
||||||
Mobile = 4,
|
|
||||||
Cli = 5,
|
|
||||||
DirectoryConnector = 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientType {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ClientType::Unspecified => "",
|
|
||||||
ClientType::Web => "web",
|
|
||||||
ClientType::Browser => "browser",
|
|
||||||
ClientType::Desktop => "desktop",
|
|
||||||
ClientType::Mobile => "mobile",
|
|
||||||
ClientType::Cli => "cli",
|
|
||||||
ClientType::DirectoryConnector => "connector",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_str(client_name: &str) -> ClientType {
|
|
||||||
match client_name {
|
|
||||||
"web" => ClientType::Web,
|
|
||||||
"browser" => ClientType::Browser,
|
|
||||||
"desktop" => ClientType::Desktop,
|
|
||||||
"mobile" => ClientType::Mobile,
|
|
||||||
"cli" => ClientType::Cli,
|
|
||||||
"connector" => ClientType::DirectoryConnector,
|
|
||||||
_ => ClientType::Unspecified,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub struct ClientHeaders {
|
pub struct ClientHeaders {
|
||||||
pub device_type: i32,
|
pub device_type: i32,
|
||||||
pub ip: ClientIp,
|
pub ip: ClientIp,
|
||||||
pub client_type: ClientType,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
|
@ -460,13 +422,9 @@ impl<'r> FromRequest<'r> for ClientHeaders {
|
||||||
let device_type: i32 =
|
let device_type: i32 =
|
||||||
request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14);
|
request.headers().get_one("device-type").map(|d| d.parse().unwrap_or(14)).unwrap_or_else(|| 14);
|
||||||
|
|
||||||
let client_name = request.headers().get_one("Bitwarden-Client-Name").unwrap_or_else(|| "");
|
|
||||||
let client_type: ClientType = ClientType::from_str(client_name);
|
|
||||||
|
|
||||||
Outcome::Success(ClientHeaders {
|
Outcome::Success(ClientHeaders {
|
||||||
device_type,
|
device_type,
|
||||||
ip,
|
ip,
|
||||||
client_type,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Laden …
In neuem Issue referenzieren