use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use time::{Duration, OffsetDateTime};
use super::error::OAuthError;
use super::types::ClientSecret;
const MAX_TOKEN_EXPIRY_SECONDS: i64 = 10 * 365 * 24 * 60 * 60;
#[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>,
#[serde(
default,
serialize_with = "serialize_client_secret",
deserialize_with = "deserialize_client_secret"
)]
pub client_secret: Option<ClientSecret>,
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,
}
impl ProviderOptions {
pub fn client_secret_str(&self) -> Option<&str> {
self.client_secret.as_ref().map(ClientSecret::expose_secret)
}
pub fn with_client_secret(mut self, secret: impl Into<String>) -> Result<Self, OAuthError> {
self.client_secret = Some(ClientSecret::new(secret)?);
Ok(self)
}
}
fn serialize_client_secret<S: Serializer>(
secret: &Option<ClientSecret>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match secret {
Some(secret) => serializer.serialize_some(secret.expose_secret()),
None => serializer.serialize_none(),
}
}
fn deserialize_client_secret<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<ClientSecret>, D::Error> {
Option::<String>::deserialize(deserializer)?
.map(|value| ClientSecret::new(value).map_err(serde::de::Error::custom))
.transpose()
}
#[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::InvalidTokenResponse("token response must be a JSON object".to_owned())
})?;
let now = OffsetDateTime::now_utc();
let access_token = optional_string_field(object, "access_token")?;
let refresh_token = optional_string_field(object, "refresh_token")?;
let id_token = optional_string_field(object, "id_token")?;
if access_token.is_none() && refresh_token.is_none() && id_token.is_none() {
return Err(OAuthError::InvalidTokenResponse(
"token response must include access_token, refresh_token, or id_token".to_owned(),
));
}
Ok(OAuth2Tokens {
token_type: optional_string_field(object, "token_type")?,
access_token,
refresh_token,
access_token_expires_at: expires_at(object, "expires_in", now)?,
refresh_token_expires_at: expires_at(object, "refresh_token_expires_in", now)?,
scopes: scopes_field(object.get("scope"))?,
id_token,
raw: data,
})
}
fn optional_string_field(
object: &serde_json::Map<String, Value>,
key: &'static str,
) -> Result<Option<String>, OAuthError> {
match object.get(key) {
Some(Value::String(value)) => Ok(Some(value.clone())),
Some(_) => Err(OAuthError::InvalidTokenResponse(format!(
"`{key}` must be a string"
))),
None => Ok(None),
}
}
fn expires_at(
object: &serde_json::Map<String, Value>,
key: &'static str,
now: OffsetDateTime,
) -> Result<Option<OffsetDateTime>, OAuthError> {
let Some(value) = object.get(key) else {
return Ok(None);
};
let seconds = value.as_i64().ok_or_else(|| {
OAuthError::InvalidTokenResponse(format!("`{key}` must be an integer number of seconds"))
})?;
if !(0..=MAX_TOKEN_EXPIRY_SECONDS).contains(&seconds) {
return Err(OAuthError::InvalidTokenResponse(format!(
"`{key}` must be between 0 and {MAX_TOKEN_EXPIRY_SECONDS} seconds"
)));
}
now.checked_add(Duration::seconds(seconds))
.ok_or_else(|| OAuthError::InvalidTokenResponse(format!("`{key}` is out of range")))
.map(Some)
}
fn scopes_field(value: Option<&Value>) -> Result<Vec<String>, OAuthError> {
match value {
Some(Value::String(scope)) => Ok(scope.split_whitespace().map(str::to_owned).collect()),
Some(Value::Array(scopes)) => scopes
.iter()
.map(|value| {
value.as_str().map(str::to_owned).ok_or_else(|| {
OAuthError::InvalidTokenResponse(
"`scope` array values must be strings".to_owned(),
)
})
})
.collect(),
Some(_) => Err(OAuthError::InvalidTokenResponse(
"`scope` must be a string or string array".to_owned(),
)),
None => Ok(Vec::new()),
}
}