rustauth-oauth 0.3.1

OAuth support for RustAuth.
Documentation
use std::collections::BTreeMap;

use super::error::OAuthError;
use super::request::{
    apply_client_authentication, is_protected_oauth_param, ClientAuthentication, OAuthFormRequest,
};
use super::tokens::ProviderOptions;
use super::utils::validate_code_verifier;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthorizationCodeRequest {
    pub code: String,
    pub redirect_uri: String,
    pub options: ProviderOptions,
    pub code_verifier: Option<String>,
    pub device_id: Option<String>,
    pub authentication: ClientAuthentication,
    pub headers: BTreeMap<String, String>,
    pub additional_params: BTreeMap<String, String>,
    pub override_params: BTreeMap<String, String>,
    pub resource: Vec<String>,
}

impl Default for AuthorizationCodeRequest {
    fn default() -> Self {
        Self {
            code: String::new(),
            redirect_uri: String::new(),
            options: ProviderOptions::default(),
            code_verifier: None,
            device_id: None,
            authentication: ClientAuthentication::Post,
            headers: BTreeMap::new(),
            additional_params: BTreeMap::new(),
            override_params: BTreeMap::new(),
            resource: Vec::new(),
        }
    }
}

impl AuthorizationCodeRequest {
    pub fn try_new(
        code: impl Into<String>,
        redirect_uri: impl Into<String>,
        options: ProviderOptions,
    ) -> Result<Self, OAuthError> {
        let code = code.into();
        if code.is_empty() {
            return Err(OAuthError::InvalidConfiguration(
                "authorization code cannot be empty".to_owned(),
            ));
        }
        let redirect_uri = redirect_uri.into();
        url::Url::parse(options.redirect_uri.as_deref().unwrap_or(&redirect_uri))?;
        Ok(Self {
            code,
            redirect_uri,
            options,
            ..Self::default()
        })
    }

    pub fn code_verifier(mut self, code_verifier: impl Into<String>) -> Self {
        self.code_verifier = Some(code_verifier.into());
        self
    }

    pub fn authentication(mut self, authentication: ClientAuthentication) -> Self {
        self.authentication = authentication;
        self
    }

    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.headers.insert(key.into(), value.into());
        self
    }

    /// Adds a non-sensitive extension form field if not already set.
    /// Security-critical keys (`state`, `redirect_uri`, PKCE, `grant_type`,
    /// `code`, and client credential fields) are ignored.
    pub fn additional_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.additional_params.insert(key.into(), value.into());
        self
    }

    /// Overrides a non-sensitive form field. Security-critical keys (`state`,
    /// `redirect_uri`, PKCE, `grant_type`, `code`, and client credential
    /// fields) are ignored so validated flow invariants and client credentials
    /// cannot be replaced after authentication is applied.
    pub fn override_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.override_params.insert(key.into(), value.into());
        self
    }

    pub fn resource(mut self, resource: impl Into<String>) -> Self {
        self.resource.push(resource.into());
        self
    }
}

pub fn create_authorization_code_request(
    input: AuthorizationCodeRequest,
) -> Result<OAuthFormRequest, OAuthError> {
    validate_authorization_code_request(&input)?;
    let mut request = OAuthFormRequest::new();
    for (key, value) in input.headers {
        request.set_header(key, value);
    }
    request.set_body("grant_type", "authorization_code");
    request.set_body("code", input.code);
    if let Some(code_verifier) = input.code_verifier {
        request.set_body("code_verifier", code_verifier);
    }
    if let Some(client_key) = &input.options.client_key {
        request.set_body("client_key", client_key);
    }
    if let Some(device_id) = input.device_id {
        request.set_body("device_id", device_id);
    }
    request.set_body(
        "redirect_uri",
        input
            .options
            .redirect_uri
            .as_deref()
            .unwrap_or(&input.redirect_uri),
    );
    for resource in input.resource {
        request.push_body("resource", resource);
    }
    apply_client_authentication(&mut request, &input.options, input.authentication, false)?;
    for (key, value) in input.additional_params {
        if is_protected_oauth_param(&key) || request.has_body(&key) {
            continue;
        }
        request.push_body(key, value);
    }
    for (key, value) in input.override_params {
        if is_protected_oauth_param(&key) {
            continue;
        }
        request.set_body(key, value);
    }
    Ok(request)
}

fn validate_authorization_code_request(input: &AuthorizationCodeRequest) -> Result<(), OAuthError> {
    if input.code.is_empty() {
        return Err(OAuthError::InvalidConfiguration(
            "authorization code cannot be empty".to_owned(),
        ));
    }
    let redirect_uri = input
        .options
        .redirect_uri
        .as_deref()
        .unwrap_or(&input.redirect_uri);
    url::Url::parse(redirect_uri)?;
    if let Some(code_verifier) = &input.code_verifier {
        validate_code_verifier(code_verifier)?;
    }
    Ok(())
}