modio/
auth.rs

1//! Authentication Flow interface
2use std::collections::BTreeMap;
3use std::fmt;
4
5use crate::routing::Route;
6use crate::types::auth::AccessToken;
7use crate::types::{Message, Timestamp};
8use crate::Modio;
9use crate::Result;
10
11pub use crate::types::auth::{Link, Links, Terms};
12
13/// [mod.io](https://mod.io) credentials. API key with optional OAuth2 access token.
14#[derive(Clone, Eq, PartialEq)]
15pub struct Credentials {
16    pub api_key: String,
17    pub token: Option<Token>,
18}
19
20/// Access token and optional Unix timestamp of the date this token will expire.
21#[derive(Clone, Eq, PartialEq)]
22pub struct Token {
23    pub value: String,
24    pub expired_at: Option<Timestamp>,
25}
26
27impl fmt::Debug for Credentials {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        if self.token.is_some() {
30            f.write_str("Credentials(apikey+token)")
31        } else {
32            f.write_str("Credentials(apikey)")
33        }
34    }
35}
36
37impl Credentials {
38    pub fn new<S: Into<String>>(api_key: S) -> Credentials {
39        Credentials {
40            api_key: api_key.into(),
41            token: None,
42        }
43    }
44
45    pub fn with_token<S: Into<String>, T: Into<String>>(api_key: S, token: T) -> Credentials {
46        Credentials {
47            api_key: api_key.into(),
48            token: Some(Token {
49                value: token.into(),
50                expired_at: None,
51            }),
52        }
53    }
54}
55
56impl From<&str> for Credentials {
57    fn from(api_key: &str) -> Credentials {
58        Credentials::new(api_key)
59    }
60}
61
62impl From<(&str, &str)> for Credentials {
63    fn from((api_key, token): (&str, &str)) -> Credentials {
64        Credentials::with_token(api_key, token)
65    }
66}
67
68impl From<String> for Credentials {
69    fn from(api_key: String) -> Credentials {
70        Credentials::new(api_key)
71    }
72}
73
74impl From<(String, String)> for Credentials {
75    fn from((api_key, token): (String, String)) -> Credentials {
76        Credentials::with_token(api_key, token)
77    }
78}
79
80/// Authentication Flow interface to retrieve access tokens. See the [mod.io Authentication
81/// docs](https://docs.mod.io/restapiref/#email-exchange) for more information.
82///
83/// # Example
84/// ```no_run
85/// use std::io::{self, Write};
86///
87/// use modio::{Credentials, Modio, Result};
88///
89/// fn prompt(prompt: &str) -> io::Result<String> {
90///     print!("{}", prompt);
91///     io::stdout().flush()?;
92///     let mut buffer = String::new();
93///     io::stdin().read_line(&mut buffer)?;
94///     Ok(buffer.trim().to_string())
95/// }
96///
97/// #[tokio::main]
98/// async fn main() -> Result<()> {
99///     let modio = Modio::new(Credentials::new("api-key"))?;
100///
101///     let email = prompt("Enter email: ").expect("read email");
102///     modio.auth().request_code(&email).await?;
103///
104///     let code = prompt("Enter security code: ").expect("read code");
105///     let token = modio.auth().security_code(&code).await?;
106///
107///     // Consume the endpoint and create an endpoint with new credentials.
108///     let _modio = modio.with_credentials(token);
109///
110///     Ok(())
111/// }
112/// ```
113#[derive(Clone)]
114pub struct Auth {
115    modio: Modio,
116}
117
118impl Auth {
119    pub(crate) fn new(modio: Modio) -> Self {
120        Self { modio }
121    }
122
123    /// Get text and links for user agreement and consent prior to authentication. [required: apikey]
124    ///
125    /// See the [mod.io docs](https://docs.mod.io/restapiref/#terms) for more information.
126    pub async fn terms(self) -> Result<Terms> {
127        self.modio.request(Route::Terms).send().await
128    }
129
130    /// Request a security code be sent to the email of the user. [required: apikey]
131    pub async fn request_code(self, email: &str) -> Result<()> {
132        self.modio
133            .request(Route::OAuthEmailRequest)
134            .form(&[("email", email)])
135            .send::<Message>()
136            .await?;
137
138        Ok(())
139    }
140
141    /// Get the access token for a security code. [required: apikey]
142    pub async fn security_code(self, code: &str) -> Result<Credentials> {
143        let t = self
144            .modio
145            .request(Route::OAuthEmailExchange)
146            .form(&[("security_code", code)])
147            .send::<AccessToken>()
148            .await?;
149
150        let token = Token {
151            value: t.value,
152            expired_at: t.expired_at,
153        };
154        Ok(Credentials {
155            api_key: self.modio.inner.credentials.api_key.clone(),
156            token: Some(token),
157        })
158    }
159
160    /// Authenticate via external services ([Steam], [Switch], [Xbox], [Discord], [Oculus], [Google]).
161    ///
162    /// See the [mod.io docs](https://docs.mod.io/restapiref/#authentication-2) for more information.
163    ///
164    /// [Steam]: SteamOptions
165    /// [Oculus]: OculusOptions
166    /// [Switch]: SwitchOptions
167    /// [Xbox]: XboxOptions
168    /// [Discord]: DiscordOptions
169    /// [Google]: GoogleOptions
170    ///
171    /// # Examples
172    ///
173    /// ```no_run
174    /// # use modio::{Credentials, Modio, Result};
175    /// # #[tokio::main]
176    /// # async fn run() -> Result<()> {
177    /// #   let modio = modio::Modio::new("apikey")?;
178    /// use modio::auth::SteamOptions;
179    /// let opts = SteamOptions::new("ticket");
180    /// modio.auth().external(opts).await?;
181    /// #   Ok(())
182    /// # }
183    /// ```
184    pub async fn external<T>(self, auth_options: T) -> Result<Credentials>
185    where
186        T: Into<AuthOptions>,
187    {
188        let AuthOptions { route, params } = auth_options.into();
189
190        let t = self
191            .modio
192            .request(route)
193            .form(&params)
194            .send::<AccessToken>()
195            .await?;
196
197        let token = Token {
198            value: t.value,
199            expired_at: t.expired_at,
200        };
201        Ok(Credentials {
202            api_key: self.modio.inner.credentials.api_key.clone(),
203            token: Some(token),
204        })
205    }
206
207    /// Logout by revoking the current access token.
208    pub async fn logout(self) -> Result<()> {
209        self.modio
210            .request(Route::OAuthLogout)
211            .send::<Message>()
212            .await?;
213
214        Ok(())
215    }
216}
217
218/// Options for external authentication.
219pub struct AuthOptions {
220    route: Route,
221    params: BTreeMap<&'static str, String>,
222}
223
224// impl From<*Options> for AuthOptions {{{
225impl From<OculusOptions> for AuthOptions {
226    fn from(options: OculusOptions) -> AuthOptions {
227        AuthOptions {
228            route: Route::ExternalAuthMeta,
229            params: options.params,
230        }
231    }
232}
233
234impl From<SteamOptions> for AuthOptions {
235    fn from(options: SteamOptions) -> AuthOptions {
236        AuthOptions {
237            route: Route::ExternalAuthSteam,
238            params: options.params,
239        }
240    }
241}
242
243impl From<SwitchOptions> for AuthOptions {
244    fn from(options: SwitchOptions) -> AuthOptions {
245        AuthOptions {
246            route: Route::ExternalAuthSwitch,
247            params: options.params,
248        }
249    }
250}
251
252impl From<XboxOptions> for AuthOptions {
253    fn from(options: XboxOptions) -> AuthOptions {
254        AuthOptions {
255            route: Route::ExternalAuthXbox,
256            params: options.params,
257        }
258    }
259}
260
261impl From<DiscordOptions> for AuthOptions {
262    fn from(options: DiscordOptions) -> AuthOptions {
263        AuthOptions {
264            route: Route::ExternalAuthDiscord,
265            params: options.params,
266        }
267    }
268}
269
270impl From<GoogleOptions> for AuthOptions {
271    fn from(options: GoogleOptions) -> AuthOptions {
272        AuthOptions {
273            route: Route::ExternalAuthGoogle,
274            params: options.params,
275        }
276    }
277}
278// }}}
279
280/// Authentication options for an encrypted gog app ticket.
281///
282/// See the [mod.io docs](https://docs.mod.io/restapiref/#gog-galaxy) for more information.
283pub struct GalaxyOptions {
284    params: BTreeMap<&'static str, String>,
285}
286
287impl GalaxyOptions {
288    pub fn new<T>(ticket: T) -> Self
289    where
290        T: Into<String>,
291    {
292        let mut params = BTreeMap::new();
293        params.insert("appdata", ticket.into());
294        Self { params }
295    }
296
297    option!(email >> "email");
298    option!(
299        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
300        /// than the default value which is a common year.
301        expired_at u64 >> "date_expires"
302    );
303    option!(terms_agreed bool >> "terms_agreed");
304}
305
306/// Authentication options for an itch.io JWT token.
307///
308/// See the [mod.io docs](https://docs.mod.io/restapiref/#itch-io) for more information.
309pub struct ItchioOptions {
310    params: BTreeMap<&'static str, String>,
311}
312
313impl ItchioOptions {
314    pub fn new<T>(token: T) -> Self
315    where
316        T: Into<String>,
317    {
318        let mut params = BTreeMap::new();
319        params.insert("itchio_token", token.into());
320        Self { params }
321    }
322
323    option!(email >> "email");
324    option!(
325        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
326        /// than the default value which is a week.
327        expired_at u64 >> "date_expires"
328    );
329    option!(terms_agreed bool >> "terms_agreed");
330}
331
332/// Authentication options for an Oculus user.
333///
334/// See the [mod.io docs](https://docs.mod.io/restapiref/#meta-quest) for more information.
335pub struct OculusOptions {
336    params: BTreeMap<&'static str, String>,
337}
338
339impl OculusOptions {
340    pub fn new_for_quest<T>(nonce: T, user_id: u64, auth_token: T) -> Self
341    where
342        T: Into<String>,
343    {
344        OculusOptions::new("quest".to_owned(), nonce.into(), user_id, auth_token.into())
345    }
346
347    pub fn new_for_rift<T>(nonce: T, user_id: u64, auth_token: T) -> Self
348    where
349        T: Into<String>,
350    {
351        OculusOptions::new("rift".to_owned(), nonce.into(), user_id, auth_token.into())
352    }
353
354    fn new(device: String, nonce: String, user_id: u64, auth_token: String) -> Self {
355        let mut params = BTreeMap::new();
356        params.insert("device", device);
357        params.insert("nonce", nonce);
358        params.insert("user_id", user_id.to_string());
359        params.insert("auth_token", auth_token);
360        Self { params }
361    }
362
363    option!(email >> "email");
364    option!(
365        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
366        /// than the default value which is a common year.
367        expired_at u64 >> "date_expires"
368    );
369    option!(terms_agreed bool >> "terms_agreed");
370}
371
372/// Authentication options for an encrypted steam app ticket.
373///
374/// See the [mod.io docs](https://docs.mod.io/restapiref/#steam) for more information.
375pub struct SteamOptions {
376    params: BTreeMap<&'static str, String>,
377}
378
379impl SteamOptions {
380    pub fn new<T>(ticket: T) -> Self
381    where
382        T: Into<String>,
383    {
384        let mut params = BTreeMap::new();
385        params.insert("appdata", ticket.into());
386        Self { params }
387    }
388
389    option!(email >> "email");
390    option!(
391        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
392        /// than the default value which is a common year.
393        expired_at u64 >> "date_expires"
394    );
395    option!(terms_agreed bool >> "terms_agreed");
396}
397
398/// Authentication options for the NSA ID token.
399///
400/// See the [mod.io docs](https://docs.mod.io/restapiref/#nintendo-switch) for more information.
401pub struct SwitchOptions {
402    params: BTreeMap<&'static str, String>,
403}
404
405impl SwitchOptions {
406    pub fn new<T>(id_token: T) -> Self
407    where
408        T: Into<String>,
409    {
410        let mut params = BTreeMap::new();
411        params.insert("id_token", id_token.into());
412        Self { params }
413    }
414
415    option!(email >> "email");
416    option!(
417        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
418        /// than the default value which is a common year.
419        expired_at u64 >> "date_expires"
420    );
421    option!(terms_agreed bool >> "terms_agreed");
422}
423
424/// Authentication options for an Xbox Live token.
425///
426/// See the [mod.io docs](https://docs.mod.io/restapiref/#xbox-live) for more information.
427pub struct XboxOptions {
428    params: BTreeMap<&'static str, String>,
429}
430
431impl XboxOptions {
432    pub fn new<T>(token: T) -> Self
433    where
434        T: Into<String>,
435    {
436        let mut params = BTreeMap::new();
437        params.insert("xbox_token", token.into());
438        Self { params }
439    }
440
441    option!(email >> "email");
442    option!(
443        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
444        /// than the default value which is a common year.
445        expired_at u64 >> "date_expires"
446    );
447    option!(terms_agreed bool >> "terms_agreed");
448}
449
450/// Authentication options for an Discord token.
451///
452/// See the [mod.io docs](https://docs.mod.io/restapiref/#discord) for more information.
453pub struct DiscordOptions {
454    params: BTreeMap<&'static str, String>,
455}
456
457impl DiscordOptions {
458    pub fn new<T>(token: T) -> Self
459    where
460        T: Into<String>,
461    {
462        let mut params = BTreeMap::new();
463        params.insert("discord_token", token.into());
464        Self { params }
465    }
466
467    option!(email >> "email");
468    option!(
469        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
470        /// than the default value which is a week.
471        expired_at u64 >> "date_expires"
472    );
473    option!(terms_agreed bool >> "terms_agreed");
474}
475
476/// Authentication options for an Google token.
477///
478/// See the [mod.io docs](https://docs.mod.io/restapiref/#google) for more information.
479pub struct GoogleOptions {
480    params: BTreeMap<&'static str, String>,
481}
482
483impl GoogleOptions {
484    pub fn new<T>(token: T) -> Self
485    where
486        T: Into<String>,
487    {
488        let mut params = BTreeMap::new();
489        params.insert("id_token", token.into());
490        Self { params }
491    }
492
493    option!(email >> "email");
494    option!(
495        /// Unix timestamp of date in which the returned token will expire. Value cannot be higher
496        /// than the default value which is a week.
497        expired_at u64 >> "date_expires"
498    );
499    option!(terms_agreed bool >> "terms_agreed");
500}
501
502// vim: fdm=marker