openauth-oauth 0.0.5

OAuth support for OpenAuth.
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;
use time::{Duration, OffsetDateTime};

use super::error::OAuthError;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ClientId {
    Single(String),
    Multiple(Vec<String>),
}

impl ClientId {
    pub fn primary(&self) -> Option<&str> {
        match self {
            Self::Single(value) if !value.is_empty() => Some(value),
            Self::Single(_) => None,
            Self::Multiple(values) => values
                .first()
                .map(String::as_str)
                .filter(|value| !value.is_empty()),
        }
    }
}

impl From<&str> for ClientId {
    fn from(value: &str) -> Self {
        Self::Single(value.to_owned())
    }
}

impl From<String> for ClientId {
    fn from(value: String) -> Self {
        Self::Single(value)
    }
}

impl From<Vec<String>> for ClientId {
    fn from(value: Vec<String>) -> Self {
        Self::Multiple(value)
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderOptions {
    pub client_id: Option<ClientId>,
    pub client_secret: Option<String>,
    pub scope: Vec<String>,
    pub disable_default_scope: bool,
    pub redirect_uri: Option<String>,
    pub authorization_endpoint: Option<String>,
    pub client_key: Option<String>,
    pub disable_id_token_sign_in: bool,
    pub disable_implicit_sign_up: bool,
    pub disable_sign_up: bool,
    pub prompt: Option<String>,
    pub response_mode: Option<String>,
    pub override_user_info_on_sign_in: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OAuth2Tokens {
    pub token_type: Option<String>,
    pub access_token: Option<String>,
    pub refresh_token: Option<String>,
    pub access_token_expires_at: Option<OffsetDateTime>,
    pub refresh_token_expires_at: Option<OffsetDateTime>,
    pub scopes: Vec<String>,
    pub id_token: Option<String>,
    pub raw: Value,
}

impl Default for OAuth2Tokens {
    fn default() -> Self {
        Self {
            token_type: None,
            access_token: None,
            refresh_token: None,
            access_token_expires_at: None,
            refresh_token_expires_at: None,
            scopes: Vec::new(),
            id_token: None,
            raw: Value::Null,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OAuth2UserInfo {
    pub id: String,
    pub name: Option<String>,
    pub email: Option<String>,
    pub image: Option<String>,
    pub email_verified: bool,
}

pub fn get_primary_client_id(client_id: &Option<ClientId>) -> Option<&str> {
    client_id.as_ref().and_then(ClientId::primary)
}

pub fn get_oauth2_tokens(data: Value) -> Result<OAuth2Tokens, OAuthError> {
    let object = data.as_object().ok_or_else(|| {
        OAuthError::InvalidResponse("token response must be a JSON object".to_owned())
    })?;
    let now = OffsetDateTime::now_utc();
    let expires_at = |key: &str| {
        object
            .get(key)
            .and_then(Value::as_i64)
            .map(|seconds| now + Duration::seconds(seconds))
    };

    Ok(OAuth2Tokens {
        token_type: string_field(object, "token_type"),
        access_token: string_field(object, "access_token"),
        refresh_token: string_field(object, "refresh_token"),
        access_token_expires_at: expires_at("expires_in"),
        refresh_token_expires_at: expires_at("refresh_token_expires_in"),
        scopes: scopes_field(object.get("scope")),
        id_token: string_field(object, "id_token"),
        raw: data,
    })
}

fn string_field(object: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
    object.get(key).and_then(Value::as_str).map(str::to_owned)
}

fn scopes_field(value: Option<&Value>) -> Vec<String> {
    match value {
        Some(Value::String(scope)) => scope.split_whitespace().map(str::to_owned).collect(),
        Some(Value::Array(scopes)) => scopes
            .iter()
            .filter_map(Value::as_str)
            .map(str::to_owned)
            .collect(),
        _ => Vec::new(),
    }
}