crowbar 0.4.10

Securily generates temporary AWS credentials through Identity Providers using SAML
Documentation
use crate::providers::okta::auth::PUSH_WAIT_TIMEOUT;
use crate::providers::okta::factors::Factor;

use reqwest::Url;
use serde::Deserialize;
use std::collections::HashMap;
use std::fmt;

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
pub struct Response {
    pub state_token: Option<String>,
    pub session_token: Option<String>,
    expires_at: String,
    pub status: Status,
    pub factor_result: Option<FactorResult>,
    relay_state: Option<String>,
    #[serde(rename = "_links", default)]
    pub links: Option<HashMap<String, Links>>,
    #[serde(rename = "_embedded")]
    pub embedded: Option<Embedded>,
}

#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Status {
    Unauthenticated,
    PasswordWarn,
    PasswordExpired,
    Recovery,
    RecoveryChallenge,
    PasswordReset,
    LockedOut,
    MfaEnroll,
    MfaEnrollActivate,
    MfaRequired,
    MfaChallenge,
    Success,
}

#[derive(Deserialize, PartialEq, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FactorResult {
    Challenge,
    Success,
    Timeout,
    Waiting,
    Rejected,
}

impl fmt::Display for FactorResult {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            FactorResult::Waiting { .. } | FactorResult::Challenge => {
                write!(f, "Waiting for confirmation")
            }
            FactorResult::Timeout => {
                write!(f, "No verification after {} seconds", PUSH_WAIT_TIMEOUT)
            }
            FactorResult::Rejected { .. } => write!(f, "Verification challenge was rejected"),
            FactorResult::Success { .. } => write!(f, "Verification challenge was successful"),
        }
    }
}

#[derive(Deserialize, Debug, Clone)]
#[allow(dead_code)]
#[serde(rename_all = "camelCase")]
pub struct Embedded {
    #[serde(default)]
    pub factors: Option<Vec<Factor>>,
    #[serde(default)]
    pub factor: Option<Factor>,
    user: User,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(untagged)]
pub enum Links {
    Single(Link),
    Multi(Vec<Link>),
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Link {
    name: Option<String>,
    #[serde(with = "serde_str")]
    pub href: Url,
    hints: Hint,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Hint {
    allow: Vec<String>,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct User {
    id: String,
    profile: UserProfile,
}

#[derive(Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UserProfile {
    login: String,
    first_name: String,
    last_name: String,
    locale: String,
    time_zone: String,
}

#[cfg(test)]
mod test {
    use super::*;
    use anyhow::Result;
    use std::fs;

    #[test]
    fn parses_login_response() -> Result<()> {
        let response = serde_json::de::from_str::<Response>(&fs::read_to_string(
            "tests/fixtures/okta/login_response_mfa_required.json",
        )?)?;

        let factor_result = &response.factor_result.unwrap();
        let status = &response.status;
        let embedded = &response.embedded.unwrap();
        let factor = embedded.factors.clone().unwrap();
        let id = match factor.first().unwrap() {
            Factor::WebAuthn { ref id, .. } => Some(id),
            _ => None,
        };

        assert_eq!(factor_result, &FactorResult::Success);
        assert_eq!(status, &Status::MfaRequired);
        assert_eq!(id.unwrap(), "factor-id-webauthn");

        Ok(())
    }

    #[test]
    fn parses_webauthn_challenge_response() -> Result<()> {
        let response = serde_json::de::from_str::<Response>(&fs::read_to_string(
            "tests/fixtures/okta/challenge_response_webauthn.json",
        )?)?;

        let factor_result = &response.factor_result.unwrap();
        let status = &response.status;
        let embedded = &response.embedded.unwrap();
        let factor = embedded.factor.clone().unwrap();
        let (id, factor_embedded, profile) = match factor {
            Factor::WebAuthn {
                ref id,
                ref embedded,
                ref profile,
                ..
            } => (id, embedded.clone().unwrap(), profile),
            _ => panic!("Didn't find the expected factor!"),
        };

        assert_eq!(factor_result, &FactorResult::Challenge);
        assert_eq!(status, &Status::MfaChallenge);
        assert_eq!(
            factor_embedded.challenge.unwrap().challenge.unwrap(),
            "challenge"
        );
        assert_eq!(profile.credential_id, "credential-id");
        assert_eq!(id, "factor-id-webauthn");

        Ok(())
    }

    #[test]
    fn parses_login_response_with_unknown_factors() -> Result<()> {
        let response = serde_json::de::from_str::<Response>(&fs::read_to_string(
            "tests/fixtures/okta/login_response_unimplemented_factors.json",
        )?);

        assert!(response.is_ok());
        Ok(())
    }
}