rustauth-oauth-provider 0.3.0

OAuth 2.1 and OpenID Connect provider support for RustAuth.
Documentation
use rustauth_core::db::{DbAdapter, DbRecord, DbValue, Delete, FindOne, Update, Where};
use rustauth_core::error::RustAuthError;

use crate::models::OAuthConsent;
use crate::schema::OAUTH_CONSENT_MODEL;
use crate::utils::{create_query, now, random_id, string, string_array, timestamp};

/// Input for creating or updating an OAuth consent grant.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConsentGrantInput {
    pub client_id: String,
    pub user_id: Option<String>,
    pub reference_id: Option<String>,
    pub scopes: Vec<String>,
}

pub fn has_granted_scopes(consent: &OAuthConsent, requested: &[String]) -> bool {
    requested
        .iter()
        .all(|scope| consent.scopes.iter().any(|granted| granted == scope))
}

#[cfg_attr(not(feature = "test-util"), allow(dead_code))]
pub async fn find_consent(
    adapter: &dyn DbAdapter,
    user_id: &str,
    client_id: &str,
) -> Result<Option<OAuthConsent>, RustAuthError> {
    find_consent_for_reference(adapter, user_id, client_id, None).await
}

pub async fn find_consent_for_reference(
    adapter: &dyn DbAdapter,
    user_id: &str,
    client_id: &str,
    reference_id: Option<&str>,
) -> Result<Option<OAuthConsent>, RustAuthError> {
    adapter
        .find_one(consent_by_user_client_reference_query(
            user_id,
            client_id,
            reference_id,
        ))
        .await?
        .map(consent_from_record)
        .transpose()
}

pub async fn upsert_consent(
    adapter: &dyn DbAdapter,
    input: ConsentGrantInput,
) -> Result<OAuthConsent, RustAuthError> {
    let existing = match input.user_id.as_deref() {
        Some(user_id) => {
            find_consent_for_reference(
                adapter,
                user_id,
                &input.client_id,
                input.reference_id.as_deref(),
            )
            .await?
        }
        None => None,
    };
    let timestamp = now();
    if let Some(existing) = existing {
        let mut record = DbRecord::new();
        optional_string(&mut record, "reference_id", input.reference_id);
        record.insert("scopes".to_owned(), DbValue::StringArray(input.scopes));
        record.insert("updated_at".to_owned(), DbValue::Timestamp(timestamp));
        let id = existing.id;
        return adapter
            .update(apply_data(
                Update::new(OAUTH_CONSENT_MODEL).where_clause(string_where("id", &id)),
                record,
            ))
            .await?
            .map(consent_from_record)
            .transpose()?
            .ok_or_else(|| RustAuthError::Adapter("oauth consent disappeared".to_owned()));
    }

    let consent = OAuthConsent {
        id: random_id("oauth_consent"),
        client_id: input.client_id,
        user_id: input.user_id,
        reference_id: input.reference_id,
        scopes: input.scopes,
        created_at: timestamp,
        updated_at: timestamp,
    };
    let record = adapter
        .create(create_query(
            OAUTH_CONSENT_MODEL,
            consent_to_record(&consent),
        ))
        .await?;
    consent_from_record(record)
}

#[cfg_attr(not(feature = "test-util"), allow(dead_code))]
pub async fn delete_consent(
    adapter: &dyn DbAdapter,
    user_id: &str,
    client_id: &str,
) -> Result<(), RustAuthError> {
    delete_consent_for_reference(adapter, user_id, client_id, None).await
}

#[cfg_attr(not(feature = "test-util"), allow(dead_code))]
pub async fn delete_consent_for_reference(
    adapter: &dyn DbAdapter,
    user_id: &str,
    client_id: &str,
    reference_id: Option<&str>,
) -> Result<(), RustAuthError> {
    adapter
        .delete(consent_delete_query(user_id, client_id, reference_id))
        .await?;
    Ok(())
}

fn consent_by_user_client_reference_query(
    user_id: &str,
    client_id: &str,
    reference_id: Option<&str>,
) -> FindOne {
    let query = FindOne::new(OAUTH_CONSENT_MODEL)
        .where_clause(string_where("user_id", user_id))
        .where_clause(string_where("client_id", client_id));
    match reference_id {
        Some(reference_id) => query.where_clause(string_where("reference_id", reference_id)),
        None => query.where_clause(Where::new("reference_id", DbValue::Null)),
    }
}

#[cfg_attr(not(feature = "test-util"), allow(dead_code))]
fn consent_delete_query(user_id: &str, client_id: &str, reference_id: Option<&str>) -> Delete {
    let query = Delete::new(OAUTH_CONSENT_MODEL)
        .where_clause(string_where("user_id", user_id))
        .where_clause(string_where("client_id", client_id));
    match reference_id {
        Some(reference_id) => query.where_clause(string_where("reference_id", reference_id)),
        None => query.where_clause(Where::new("reference_id", DbValue::Null)),
    }
}

fn consent_to_record(consent: &OAuthConsent) -> DbRecord {
    let mut record = DbRecord::new();
    record.insert("id".to_owned(), DbValue::String(consent.id.clone()));
    record.insert(
        "client_id".to_owned(),
        DbValue::String(consent.client_id.clone()),
    );
    optional_string(&mut record, "user_id", consent.user_id.clone());
    optional_string(&mut record, "reference_id", consent.reference_id.clone());
    record.insert(
        "scopes".to_owned(),
        DbValue::StringArray(consent.scopes.clone()),
    );
    record.insert(
        "created_at".to_owned(),
        DbValue::Timestamp(consent.created_at),
    );
    record.insert(
        "updated_at".to_owned(),
        DbValue::Timestamp(consent.updated_at),
    );
    record
}

pub(crate) fn consent_from_record(record: DbRecord) -> Result<OAuthConsent, RustAuthError> {
    Ok(OAuthConsent {
        id: required_string(&record, "id")?,
        client_id: required_string(&record, "client_id")?,
        user_id: string(&record, "user_id"),
        reference_id: string(&record, "reference_id"),
        scopes: string_array(&record, "scopes").unwrap_or_default(),
        created_at: required_timestamp(&record, "created_at")?,
        updated_at: required_timestamp(&record, "updated_at")?,
    })
}

fn apply_data(mut query: Update, data: DbRecord) -> Update {
    for (field, value) in data {
        query = query.data(field, value);
    }
    query
}

fn optional_string(record: &mut DbRecord, field: &str, value: Option<String>) {
    record.insert(
        field.to_owned(),
        value.map(DbValue::String).unwrap_or(DbValue::Null),
    );
}

fn required_string(record: &DbRecord, field: &str) -> Result<String, RustAuthError> {
    string(record, field)
        .ok_or_else(|| RustAuthError::Adapter(format!("oauth consent missing {field}")))
}

fn required_timestamp(
    record: &DbRecord,
    field: &str,
) -> Result<time::OffsetDateTime, RustAuthError> {
    timestamp(record, field)
        .ok_or_else(|| RustAuthError::Adapter(format!("oauth consent missing {field}")))
}

fn string_where(field: &str, value: &str) -> Where {
    Where::new(field, DbValue::String(value.to_owned()))
}