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")
}
}