ring_client/client/authentication/
mod.rs

1mod 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/// A set of credentials used to authenticate with the Ring API.
71#[derive(Debug)]
72pub enum Credentials {
73    /// A username and password.
74    ///
75    /// This method is subject to two-factor authentication (2FA) and may require a
76    /// verification code to be sent to the user's associated mobile number.
77    #[allow(missing_docs)]
78    User { username: String, password: String },
79
80    /// An existing refresh token for a user.
81    ///
82    /// This can be generated by logging in with a username and password, and then
83    /// using [`get_refresh_token`](Client::get_refresh_token) to retrieve it for later
84    /// use.
85    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}