rustauth-oauth-provider 0.3.0

OAuth 2.1 and OpenID Connect provider support for RustAuth.
Documentation
use rustauth_core::db::DbAdapter;
use rustauth_core::error::RustAuthError;

use crate::consent::{find_consent_for_reference, has_granted_scopes};
use crate::models::SchemaClient;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizeDecision {
    IssueCode,
    RedirectToLogin,
    RedirectToConsent,
    RedirectError {
        error: &'static str,
        description: &'static str,
    },
}

pub async fn decide_authorize(
    adapter: &dyn DbAdapter,
    client: &SchemaClient,
    session_user_id: Option<&str>,
    requested_scopes: &[String],
    prompt: Option<&str>,
    consent_reference_id: Option<&str>,
) -> Result<AuthorizeDecision, RustAuthError> {
    let prompt = PromptSet::parse(prompt);
    if prompt.has_none_with_supported_prompt() {
        return Ok(AuthorizeDecision::RedirectError {
            error: "invalid_request",
            description: PROMPT_NONE_EXCLUSIVE_ERROR,
        });
    }
    let prompt_none = prompt.contains("none");
    if session_user_id.is_none() || prompt.contains("login") {
        return if prompt_none {
            Ok(AuthorizeDecision::RedirectError {
                error: "login_required",
                description: "authentication required",
            })
        } else {
            Ok(AuthorizeDecision::RedirectToLogin)
        };
    }

    if prompt.contains("consent") {
        return if prompt_none {
            Ok(AuthorizeDecision::RedirectError {
                error: "consent_required",
                description: "End-User consent is required",
            })
        } else {
            Ok(AuthorizeDecision::RedirectToConsent)
        };
    }

    if client.skip_consent == Some(true) {
        return Ok(AuthorizeDecision::IssueCode);
    }

    let user_id = session_user_id.unwrap_or_default();
    let consent =
        find_consent_for_reference(adapter, user_id, &client.client_id, consent_reference_id)
            .await?;
    if consent
        .as_ref()
        .is_some_and(|consent| has_granted_scopes(consent, requested_scopes))
    {
        return Ok(AuthorizeDecision::IssueCode);
    }

    if prompt_none {
        Ok(AuthorizeDecision::RedirectError {
            error: "consent_required",
            description: "End-User consent is required",
        })
    } else {
        Ok(AuthorizeDecision::RedirectToConsent)
    }
}

pub(crate) fn prompt_validation_error(
    prompt: Option<&str>,
) -> Option<(&'static str, &'static str)> {
    let prompt = PromptSet::parse(prompt);
    prompt
        .has_none_with_supported_prompt()
        .then_some(("invalid_request", PROMPT_NONE_EXCLUSIVE_ERROR))
}

const PROMPT_NONE_EXCLUSIVE_ERROR: &str = "prompt none must only be used alone";

struct PromptSet<'a> {
    values: Vec<&'a str>,
}

impl<'a> PromptSet<'a> {
    fn parse(prompt: Option<&'a str>) -> Self {
        Self {
            values: prompt
                .unwrap_or_default()
                .split_whitespace()
                .filter(|value| {
                    matches!(
                        *value,
                        "none" | "login" | "consent" | "create" | "select_account"
                    )
                })
                .collect(),
        }
    }

    fn contains(&self, prompt: &str) -> bool {
        self.values.iter().any(|value| value == &prompt)
    }

    fn has_none_with_supported_prompt(&self) -> bool {
        self.contains("none") && self.values.iter().any(|value| *value != "none")
    }
}