nadeo_api/auth/
mod.rs

1use crate::auth::token::access_token::AccessToken;
2use crate::auth::token::refresh_token::RefreshToken;
3use crate::client::{
4    EXPIRATION_TIME_BUFFER, NADEO_AUTH_URL, NADEO_REFRESH_URL, NADEO_SERVER_AUTH_URL,
5    UBISOFT_APP_ID,
6};
7use crate::request::metadata::MetaData;
8use crate::{Error, NadeoRequest, Result};
9use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
10use base64::Engine;
11use reqwest::header::{HeaderMap, HeaderValue};
12use reqwest::{Client, Response};
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15use std::str::FromStr;
16
17pub mod o_auth;
18pub mod token;
19
20const UBISOFT_AUTH_URL: &str = "https://public-ubiservices.ubi.com/v3/profiles/sessions";
21
22/// Defines authentication credentials used for the Nadeo API.
23#[derive(strum::Display, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
24pub enum AuthType {
25    #[strum(to_string = "NadeoServices")]
26    NadeoServices,
27    #[strum(to_string = "NadeoLiveServices")]
28    NadeoLiveServices,
29    OAuth,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub(crate) struct AuthInfo {
34    pub service: AuthType,
35    pub access_token: AccessToken,
36    pub refresh_token: RefreshToken,
37}
38
39impl AuthInfo {
40    pub(crate) async fn new(
41        service: AuthType,
42        ticket: &str,
43        meta_data: &MetaData,
44        client: &Client,
45    ) -> Result<Self> {
46        let mut headers = HeaderMap::new();
47        headers.insert("Content-Type", "application/json".parse().unwrap());
48
49        let auth_token = format!("ubi_v1 t={}", ticket);
50        headers.insert("Authorization", auth_token.parse().unwrap());
51        headers.insert("User-Agent", meta_data.user_agent.parse().unwrap());
52
53        let body = json!(
54            {
55                "audience": service.to_string()
56            }
57        );
58
59        // get nadeo auth token
60        let res = client
61            .post(NADEO_AUTH_URL)
62            .headers(headers)
63            .json(&body)
64            .send()
65            .await?
66            .error_for_status()?;
67
68        let json = res.json::<Value>().await?;
69
70        let access_token = AccessToken::from_str(json["accessToken"].as_str().unwrap())?;
71        let refresh_token = RefreshToken::from_str(json["refreshToken"].as_str().unwrap())?;
72
73        Ok(Self {
74            service,
75            access_token,
76            refresh_token,
77        })
78    }
79
80    /// Create with a server account
81    pub(crate) async fn new_server(
82        service: AuthType,
83        meta_data: &MetaData,
84        username: &str,
85        password: &str,
86        client: &Client,
87    ) -> Result<Self> {
88        let mut headers = HeaderMap::new();
89        headers.insert("Content-Type", "application/json".parse().unwrap());
90
91        let auth_token = format!("Basic {}", encode_auth(username, password));
92        headers.insert("Authorization", auth_token.parse().unwrap());
93        headers.insert("User-Agent", meta_data.user_agent.parse().unwrap());
94
95        let body = json!(
96            {
97                "audience": service.to_string()
98            }
99        );
100
101        // get nadeo auth token
102        let res = client
103            .post(NADEO_SERVER_AUTH_URL)
104            .headers(headers)
105            .json(&body)
106            .send()
107            .await?
108            .error_for_status()?;
109
110        let json = res.json::<Value>().await?;
111
112        let access_token = AccessToken::from_str(json["accessToken"].as_str().unwrap())?;
113        let refresh_token = RefreshToken::from_str(json["refreshToken"].as_str().unwrap())?;
114
115        Ok(Self {
116            service,
117            access_token,
118            refresh_token,
119        })
120    }
121
122    /// Forces a refresh request with the Nadeo API. [`refresh`] should be preferred over `force_refresh` in most cases.
123    ///
124    /// [`refresh`]: AuthInfo::refresh
125    pub(crate) async fn force_refresh(
126        &mut self,
127        meta_data: &MetaData,
128        client: &Client,
129    ) -> Result<()> {
130        let mut headers = HeaderMap::new();
131
132        // format refresh token
133        let auth_token = format!("nadeo_v1 t={}", self.refresh_token.encode());
134        headers.insert("Authorization", auth_token.parse().unwrap());
135        headers.insert("Content-Type", "application/json".parse().unwrap());
136        headers.insert("User-Agent", meta_data.user_agent.parse().unwrap());
137
138        let body = json!(
139            {
140                "audience": self.service.to_string()
141            }
142        );
143
144        let res = client
145            .post(NADEO_REFRESH_URL)
146            .headers(headers)
147            .json(&body)
148            .send()
149            .await
150            .map_err(Error::from)?;
151
152        let json = res.json::<Value>().await.map_err(Error::from)?;
153
154        let access_token = AccessToken::from_str(json["accessToken"].as_str().unwrap())?;
155        let refresh_token = RefreshToken::from_str(json["refreshToken"].as_str().unwrap())?;
156
157        self.access_token = access_token;
158        self.refresh_token = refresh_token;
159
160        Ok(())
161    }
162
163    /// Checks whether the token is expired. If it is [`force_refresh`] is called.
164    /// If the refresh was successful `Ok(true)` is returned but if it fails `Err(Error)` is returned.
165    /// If the token is not expired `Ok(false)` is returned and a token refresh is not attempted.
166    ///
167    /// # Errors
168    ///
169    /// Returns an [`Error`] if the token is expired and the refresh request fails.
170    ///
171    /// [`Error`]: Error
172    /// [`force_refresh`]: AuthInfo::force_refresh
173    pub(crate) async fn refresh(&mut self, meta_data: &MetaData, client: &Client) -> Result<bool> {
174        if !self.expires_in() < EXPIRATION_TIME_BUFFER {
175            return Ok(false);
176        }
177
178        self.force_refresh(meta_data, client).await.map(|_| true)
179    }
180
181    /// Returns the amount of **seconds** until the token expires.
182    pub(crate) fn expires_in(&self) -> i64 {
183        self.access_token.expires_in()
184    }
185
186    /// Executes a [`NadeoRequest`].
187    ///
188    /// # Panics
189    ///
190    /// Panics if the service of the [`AuthInfo`] and the [`NadeoRequest`] are not the same.
191    pub(crate) async fn execute(
192        &mut self,
193        request: NadeoRequest,
194        meta_data: &MetaData,
195        client: &Client,
196    ) -> Result<Response> {
197        assert_eq!(self.service, request.auth_type);
198
199        self.refresh(meta_data, client).await?;
200        let token = format!("nadeo_v1 t={}", self.access_token.encode());
201
202        let api_request = client.request(request.method, request.url);
203
204        let mut res = api_request
205            .header("Authorization", token.parse::<HeaderValue>().unwrap())
206            .header(
207                "User-Agent",
208                meta_data.user_agent.parse::<HeaderValue>().unwrap(),
209            )
210            .headers(request.headers);
211        if let Some(json) = request.body {
212            res = res.body(json);
213        }
214
215        let res = res.send().await?.error_for_status()?;
216
217        Ok(res)
218    }
219}
220
221fn encode_auth(username: &str, password: &str) -> String {
222    let auth = format!("{}:{}", username, password);
223    let auth = auth.as_bytes();
224
225    let mut b64 = String::new();
226    BASE64_STANDARD.encode_string(auth, &mut b64);
227    b64
228}
229
230pub(crate) async fn get_ubi_auth_ticket(
231    email: &str,
232    password: &str,
233    meta_data: &MetaData,
234    client: &Client,
235) -> Result<String> {
236    let mut headers = HeaderMap::new();
237
238    headers.insert("Content-Type", "application/json".parse().unwrap());
239    headers.insert("Ubi-AppId", UBISOFT_APP_ID.parse().unwrap());
240    headers.insert("User-Agent", meta_data.user_agent.parse().unwrap());
241
242    let ubi_auth_token = format!("Basic {}", encode_auth(email, password));
243    headers.insert("Authorization", ubi_auth_token.parse().unwrap());
244
245    // get ubisoft ticket
246    let res = client
247        .post(UBISOFT_AUTH_URL)
248        .headers(headers)
249        .send()
250        .await?
251        .error_for_status()?;
252
253    let json = res.json::<Value>().await?;
254    let ticket = json["ticket"].as_str().unwrap().to_string();
255
256    Ok(ticket)
257}