ws-auth 0.0.3

A library to help build authentication services and client libraries for web services.
Documentation
#[cfg(test)]
pub mod tests;

use std::str::FromStr;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum AuthFlow {
    Implicit,
    SinglePageApplication,
    Hybrid,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum ResponseMode {
    #[serde(rename = "query")]
    Query,

    #[serde(rename = "fragment")]
    Fragment,

    #[serde(rename = "form_post")]
    FormPost,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum RequestPrompt {
    #[serde(rename = "none")]
    None,

    #[serde(rename = "login")]
    Login,

    #[serde(rename = "consent")]
    Consent,

    #[serde(rename = "select_account")]
    SelectAccount,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum ResponseType {
    #[serde(rename = "none")]
    None,

    #[serde(rename = "code")]
    Code,

    #[serde(rename = "token")]
    Token,

    #[serde(rename = "id_token")]
    IdToken,
}

impl FromStr for ResponseType {
    type Err = ();

    fn from_str(input: &str) -> Result<ResponseType, Self::Err> {
        match input {
            "none" => Ok(ResponseType::None),
            "code" => Ok(ResponseType::Code),
            "token" => Ok(ResponseType::Token),
            "id_token" => Ok(ResponseType::IdToken),
            _ => Err(()),
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum AuthErrorCode {
    /// Description
    /// Protocol error, such as a missing required parameter.
    ///
    /// Action:
    /// Fix and resubmit the request. This error is a development error typically caught during initial testing.
    ///
    #[serde(rename = "invalid_request")]
    InvalidRequest,

    /// Description:
    /// The client application isn't permitted to request an authorization code.
    ///
    /// Action:
    /// This error usually occurs when the client application isn't registered or isn't added to the user's tenant.
    /// The application can prompt the user with instruction for installing the application and adding it.
    #[serde(rename = "unauthorized_client")]
    UnauthorizedClient,

    /// Description:
    /// Resource owner denied consent.
    ///
    /// Action:
    /// The client application can notify the user that it can't continue unless the user consents.
    #[serde(rename = "access_denied")]
    AccessDenied,

    /// Description:
    /// The authorization server doesn't support the response type in the request.
    ///
    /// Action:
    /// Fix and resubmit the request. This error is a development error typically caught during initial testing.
    /// In the hybrid flow, this error signals that you must enable the ID token implicit grant setting on the client app registration.
    #[serde(rename = "unsupported_response_type")]
    UnsupportedResponseType,

    /// Description:
    /// The server encountered an unexpected error.
    ///
    /// Action:
    /// Retry the request. These errors can result from temporary conditions.
    /// The client application might explain to the user that its response is delayed to a temporary error.
    #[serde(rename = "server_error")]
    ServerError,

    /// Description:
    /// The server is temporarily too busy to handle the request.
    ///
    /// Action:
    /// Retry the request. The client application might explain to the user that its response is delayed because of a temporary condition.
    #[serde(rename = "temporarily_unavailable")]
    TemporarilyUnavailable,

    /// Description:
    /// The target resource is invalid because it does not exist, or it's not correctly configured.
    ///
    /// Action:
    /// This error indicates the resource, if it exists, hasn't been configured in the tenant.
    /// The application can prompt the user with instruction for installing the application and adding it.
    #[serde(rename = "invalid_resource")]
    InvalidResource,

    /// Description:
    /// Too many or no users found.
    ///
    /// Action:
    /// The client requested silent authentication (prompt=none), but a single user couldn't be found.
    /// This error may mean there are multiple users active in the session, or no users.
    /// This error takes into account the tenant chosen. For example, if there are two accounts active and one social account,
    /// and social is chosen, silent authentication works.
    #[serde(rename = "login_required")]
    LoginRequired,

    /// Description:
    /// The request requires user interaction.
    ///
    /// Action:
    /// Another authentication step or consent is required. Retry the request without prompt=none.
    #[serde(rename = "interaction_required")]
    InteractionRequired,
}

/// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuthRequest {
    /// Required - TODO: Extract this from the path!
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub tenant: Option<String>,

    /// Required
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub client_id: Option<String>,

    /// Required
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub response_type: Option<String>,

    /// Required
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub redirect_uri: Option<String>,

    /// Required
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub scope: Option<String>,

    /// Recommended (query, fragment, form_post)
    /// query: is unsupported when requesting an id token by using an implicit flow
    /// fragment: id token or ONLY code
    /// form_post: when requesting code
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub response_mode: Option<ResponseMode>,

    /// Recommended
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub state: Option<String>,

    /// Optional
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub prompt: Option<RequestPrompt>,

    /// Optional
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub login_hint: Option<String>,

    /// Optional
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub domain_hint: Option<String>,

    /// Recommended, Required when code_challenge_method is included
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub code_challenge: Option<String>,

    /// Recommended, Required
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub code_challenge_method: Option<String>,
}

impl AuthRequest {
    pub fn get_response_types(&self) -> Vec<ResponseType> {
        let mut response_types: Vec<ResponseType> = Vec::new();
        let raw_response_types_str = self.response_type.to_owned().unwrap();
        let response_types_str = raw_response_types_str.split(" ");
        for response_type_str in response_types_str.into_iter() {
            let response_type_result: Result<ResponseType, _> =
                ResponseType::from_str(response_type_str);

            if response_type_result.is_ok() {
                response_types.push(response_type_result.unwrap());
            }
        }

        return response_types;
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuthResponse {
    /// CODE that is to be exchanged for an Access Token
    /// CODE + ID_TOKEN = Hybrid Flow
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub code: Option<String>,

    /// response_type = token
    /// If TOKEN is included in the request, the access_token is returned immediately
    /// without making a secondary request ...
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub access_token: Option<String>,

    /// response_type = token
    /// Will always be `Bearer` if used
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub token_type: Option<String>,

    /// response_type = token
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub expires_in: Option<u64>,

    /// Only provided if `id_token` response_type was specified and `openid` scope was requested.
    /// This should never be allowed / used for authorization purposes.
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub id_token: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub state: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub scope: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub refresh_token: Option<String>,

    #[serde(skip_serializing)]
    pub callback_uri: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuthErrorResponse {
    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub error: Option<AuthErrorCode>,

    #[serde(skip_serializing_if = "Option::is_none")]
    #[serde(default)]
    pub error_description: Option<String>,

    #[serde(skip_serializing)]
    pub callback_uri: Option<String>,
}