7 changed files with 563 additions and 19 deletions
			
			
		@ -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 | 
				
			||||
 | 
					                } | 
				
			||||
 | 
					            ) | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue