ring_client/client/authentication/
mod.rs1mod error;
2
3pub use error::AuthenticationError;
4
5use crate::helper::url::Url;
6use crate::{helper, Client};
7use chrono::{DateTime, Utc};
8use reqwest::StatusCode;
9use serde::Serialize;
10use serde_json::json;
11use std::fmt::Debug;
12use std::ops::Add;
13use std::sync::Arc;
14
15use crate::helper::OperatingSystem;
16
17#[derive(Debug, Serialize)]
18pub(crate) struct Tokens {
19 pub(crate) access_token: String,
20 pub(crate) expires_at: DateTime<Utc>,
21 pub(crate) refresh_token: String,
22}
23
24impl Tokens {
25 #[must_use]
26 pub const fn new(
27 access_token: String,
28 expires_at: DateTime<Utc>,
29 refresh_token: String,
30 ) -> Self {
31 Self {
32 access_token,
33 expires_at,
34 refresh_token,
35 }
36 }
37}
38
39impl<'de> serde::Deserialize<'de> for Tokens {
40 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
41 where
42 D: serde::Deserializer<'de>,
43 {
44 let value = serde_json::Value::deserialize(deserializer)?;
45
46 let access_token = value["access_token"]
47 .as_str()
48 .ok_or_else(|| serde::de::Error::custom("Invalid access token"))?
49 .to_string();
50 let refresh_token = value["refresh_token"]
51 .as_str()
52 .ok_or_else(|| serde::de::Error::custom("Invalid refresh token"))?
53 .to_string();
54 let expires_at = Utc::now().add(chrono::Duration::seconds(
55 value["expires_in"]
56 .as_i64()
57 .ok_or_else(|| serde::de::Error::custom("Invalid expires_in value"))?,
58 ));
59
60 Ok(Self::new(access_token, expires_at, refresh_token))
61 }
62}
63
64#[derive(Debug)]
65pub(crate) struct RingAuth {
66 client: reqwest::Client,
67 operating_system: OperatingSystem,
68}
69
70#[derive(Debug)]
72pub enum Credentials {
73 #[allow(missing_docs)]
78 User { username: String, password: String },
79
80 RefreshToken(String),
86}
87
88impl RingAuth {
89 #[must_use]
90 pub fn new(operating_system: OperatingSystem) -> Self {
91 Self {
92 client: reqwest::Client::new(),
93 operating_system,
94 }
95 }
96
97 pub(crate) async fn login(
98 &self,
99 username: &str,
100 password: &str,
101 system_id: &str,
102 ) -> Result<Tokens, AuthenticationError> {
103 let response = self
104 .client
105 .post(helper::url::get_base_url(&Url::Oauth))
106 .header("User-Agent", self.operating_system.get_user_agent())
107 .header("2fa-support", "true")
108 .header(
109 "hardware_id",
110 crate::helper::hardware::generate_hardware_id(system_id),
111 )
112 .json(&json!({
113 "client_id": self.operating_system.get_client_id(),
114 "scope": "client",
115 "grant_type": "password",
116 "password": password,
117 "username": username,
118 }))
119 .send()
120 .await?;
121
122 if response.status() == StatusCode::PRECONDITION_FAILED {
123 return Err(AuthenticationError::MfaCodeRequired);
124 }
125
126 if response.status() != StatusCode::OK {
127 log::error!("Failed to login with status code: {}", response.status());
128 return Err(AuthenticationError::InvalidCredentials);
129 }
130
131 Ok(response.json::<Tokens>().await?)
132 }
133
134 pub(crate) async fn respond_to_challenge(
135 &self,
136 username: &str,
137 password: &str,
138 system_id: &str,
139 code: &str,
140 ) -> Result<Tokens, AuthenticationError> {
141 Ok(self
142 .client
143 .post(helper::url::get_base_url(&Url::Oauth))
144 .header("User-Agent", self.operating_system.get_user_agent())
145 .header("2fa-support", "true")
146 .header("2fa-code", code)
147 .header(
148 "hardware_id",
149 crate::helper::hardware::generate_hardware_id(system_id),
150 )
151 .json(&json!({
152 "client_id": self.operating_system.get_client_id(),
153 "scope": "client",
154 "grant_type": "password",
155 "password": &password,
156 "username": &username,
157 }))
158 .send()
159 .await?
160 .json::<Tokens>()
161 .await?)
162 }
163
164 pub(crate) async fn refresh_tokens(
165 &self,
166 tokens: Arc<Tokens>,
167 ) -> Result<Tokens, AuthenticationError> {
168 Ok(self
169 .client
170 .post(helper::url::get_base_url(&Url::Oauth))
171 .header("User-Agent", self.operating_system.get_user_agent())
172 .header("2fa-support", "true")
173 .json(&json!({
174 "client_id": "ring_official_ios",
175 "grant_type": "refresh_token",
176 "scope": "client",
177 "refresh_token": tokens.refresh_token,
178 }))
179 .send()
180 .await?
181 .json::<Tokens>()
182 .await?)
183 }
184}
185
186impl Client {
187 pub(crate) async fn refresh_tokens_if_needed(
188 &self,
189 ) -> Result<Arc<Tokens>, AuthenticationError> {
190 let mut token_to_refresh = self
191 .tokens
192 .write()
193 .await
194 .take_if(|current_tokens| current_tokens.expires_at < Utc::now());
195
196 if let Some(current_tokens) = &token_to_refresh {
197 let replacement_tokens =
198 Arc::new(self.auth.refresh_tokens(Arc::clone(current_tokens)).await?);
199
200 token_to_refresh.replace(Arc::clone(&replacement_tokens));
201
202 log::info!(
203 "Tokens have been replaced successfully. New expiration time: {}",
204 replacement_tokens.expires_at,
205 );
206
207 return Ok(Arc::clone(&replacement_tokens));
208 }
209
210 self.tokens.read().await.as_ref().map_or_else(
211 || Err(AuthenticationError::InvalidCredentials),
212 |current_tokens| Ok(Arc::clone(current_tokens)),
213 )
214 }
215}