mod error;
pub use error::AuthenticationError;
use crate::helper::url::Url;
use crate::{Client, helper};
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::Serialize;
use serde_json::json;
use std::fmt::Debug;
use std::ops::Add;
use std::sync::Arc;
use crate::helper::OperatingSystem;
#[derive(Debug, Serialize)]
pub(crate) struct Tokens {
pub(crate) access_token: String,
pub(crate) expires_at: DateTime<Utc>,
pub(crate) refresh_token: String,
}
impl Tokens {
#[must_use]
pub const fn new(
access_token: String,
expires_at: DateTime<Utc>,
refresh_token: String,
) -> Self {
Self {
access_token,
expires_at,
refresh_token,
}
}
}
impl<'de> serde::Deserialize<'de> for Tokens {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
let access_token = value["access_token"]
.as_str()
.ok_or_else(|| serde::de::Error::custom("Invalid access token"))?
.to_string();
let refresh_token = value["refresh_token"]
.as_str()
.ok_or_else(|| serde::de::Error::custom("Invalid refresh token"))?
.to_string();
let expires_at = Utc::now().add(chrono::Duration::seconds(
value["expires_in"]
.as_i64()
.ok_or_else(|| serde::de::Error::custom("Invalid expires_in value"))?,
));
Ok(Self::new(access_token, expires_at, refresh_token))
}
}
#[derive(Debug)]
pub(crate) struct RingAuth {
client: reqwest::Client,
operating_system: OperatingSystem,
}
#[derive(Debug)]
pub enum Credentials {
#[allow(missing_docs)]
User { username: String, password: String },
RefreshToken(String),
}
impl RingAuth {
#[must_use]
pub fn new(operating_system: OperatingSystem) -> Self {
Self {
client: reqwest::Client::new(),
operating_system,
}
}
pub(crate) async fn login(
&self,
username: &str,
password: &str,
system_id: &str,
) -> Result<Tokens, AuthenticationError> {
let response = self
.client
.post(helper::url::get_base_url(&Url::Oauth))
.header("User-Agent", self.operating_system.get_user_agent())
.header("2fa-support", "true")
.header(
"hardware_id",
crate::helper::hardware::generate_hardware_id(system_id),
)
.json(&json!({
"client_id": self.operating_system.get_client_id(),
"scope": "client",
"grant_type": "password",
"password": password,
"username": username,
}))
.send()
.await?;
if response.status() == StatusCode::PRECONDITION_FAILED {
return Err(AuthenticationError::MfaCodeRequired);
}
if response.status() != StatusCode::OK {
log::error!("Failed to login with status code: {}", response.status());
return Err(AuthenticationError::InvalidCredentials);
}
Ok(response.json::<Tokens>().await?)
}
pub(crate) async fn respond_to_challenge(
&self,
username: &str,
password: &str,
system_id: &str,
code: &str,
) -> Result<Tokens, AuthenticationError> {
Ok(self
.client
.post(helper::url::get_base_url(&Url::Oauth))
.header("User-Agent", self.operating_system.get_user_agent())
.header("2fa-support", "true")
.header("2fa-code", code)
.header(
"hardware_id",
crate::helper::hardware::generate_hardware_id(system_id),
)
.json(&json!({
"client_id": self.operating_system.get_client_id(),
"scope": "client",
"grant_type": "password",
"password": &password,
"username": &username,
}))
.send()
.await?
.json::<Tokens>()
.await?)
}
pub(crate) async fn refresh_tokens(
&self,
tokens: Arc<Tokens>,
) -> Result<Tokens, AuthenticationError> {
Ok(self
.client
.post(helper::url::get_base_url(&Url::Oauth))
.header("User-Agent", self.operating_system.get_user_agent())
.header("2fa-support", "true")
.json(&json!({
"client_id": "ring_official_ios",
"grant_type": "refresh_token",
"scope": "client",
"refresh_token": tokens.refresh_token,
}))
.send()
.await?
.json::<Tokens>()
.await?)
}
}
impl Client {
pub(crate) async fn refresh_tokens_if_needed(
&self,
) -> Result<Arc<Tokens>, AuthenticationError> {
let mut token_to_refresh = self
.tokens
.write()
.await
.take_if(|current_tokens| current_tokens.expires_at < Utc::now());
if let Some(current_tokens) = &token_to_refresh {
let replacement_tokens =
Arc::new(self.auth.refresh_tokens(Arc::clone(current_tokens)).await?);
token_to_refresh.replace(Arc::clone(&replacement_tokens));
log::info!(
"Tokens have been replaced successfully. New expiration time: {}",
replacement_tokens.expires_at,
);
return Ok(Arc::clone(&replacement_tokens));
}
self.tokens.read().await.as_ref().map_or_else(
|| Err(AuthenticationError::InvalidCredentials),
|current_tokens| Ok(Arc::clone(current_tokens)),
)
}
}