Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-16 04:12:53 +01:00
Add initial working Duo Universal Prompt support.
Dieser Commit ist enthalten in:
Ursprung
8f05a90b96
Commit
27e9c330a6
7 geänderte Dateien mit 563 neuen und 19 gelöschten Zeilen
|
@ -422,15 +422,21 @@
|
|||
# YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify
|
||||
|
||||
## Duo Settings
|
||||
## You need to configure all options to enable global Duo support, otherwise users would need to configure it themselves
|
||||
## You need to configure the DUO_IKEY, DUO_SKEY, and DUO_HOST options to enable global Duo support.
|
||||
## Otherwise users will need to configure it themselves.
|
||||
## Create an account and protect an application as mentioned in this link (only the first step, not the rest):
|
||||
## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account
|
||||
## Then set the following options, based on the values obtained from the last step:
|
||||
# DUO_IKEY=<Integration Key>
|
||||
# DUO_SKEY=<Secret Key>
|
||||
# DUO_IKEY=<Client ID>
|
||||
# DUO_SKEY=<Client Secret>
|
||||
# DUO_HOST=<API Hostname>
|
||||
## After that, you should be able to follow the rest of the guide linked above,
|
||||
## ignoring the fields that ask for the values that you already configured beforehand.
|
||||
##
|
||||
## If you want to attempt to use Duo's 'Traditional Prompt' (deprecated, iframe based) set DUO_USE_IFRAME to 'true'.
|
||||
## Duo no longer supports this, but it still works for some integrations.
|
||||
## If you aren't sure, leave this alone.
|
||||
# DUO_USE_IFRAME=false
|
||||
|
||||
## Email 2FA settings
|
||||
## Email token size
|
||||
|
|
|
@ -255,7 +255,7 @@ async fn get_user_duo_data(uuid: &str, conn: &mut DbConn) -> DuoStatus {
|
|||
}
|
||||
|
||||
// let (ik, sk, ak, host) = get_duo_keys();
|
||||
async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
|
||||
pub(crate) async fn get_duo_keys_email(email: &str, conn: &mut DbConn) -> ApiResult<(String, String, String, String)> {
|
||||
let data = match User::find_by_mail(email, conn).await {
|
||||
Some(u) => get_user_duo_data(&u.uuid, conn).await.data(),
|
||||
_ => DuoData::global(),
|
||||
|
|
473
src/api/core/two_factor/duo_oidc.rs
Normale Datei
473
src/api/core/two_factor/duo_oidc.rs
Normale Datei
|
@ -0,0 +1,473 @@
|
|||
use chrono::{TimeDelta, Utc};
|
||||
use jsonwebtoken::{decode_header, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use reqwest::{header, StatusCode};
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
use crate::{
|
||||
api::{core::two_factor::duo::get_duo_keys_email, EmptyResult},
|
||||
auth::ClientType,
|
||||
crypto,
|
||||
db::{models::EventType, DbConn},
|
||||
error::Error,
|
||||
util::get_reqwest_client,
|
||||
CONFIG,
|
||||
};
|
||||
|
||||
// Pool of characters for state and nonce generation
|
||||
// 0-9 -> 0x30-0x39
|
||||
// A-Z -> 0x41-0x5A
|
||||
// a-z -> 0x61-0x7A
|
||||
const STATE_CHAR_POOL: [u8; 62] = [
|
||||
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
|
||||
0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A,
|
||||
];
|
||||
|
||||
const MIN_STATE_SIZE: usize = 16;
|
||||
const MAX_STATE_SIZE: usize = 1024;
|
||||
const STATE_LENGTH: usize = 36; // Default size of state for generate_state_default()
|
||||
|
||||
// Client URL constants. Defined as macros, so they can be passed into format!()
|
||||
#[allow(non_snake_case)]
|
||||
macro_rules! HEALTH_ENDPOINT {
|
||||
() => {
|
||||
"https://{}/oauth/v1/health_check"
|
||||
};
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
macro_rules! AUTHZ_ENDPOINT {
|
||||
() => {
|
||||
"https://{}/oauth/v1/authorize"
|
||||
};
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
macro_rules! API_HOST_FMT {
|
||||
() => {
|
||||
"https://{}"
|
||||
};
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
macro_rules! TOKEN_ENDPOINT {
|
||||
() => {
|
||||
"https://{}/oauth/v1/token"
|
||||
};
|
||||
}
|
||||
|
||||
// Default JWT validity time
|
||||
const JWT_VALIDITY_SECS: i64 = 300;
|
||||
|
||||
// Generate a new Duo WebSDKv4 state string with a given size.
|
||||
// This can also be used to generate the optional OpenID Connect nonce.
|
||||
// Size must be between 16 and 1024 (inclusive).
|
||||
pub fn generate_state_len(size: usize) -> String {
|
||||
if (size < MIN_STATE_SIZE) || (MAX_STATE_SIZE < size) {
|
||||
panic!("Illegal Duo state size: {size}. Size must be 15 < size < 1025")
|
||||
}
|
||||
|
||||
return crypto::get_random_string(&STATE_CHAR_POOL, size);
|
||||
}
|
||||
|
||||
pub fn generate_state_default() -> String {
|
||||
return generate_state_len(STATE_LENGTH);
|
||||
}
|
||||
|
||||
// Structs for serializing calls to Duo
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct ClientAssertionJwt {
|
||||
pub iss: String,
|
||||
pub sub: String,
|
||||
pub aud: String,
|
||||
pub exp: i64,
|
||||
pub jti: String,
|
||||
pub iat: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AuthUrlJwt {
|
||||
pub response_type: String,
|
||||
pub scope: String,
|
||||
pub exp: i64,
|
||||
pub client_id: String,
|
||||
pub redirect_uri: String,
|
||||
pub state: String,
|
||||
pub duo_uname: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iss: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aud: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nonce: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_duo_code_attribute: Option<bool>,
|
||||
}
|
||||
|
||||
/*
|
||||
Structs for deserializing responses from Duo's API
|
||||
*/
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct HealthOKTS {
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum HealthCheckResponse {
|
||||
HealthOK {
|
||||
stat: String,
|
||||
response: HealthOKTS,
|
||||
},
|
||||
HealthFail {
|
||||
stat: String,
|
||||
code: i32,
|
||||
timestamp: i64,
|
||||
message: String,
|
||||
message_detail: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct IdTokenResponse {
|
||||
id_token: String,
|
||||
access_token: String,
|
||||
expires_in: i64,
|
||||
token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct IdTokenClaims {
|
||||
aud: String,
|
||||
iss: String,
|
||||
preferred_username: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nonce: Option<String>,
|
||||
}
|
||||
|
||||
// Duo WebSDK 4 Client
|
||||
struct DuoClient {
|
||||
client_id: String, // Duo Client ID (DuoData.ik)
|
||||
client_secret: String, // Duo Client Secret (DuoData.sk)
|
||||
api_host: String, // Duo API hostname (DuoData.host)
|
||||
redirect_uri: String, // URL in this application clients should call for MFA verification
|
||||
jwt_exp_seconds: i64, // Number of seconds that JWTs we create should be valid for
|
||||
}
|
||||
// TODO: Cert pinning for calls to Duo?
|
||||
|
||||
// See https://duo.com/docs/oauthapi
|
||||
impl DuoClient {
|
||||
fn new(client_id: String, client_secret: String, api_host: String, redirect_uri: String) -> DuoClient {
|
||||
return DuoClient {
|
||||
client_id,
|
||||
client_secret,
|
||||
api_host,
|
||||
redirect_uri,
|
||||
jwt_exp_seconds: JWT_VALIDITY_SECS,
|
||||
};
|
||||
}
|
||||
|
||||
// Given a serde-serializable struct, attempt to encode it as a JWT
|
||||
fn encode_duo_jwt<T: Serialize>(&self, jwt_payload: T) -> Result<String, Error> {
|
||||
match jsonwebtoken::encode(
|
||||
&Header::new(Algorithm::HS512),
|
||||
&jwt_payload,
|
||||
&EncodingKey::from_secret(&self.client_secret.as_bytes()),
|
||||
) {
|
||||
Ok(token) => Ok(token),
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
// "required" health check to verify the integration is configured and Duo's services
|
||||
// are up.
|
||||
// https://duo.com/docs/oauthapi#health-check
|
||||
async fn health_check(&self) -> Result<(), Error> {
|
||||
let health_check_url: String = format!(HEALTH_ENDPOINT!(), self.api_host);
|
||||
|
||||
let now = Utc::now();
|
||||
let jwt_id = generate_state_default();
|
||||
let jwt_payload = ClientAssertionJwt {
|
||||
iss: self.client_id.clone(),
|
||||
sub: self.client_id.clone(),
|
||||
aud: health_check_url.clone(),
|
||||
exp: (now + TimeDelta::try_seconds(self.jwt_exp_seconds).unwrap()).timestamp(),
|
||||
jti: jwt_id,
|
||||
iat: now.timestamp(),
|
||||
};
|
||||
|
||||
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||
Ok(token) => token,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
let mut post_body = HashMap::new();
|
||||
post_body.insert("client_assertion", token);
|
||||
post_body.insert("client_id", self.client_id.clone());
|
||||
|
||||
let res = match get_reqwest_client()
|
||||
.post(health_check_url)
|
||||
.header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)")
|
||||
.form(&post_body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Error requesting Duo health check: {}", e)),
|
||||
};
|
||||
|
||||
let response: HealthCheckResponse = match res.json::<HealthCheckResponse>().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Duo health check response decode error: {}", e)),
|
||||
};
|
||||
|
||||
let health_stat: String = match response {
|
||||
HealthCheckResponse::HealthOK {
|
||||
stat,
|
||||
response: _,
|
||||
} => stat,
|
||||
HealthCheckResponse::HealthFail {
|
||||
stat: _,
|
||||
code: _,
|
||||
timestamp: _,
|
||||
message,
|
||||
message_detail,
|
||||
} => err!(format!("Duo health check FAIL response msg: {}, detail: {}", message, message_detail)),
|
||||
};
|
||||
|
||||
if health_stat != "OK" {
|
||||
err!("Duo health check returned OK-like body but did not contain an OK stat.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Constructs the URL for the authorization request endpoint on Duo's service.
|
||||
// Clients are sent here to continue authentication.
|
||||
// https://duo.com/docs/oauthapi#authorization-request
|
||||
fn make_authz_req_url(&self, duo_username: &str, state: String, nonce: Option<String>) -> Result<String, Error> {
|
||||
let now = Utc::now();
|
||||
|
||||
let jwt_payload = AuthUrlJwt {
|
||||
response_type: String::from("code"),
|
||||
scope: String::from("openid"),
|
||||
exp: (now + TimeDelta::try_seconds(self.jwt_exp_seconds).unwrap()).timestamp(),
|
||||
client_id: self.client_id.clone(),
|
||||
redirect_uri: self.redirect_uri.clone(),
|
||||
state,
|
||||
duo_uname: String::from(duo_username),
|
||||
iss: Some(self.client_id.clone()),
|
||||
aud: Some(format!(API_HOST_FMT!(), self.api_host)),
|
||||
nonce,
|
||||
use_duo_code_attribute: Some(false),
|
||||
};
|
||||
|
||||
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||
Ok(token) => token,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
let authz_endpoint = format!(AUTHZ_ENDPOINT!(), self.api_host);
|
||||
let mut auth_url = match Url::parse(authz_endpoint.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
{
|
||||
let mut query_params = auth_url.query_pairs_mut();
|
||||
query_params.append_pair("response_type", "code");
|
||||
query_params.append_pair("client_id", self.client_id.as_str());
|
||||
query_params.append_pair("request", token.as_str());
|
||||
}
|
||||
|
||||
let final_auth_url = auth_url.to_string();
|
||||
return Ok(final_auth_url);
|
||||
}
|
||||
|
||||
async fn exchange_authz_code_for_result(
|
||||
&self,
|
||||
duo_code: &str,
|
||||
duo_username: &str,
|
||||
nonce: Option<&str>,
|
||||
) -> Result<(), Error> {
|
||||
if duo_code == "" {
|
||||
err!("Invalid Duo Code")
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let token_url = format!(TOKEN_ENDPOINT!(), self.api_host);
|
||||
let jwt_id = generate_state_default();
|
||||
|
||||
let jwt_payload = ClientAssertionJwt {
|
||||
iss: self.client_id.clone(),
|
||||
sub: self.client_id.clone(),
|
||||
aud: token_url.clone(),
|
||||
exp: (now + TimeDelta::try_seconds(self.jwt_exp_seconds).unwrap()).timestamp(),
|
||||
jti: jwt_id,
|
||||
iat: now.timestamp(),
|
||||
};
|
||||
|
||||
let token = match self.encode_duo_jwt(jwt_payload) {
|
||||
Ok(token) => token,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
let mut post_body = HashMap::new();
|
||||
post_body.insert("grant_type", String::from("authorization_code"));
|
||||
post_body.insert("code", String::from(duo_code));
|
||||
post_body.insert("redirect_uri", self.redirect_uri.clone());
|
||||
post_body
|
||||
.insert("client_assertion_type", String::from("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"));
|
||||
post_body.insert("client_assertion", token);
|
||||
|
||||
let res = match get_reqwest_client()
|
||||
.post(token_url.clone())
|
||||
.header(header::USER_AGENT, "vaultwarden:Duo/2.0 (Rust)")
|
||||
.form(&post_body)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Error exchanging Duo code: {}", e)),
|
||||
};
|
||||
|
||||
let status_code = res.status();
|
||||
if status_code != StatusCode::OK {
|
||||
err!(format!("Failure response from Duo: {}", status_code))
|
||||
}
|
||||
|
||||
let response: IdTokenResponse = match res.json::<IdTokenResponse>().await {
|
||||
Ok(r) => r,
|
||||
Err(e) => err!(format!("Error decoding ID token response: {}", e)),
|
||||
};
|
||||
|
||||
let header = decode_header(&response.id_token).unwrap();
|
||||
|
||||
let mut validation = Validation::new(header.alg);
|
||||
validation.set_required_spec_claims(&["exp", "aud", "iss"]);
|
||||
validation.set_audience(&[&self.client_id]);
|
||||
validation.set_issuer(&[token_url.as_str()]);
|
||||
|
||||
let token_data = match jsonwebtoken::decode::<IdTokenClaims>(
|
||||
&response.id_token,
|
||||
&DecodingKey::from_secret(self.client_secret.as_bytes()),
|
||||
&validation,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(e) => err!(format!("Failed to decode Duo token {}", e)),
|
||||
};
|
||||
|
||||
if !crypto::ct_eq(&duo_username, &token_data.claims.preferred_username) {
|
||||
err!(format!(
|
||||
"Error validating Duo user, expected {}, got {}",
|
||||
duo_username, token_data.claims.preferred_username
|
||||
))
|
||||
};
|
||||
|
||||
match nonce {
|
||||
Some(nonce) => {
|
||||
_ = nonce; // FIXME: Add Nonce support
|
||||
Ok(())
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the url that Duo should redirect users to.
|
||||
// The actual location is a bridge built in to the clients.
|
||||
// See: /clients/apps/web/src/connectors/duo-redirect.ts
|
||||
fn make_callback_url(client_name: &str) -> Result<String, Error> {
|
||||
const DUO_REDIRECT_LOCATION: &str = "duo-redirect-connector.html";
|
||||
|
||||
// Get the location of this application as defined in the config.
|
||||
let base = match Url::parse(CONFIG.domain().as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
// Add the client redirect bridge location
|
||||
let mut callback = match base.join(DUO_REDIRECT_LOCATION) {
|
||||
Ok(url) => url,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
// Add the 'client' string. This is sent by clients in the 'Bitwarden-Client-Name'
|
||||
// HTTP header of the request to /identity/connect/token
|
||||
{
|
||||
let mut query_params = callback.query_pairs_mut();
|
||||
query_params.append_pair("client", client_name);
|
||||
}
|
||||
return Ok(callback.to_string());
|
||||
}
|
||||
|
||||
// Initiates the first stage of the Duo WebSDKv4 authentication flow.
|
||||
// Returns the "AuthUrl" that should be passed to clients for MFA.
|
||||
pub async fn get_duo_auth_url(email: &str, client_type: &ClientType, conn: &mut DbConn) -> Result<String, Error> {
|
||||
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||
|
||||
let callback_url = match make_callback_url(client_type.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
let client = DuoClient::new(ik, sk, host, callback_url);
|
||||
|
||||
match client.health_check().await {
|
||||
Ok(()) => {}
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
// Generate a random Duo state and OIDC Nonce
|
||||
let state = generate_state_default();
|
||||
|
||||
return client.make_authz_req_url(email, state, None);
|
||||
}
|
||||
|
||||
pub async fn validate_duo_login(
|
||||
email: &str,
|
||||
two_factor_token: &str,
|
||||
client_type: &ClientType,
|
||||
conn: &mut DbConn,
|
||||
) -> EmptyResult {
|
||||
let email = &email.to_lowercase();
|
||||
|
||||
let split: Vec<&str> = two_factor_token.split('|').collect();
|
||||
if split.len() != 2 {
|
||||
err!(
|
||||
"Invalid response length",
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn2fa
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
let code = split[0];
|
||||
//let state = split[1];
|
||||
|
||||
let (ik, sk, _, host) = get_duo_keys_email(email, conn).await?;
|
||||
|
||||
let callback_url = match make_callback_url(client_type.as_str()) {
|
||||
Ok(url) => url,
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
let client = DuoClient::new(ik, sk, host, callback_url);
|
||||
|
||||
match client.health_check().await {
|
||||
Ok(()) => {}
|
||||
Err(e) => err!(format!("{}", e)),
|
||||
};
|
||||
|
||||
match client.exchange_authz_code_for_result(code, email, None).await {
|
||||
Ok(_r) => Ok(()),
|
||||
Err(_e) => {
|
||||
err!(
|
||||
"Error validating duo authentication",
|
||||
ErrorEvent {
|
||||
event: EventType::UserFailedLogIn2fa
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ pub mod email;
|
|||
pub mod protected_actions;
|
||||
pub mod webauthn;
|
||||
pub mod yubikey;
|
||||
pub mod duo_oidc;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut routes = routes![
|
||||
|
|
|
@ -12,12 +12,12 @@ use crate::{
|
|||
core::{
|
||||
accounts::{PreloginData, RegisterData, _prelogin, _register},
|
||||
log_user_event,
|
||||
two_factor::{authenticator, duo, email, enforce_2fa_policy, webauthn, yubikey},
|
||||
two_factor::{authenticator, duo, duo_oidc, email, enforce_2fa_policy, webauthn, yubikey},
|
||||
},
|
||||
push::register_push_device,
|
||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||
},
|
||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp, ClientType},
|
||||
db::{models::*, DbConn},
|
||||
error::MapResult,
|
||||
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_type, "device_type cannot be blank")?;
|
||||
|
||||
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||
_password_login(data, &mut user_uuid, &mut conn, &client_header.ip, client_header.client_type).await
|
||||
}
|
||||
"client_credentials" => {
|
||||
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||
|
@ -140,6 +140,7 @@ async fn _password_login(
|
|||
user_uuid: &mut Option<String>,
|
||||
conn: &mut DbConn,
|
||||
ip: &ClientIp,
|
||||
client_type: ClientType,
|
||||
) -> JsonResult {
|
||||
// Validate scope
|
||||
let scope = data.scope.as_ref().unwrap();
|
||||
|
@ -250,7 +251,7 @@ async fn _password_login(
|
|||
|
||||
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||
|
||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, conn).await?;
|
||||
let twofactor_token = twofactor_auth(&user, &data, &mut device, ip, &client_type, conn).await?;
|
||||
|
||||
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 {
|
||||
|
@ -485,6 +486,7 @@ async fn twofactor_auth(
|
|||
data: &ConnectData,
|
||||
device: &mut Device,
|
||||
ip: &ClientIp,
|
||||
client_type: &ClientType,
|
||||
conn: &mut DbConn,
|
||||
) -> ApiResult<Option<String>> {
|
||||
let twofactors = TwoFactor::find_by_user(&user.uuid, conn).await;
|
||||
|
@ -502,7 +504,7 @@ async fn twofactor_auth(
|
|||
|
||||
let twofactor_code = match data.two_factor_token {
|
||||
Some(ref code) => code,
|
||||
None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?, "2FA token not provided"),
|
||||
None => err_json!(_json_err_twofactor(&twofactor_ids, &user.uuid, client_type, conn).await?, "2FA token not provided"),
|
||||
};
|
||||
|
||||
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||
|
@ -518,8 +520,15 @@ async fn twofactor_auth(
|
|||
}
|
||||
Some(TwoFactorType::Webauthn) => webauthn::validate_webauthn_login(&user.uuid, twofactor_code, conn).await?,
|
||||
Some(TwoFactorType::YubiKey) => yubikey::validate_yubikey_login(twofactor_code, &selected_data?).await?,
|
||||
Some(TwoFactorType::Duo) => {
|
||||
duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?
|
||||
Some(TwoFactorType::Duo | TwoFactorType::OrganizationDuo) => {
|
||||
match CONFIG.duo_use_iframe() {
|
||||
true => {
|
||||
duo::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, conn).await?
|
||||
}
|
||||
false => {
|
||||
duo_oidc::validate_duo_login(data.username.as_ref().unwrap().trim(), twofactor_code, client_type, conn).await?
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(TwoFactorType::Email) => {
|
||||
email::validate_email_code_str(&user.uuid, twofactor_code, &selected_data?, conn).await?
|
||||
|
@ -532,7 +541,7 @@ async fn twofactor_auth(
|
|||
}
|
||||
_ => {
|
||||
err_json!(
|
||||
_json_err_twofactor(&twofactor_ids, &user.uuid, conn).await?,
|
||||
_json_err_twofactor(&twofactor_ids, &user.uuid, client_type, conn).await?,
|
||||
"2FA Remember token not provided"
|
||||
)
|
||||
}
|
||||
|
@ -560,7 +569,7 @@ fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
|||
tf.map(|t| t.data).map_res("Two factor doesn't exist")
|
||||
}
|
||||
|
||||
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbConn) -> ApiResult<Value> {
|
||||
async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, client_type: &ClientType, conn: &mut DbConn) -> ApiResult<Value> {
|
||||
let mut result = json!({
|
||||
"error" : "invalid_grant",
|
||||
"error_description" : "Two factor required.",
|
||||
|
@ -579,18 +588,29 @@ async fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &mut DbCo
|
|||
result["TwoFactorProviders2"][provider.to_string()] = request.0;
|
||||
}
|
||||
|
||||
Some(TwoFactorType::Duo) => {
|
||||
Some(TwoFactorType::Duo | TwoFactorType::OrganizationDuo) => {
|
||||
let email = match User::find_by_uuid(user_uuid, conn).await {
|
||||
Some(u) => u.email,
|
||||
None => err!("User does not exist"),
|
||||
};
|
||||
|
||||
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
||||
// Should we try to use the legacy iframe prompt?
|
||||
match CONFIG.duo_use_iframe() {
|
||||
true => {
|
||||
let (signature, host) = duo::generate_duo_signature(&email, conn).await?;
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Host": host,
|
||||
"Signature": signature,
|
||||
})
|
||||
}
|
||||
false => {
|
||||
let auth_url = duo_oidc::get_duo_auth_url(&email, client_type, conn).await?;
|
||||
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"Host": host,
|
||||
"Signature": signature,
|
||||
});
|
||||
result["TwoFactorProviders2"][provider.to_string()] = json!({
|
||||
"AuthUrl": auth_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(tf_type @ TwoFactorType::YubiKey) => {
|
||||
|
|
42
src/auth.rs
42
src/auth.rs
|
@ -404,9 +404,47 @@ 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 device_type: i32,
|
||||
pub ip: ClientIp,
|
||||
pub client_type: ClientType,
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
|
@ -422,9 +460,13 @@ impl<'r> FromRequest<'r> for ClientHeaders {
|
|||
let device_type: i32 =
|
||||
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 {
|
||||
device_type,
|
||||
ip,
|
||||
client_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -622,6 +622,8 @@ make_config! {
|
|||
duo: _enable_duo {
|
||||
/// Enabled
|
||||
_enable_duo: bool, true, def, true;
|
||||
/// Attempt to use deprecated iframe-based Traditional Prompt (Duo WebSDK 2)
|
||||
duo_use_iframe: bool, false, def, false;
|
||||
/// Integration Key
|
||||
duo_ikey: String, true, option;
|
||||
/// Secret Key
|
||||
|
|
Laden …
In neuem Issue referenzieren