xal 0.1.3

Xbox Authentication library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
//! Higher-level, bundled functionality for common tasks
use async_trait::async_trait;
use log::{debug, info, trace};
use oauth2::{
    EndUserVerificationUrl, StandardDeviceAuthorizationResponse, TokenResponse, UserCode,
    VerificationUriComplete,
};
use url::Url;

use crate::{
    response::{SisuAuthenticationResponse, WindowsLiveTokens},
    tokenstore::TokenStore,
    AccessTokenPrefix, Error, XalAuthenticator,
};

/// Argument passed into [`crate::flows::AuthPromptCallback`]
#[derive(Debug)]
pub enum AuthPromptData {
    /// User action request for authorization code / implict grant flow
    /// It requires the user to visit an URL and pass back the returned redirect URL
    RedirectUrl {
        /// Prompt message for the user
        prompt: String,
        /// URL to use for authentication
        url: EndUserVerificationUrl,
        /// Whether the caller expects a redirect URL with authorization data
        expect_url: bool,
    },

    /// User action request for device code flow
    /// It should return directly after showing the action prompt to the user
    DeviceCode {
        /// Prompt message for the user
        prompt: String,
        /// URL to use for authentication
        url: EndUserVerificationUrl,
        /// Code the user has to enter in the webform to authenticate
        code: UserCode,
        /// The complete URL with pre-filled UserCode
        full_verificiation_url: VerificationUriComplete,
        /// Whether the caller expects a redirect URL
        expect_url: bool,
    },
}

impl From<SisuAuthenticationResponse> for AuthPromptData {
    fn from(value: SisuAuthenticationResponse) -> Self {
        Self::RedirectUrl {
            prompt: format!(
                "!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0} (Query params: {1:?})\n
                \nThen enter the resulting redirected URL (might need to open DevTools in your browser before opening the link)",
                value.msa_oauth_redirect,
                value.msa_request_parameters,
            ),
            url: EndUserVerificationUrl::from_url(value.msa_oauth_redirect.clone()),
            expect_url: true,
        }
    }
}

impl From<StandardDeviceAuthorizationResponse> for AuthPromptData {
    fn from(value: StandardDeviceAuthorizationResponse) -> Self {
        let user_code = value.user_code().to_owned();
        let verification_uri = value.verification_uri().to_owned();
        let full_url = XalAuthenticator::get_device_code_verification_uri(value.user_code());

        Self::DeviceCode {
            prompt: format!(
                "!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0}\nUse code: {1}\n\nAlternatively, use this link: {2}",
                verification_uri.as_str(),
                user_code.secret(),
                full_url.secret(),
            ),
            url: verification_uri,
            code: user_code,
            full_verificiation_url: full_url,
            expect_url: false,
        }
    }
}

impl From<EndUserVerificationUrl> for AuthPromptData {
    fn from(value: EndUserVerificationUrl) -> Self {
        Self::RedirectUrl {
            prompt: format!(
                "!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0}\nNOTE: You might have to open DevTools when navigating the flow to catch redirect",
                value.as_str()
            ),
            url: value.to_owned(),
            expect_url: true,
        }
    }
}

impl AuthPromptData {
    /// Return user prompt string aka. instructions of which URL the user needs to visit to authenticate
    pub fn prompt(&self) -> String {
        match self {
            AuthPromptData::RedirectUrl { prompt, .. } => prompt.to_owned(),
            AuthPromptData::DeviceCode { prompt, .. } => prompt.to_owned(),
        }
    }

    /// Return whether the callback expects n URL as return value
    pub fn expect_url(&self) -> bool {
        match self {
            AuthPromptData::RedirectUrl { expect_url, .. } => *expect_url,
            AuthPromptData::DeviceCode { expect_url, .. } => *expect_url,
        }
    }

    /// Returns the authentication URL
    pub fn authentication_url(&self) -> Url {
        match self {
            AuthPromptData::RedirectUrl { url, .. } => Url::parse(url.as_str()).unwrap(),
            AuthPromptData::DeviceCode {
                full_verificiation_url,
                ..
            } => Url::parse(full_verificiation_url.secret()).unwrap(),
        }
    }
}

/// Sisu Auth callback trait
///
/// Used as an argument to [`crate::Flows::xbox_live_sisu_full_flow`]
///
///
/// # Examples
///
/// ```
/// # use std::io;
/// # use async_trait::async_trait;
/// # use xal::{XalAuthenticator, AuthPromptCallback, AuthPromptData};
/// # use xal::url::Url;
/// // Define callback handler for OAuth2 flow
/// struct CallbackHandler;
/// # fn do_interactive_oauth2(url: &str) -> String { String::new() }
///
/// #[async_trait]
/// impl AuthPromptCallback for CallbackHandler {
///     async fn call(
///         &self,
///         cb_data: AuthPromptData
///     ) -> Result<Option<Url>, Box<dyn std::error::Error>>
///     {
///         let prompt = cb_data.prompt();
///         let do_expect_url = cb_data.expect_url();
///         println!("{prompt}\n");
///         
///         let res = if do_expect_url {
///             // Read pasted URL from terminal
///             println!("Redirect URL> ");
///             let mut redirect_url = String::new();
///             let _ = io::stdin().read_line(&mut redirect_url)?;
///             Some(Url::parse(&redirect_url)?)
///         } else {
///             // Callback does not expect any user input, just return
///             None
///         };
///         
///         Ok(res)
///     }
/// }
/// ```
#[async_trait]
pub trait AuthPromptCallback {
    /// Callback function that is called when the Authentication flow requires the user to perform interactive authentication via a webpage.
    ///
    /// This function takes an argument of type [`crate::flows::AuthPromptData`], which provides the necessary data for the interactive
    /// authentication process.
    ///
    /// The function returns a [`Result`] that represents either a successfully completed interactive authentication or an error that
    /// occurred during the process.
    ///
    /// # Errors
    ///
    /// This function may return an error if the user fails to perform the interactive authentication process or if there is a problem with the underlying authentication process.
    async fn call(
        &self,
        cb_data: AuthPromptData,
    ) -> Result<Option<Url>, Box<dyn std::error::Error>>;
}

/// Implementation of a cli callback handler
///
/// # Examples
///
/// Using the [`CliCallbackHandler`] will prompt the user via commandline for an action.
/// e.g. Browsing to an authentication URL and pasting back the redirect URL incl. authorization data.
///
/// ```no_run
/// use xal::{XalAuthenticator, Flows, Error, CliCallbackHandler};
///
/// # async fn example() -> Result<(), Error> {
/// let mut authenticator = XalAuthenticator::default();
///
/// let token_store = Flows::xbox_live_sisu_full_flow(
///     &mut authenticator,
///     CliCallbackHandler,
/// )
/// .await?;
///
/// # Ok(())
/// # }
/// ```
pub struct CliCallbackHandler;

#[async_trait]
impl AuthPromptCallback for CliCallbackHandler {
    async fn call(
        &self,
        cb_data: AuthPromptData,
    ) -> Result<Option<Url>, Box<dyn std::error::Error>> {
        let prompt = cb_data.prompt();
        let do_expect_url = cb_data.expect_url();

        println!("{prompt}\n");

        let res = if do_expect_url {
            // Read pasted URL from terminal
            print!("Redirect URL> ");
            let mut redirect_url = String::new();
            let _ = std::io::stdin().read_line(&mut redirect_url)?;
            Some(Url::parse(&redirect_url)?)
        } else {
            // Callback does not expect any user input, just return
            None
        };

        Ok(res)
    }
}

/// Higher-level, bundled functionality for common authentication tasks
pub struct Flows;

impl Flows {
    /// Try to deserialize a JSON TokenStore from filepath and refresh the Windows Live tokens if needed.
    ///
    /// # Errors
    ///
    /// This function may return an error if the file cannot be read, fails to deserialize or the
    /// tokens cannot be refreshed.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use xal::{Error, Flows, TokenStore};
    ///
    /// # async fn demo_code() -> Result<(), Error> {
    /// // Refresh Windows Live tokens first
    /// let (mut authenticator, token_store) = Flows::try_refresh_live_tokens_from_file("tokens.json")
    ///     .await?;
    ///
    /// // Continue by requesting xbox live tokens
    /// let token_store = Flows::xbox_live_sisu_authorization_flow(
    ///     &mut authenticator,
    ///     token_store.live_token
    /// )
    /// .await?;
    ///
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Returns
    ///
    /// If successful, a tuple of [`crate::XalAuthenticator`] and [`crate::tokenstore::TokenStore`]
    /// is returned. TokenStore will contain the refreshed `live_tokens`.
    pub async fn try_refresh_live_tokens_from_file(
        filepath: &str,
    ) -> Result<(XalAuthenticator, TokenStore), Error> {
        let mut ts = TokenStore::load_from_file(filepath)?;
        let authenticator = Self::try_refresh_live_tokens_from_tokenstore(&mut ts).await?;
        Ok((authenticator, ts))
    }

    /// Try to read tokens from the token store and refresh the Windows Live tokens if needed.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use std::fs::File;
    /// use serde_json;
    /// use xal::{Flows, TokenStore};
    ///
    /// # async fn demo_code() -> Result<(), xal::Error> {
    /// let mut file = File::open("tokens.json")
    ///     .expect("Failed to open tokenfile");
    /// let mut ts: TokenStore = serde_json::from_reader(&mut file)
    ///     .expect("Failed to deserialize TokenStore");
    ///
    /// let authenticator = Flows::try_refresh_live_tokens_from_tokenstore(&mut ts)
    ///     .await
    ///     .expect("Failed refreshing Windows Live tokens");
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Errors
    ///
    /// This function may return an error if the token store cannot be read or the tokens cannot be refreshed.
    ///
    /// # Returns
    ///
    /// If successful, a tuple of [`crate::XalAuthenticator`] and [`crate::TokenStore`]
    /// is returned. TokenStore will contain the refreshed `live_tokens`.
    pub async fn try_refresh_live_tokens_from_tokenstore(
        ts: &mut TokenStore,
    ) -> Result<XalAuthenticator, Error> {
        let mut authenticator = Into::<XalAuthenticator>::into(ts.clone());

        info!("Refreshing windows live tokens");
        let refreshed_wl_tokens = authenticator
            .refresh_token(ts.live_token.refresh_token().unwrap())
            .await
            .expect("Failed to exchange refresh token for fresh WL tokens");

        debug!("Windows Live tokens: {:?}", refreshed_wl_tokens);
        ts.live_token = refreshed_wl_tokens.clone();

        Ok(authenticator)
    }

    /// Shorthand for Windows Live device code flow
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use xal::{XalAuthenticator, Flows, Error, AccessTokenPrefix, CliCallbackHandler};
    /// use xal::response::WindowsLiveTokens;
    ///
    /// # async fn async_sleep_fn(_: std::time::Duration) {}
    ///
    /// # async fn example() -> Result<(), Error> {
    /// let mut authenticator = XalAuthenticator::default();
    ///
    /// let token_store = Flows::ms_device_code_flow(
    ///     &mut authenticator,
    ///     CliCallbackHandler,
    ///     async_sleep_fn
    /// )
    /// .await?;
    ///
    /// // TokenStore will only contain live tokens
    /// assert!(token_store.user_token.is_none());
    /// assert!(token_store.title_token.is_none());
    /// assert!(token_store.device_token.is_none());
    /// assert!(token_store.authorization_token.is_none());
    /// # Ok(())
    /// # }
    /// ```
    pub async fn ms_device_code_flow<S, SF>(
        authenticator: &mut XalAuthenticator,
        cb: impl AuthPromptCallback,
        sleep_fn: S,
    ) -> Result<TokenStore, Error>
    where
        S: Fn(std::time::Duration) -> SF,
        SF: std::future::Future<Output = ()>,
    {
        trace!("Initiating device code flow");
        let device_code_flow = authenticator.initiate_device_code_auth().await?;
        debug!("Device code={:?}", device_code_flow);

        trace!("Reaching into callback to notify caller about device code url");
        cb.call(device_code_flow.clone().into())
            .await
            .map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?;

        trace!("Polling for device code");
        let live_tokens = authenticator
            .poll_device_code_auth(&device_code_flow, sleep_fn)
            .await?;

        let ts = TokenStore {
            app_params: authenticator.app_params(),
            client_params: authenticator.client_params(),
            sandbox_id: authenticator.sandbox_id(),
            live_token: live_tokens,
            user_token: None,
            title_token: None,
            device_token: None,
            authorization_token: None,
            updated: None,
        };

        Ok(ts)
    }

    /// Shorthand for Windows Live authorization flow
    /// - Depending on the argument `implicit` the
    ///   methods `implicit grant` or `authorization code` are chosen
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use xal::{XalAuthenticator, Flows, Error, AccessTokenPrefix, CliCallbackHandler};
    /// use xal::response::WindowsLiveTokens;
    ///
    /// # async fn example() -> Result<(), Error> {
    /// let do_implicit_flow = true;
    /// let mut authenticator = XalAuthenticator::default();
    ///
    /// let token_store = Flows::ms_authorization_flow(
    ///     &mut authenticator,
    ///     CliCallbackHandler,
    ///     do_implicit_flow,
    /// )
    /// .await?;
    ///
    /// // TokenStore will only contain live tokens
    /// assert!(token_store.user_token.is_none());
    /// assert!(token_store.title_token.is_none());
    /// assert!(token_store.device_token.is_none());
    /// assert!(token_store.authorization_token.is_none());
    /// # Ok(())
    /// # }
    /// ```
    pub async fn ms_authorization_flow(
        authenticator: &mut XalAuthenticator,
        cb: impl AuthPromptCallback,
        implicit: bool,
    ) -> Result<TokenStore, Error> {
        trace!("Starting implicit authorization flow");

        let (url, state) = authenticator.get_authorization_url(implicit)?;

        trace!("Reaching into callback to receive authentication redirect URL");
        let redirect_url = cb
            .call(url.into())
            .await
            .map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?
            .ok_or(Error::GeneralError(
                "Failed receiving redirect URL".to_string(),
            ))?;

        debug!("From callback: Redirect URL={:?}", redirect_url);

        let live_tokens = if implicit {
            trace!("Parsing (implicit grant) redirect URI");
            XalAuthenticator::parse_implicit_grant_url(&redirect_url, Some(&state))?
        } else {
            trace!("Parsing (authorization code) redirect URI");
            let authorization_code =
                XalAuthenticator::parse_authorization_code_response(&redirect_url, Some(&state))?;
            debug!("Authorization Code: {:?}", &authorization_code);

            trace!("Getting Windows Live tokens (exchange code)");
            authenticator
                .exchange_code_for_token(authorization_code, None)
                .await?
        };

        let ts = TokenStore {
            app_params: authenticator.app_params(),
            client_params: authenticator.client_params(),
            sandbox_id: authenticator.sandbox_id(),
            live_token: live_tokens,
            user_token: None,
            title_token: None,
            device_token: None,
            authorization_token: None,
            updated: None,
        };

        Ok(ts)
    }

    /// Shorthand for sisu authentication flow
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use xal::{XalAuthenticator, Flows, Error, CliCallbackHandler};
    ///
    /// # async fn example() -> Result<(), Error> {
    /// let mut authenticator = XalAuthenticator::default();
    ///
    /// let token_store = Flows::xbox_live_sisu_full_flow(
    ///     &mut authenticator,
    ///     CliCallbackHandler,
    /// )
    /// .await?;
    ///
    /// // TokenStore will contain user/title/device/xsts tokens
    /// assert!(token_store.user_token.is_some());
    /// assert!(token_store.title_token.is_some());
    /// assert!(token_store.device_token.is_some());
    /// assert!(token_store.authorization_token.is_some());
    /// # Ok(())
    /// # }
    /// ```
    pub async fn xbox_live_sisu_full_flow(
        authenticator: &mut XalAuthenticator,
        callback: impl AuthPromptCallback,
    ) -> Result<TokenStore, Error> {
        trace!("Getting device token");
        let device_token = authenticator.get_device_token().await?;
        debug!("Device token={:?}", device_token);
        let (code_challenge, code_verifier) = XalAuthenticator::generate_code_verifier();
        trace!("Generated Code verifier={:?}", code_verifier);
        trace!("Generated Code challenge={:?}", code_challenge);
        let state = XalAuthenticator::generate_random_state();
        trace!("Generated random state={:?}", state);

        trace!("Fetching SISU authentication URL and Session Id");
        let (auth_resp, session_id) = authenticator
            .sisu_authenticate(&device_token, &code_challenge, &state)
            .await?;
        debug!(
            "SISU Authenticate response={:?} Session Id={:?}",
            auth_resp, session_id
        );

        // Passing redirect URL to callback and expecting redirect url + authorization token back
        trace!("Reaching into callback to receive authentication redirect URL");
        let redirect_url = callback
            .call(auth_resp.into())
            .await
            .map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?
            .ok_or(Error::GeneralError(
                "Did not receive any Redirect URL from RedirectUrl callback".to_string(),
            ))?;

        debug!("From callback: Redirect URL={:?}", redirect_url);

        trace!("Parsing redirect URI");
        let authorization_code =
            XalAuthenticator::parse_authorization_code_response(&redirect_url, Some(&state))?;

        debug!("Authorization Code: {:?}", &authorization_code);
        trace!("Getting Windows Live tokens (exchange code)");
        let live_tokens = authenticator
            .exchange_code_for_token(authorization_code, Some(code_verifier))
            .await?;
        debug!("Windows live tokens={:?}", &live_tokens);

        trace!("Getting Sisu authorization response");
        let sisu_resp = authenticator
            .sisu_authorize(&live_tokens, &device_token, Some(session_id))
            .await?;
        debug!("Sisu authorizatione response={:?}", sisu_resp);

        let ts = TokenStore {
            app_params: authenticator.app_params(),
            client_params: authenticator.client_params(),
            sandbox_id: authenticator.sandbox_id(),
            live_token: live_tokens,
            device_token: Some(device_token),
            user_token: Some(sisu_resp.user_token),
            title_token: Some(sisu_resp.title_token),
            authorization_token: Some(sisu_resp.authorization_token),

            updated: None,
        };

        Ok(ts)
    }

    /// Implements the traditional Xbox Live authorization flow.
    ///
    /// The method serves as a shorthand for executing the Xbox Live authorization flow by exchanging
    /// [`crate::models::response::WindowsLiveTokens`] to ultimately acquire an authorized Xbox Live session.
    ///
    /// The authorization flow is designed to be highly modular, allowing for extensive customization
    /// based on the specific needs of your application.
    ///
    /// # Arguments
    ///
    /// - `xsts_relying_party` XSTS Relying Party URL (see #Notes)
    /// - `access_token_prefix` Whether AccessToken needs to be prefixed for the Xbox UserToken (XASU) Request (see #Notes).
    /// - `request_title_token` Whether to request a Title Token (see #Notes)
    ///
    /// # Errors
    ///
    /// This method may return an error if any of the intermediate token requests fail.
    /// For a more detailed explanation of the error, refer to the documentation of the
    /// [`crate::XalAuthenticator`] methods.
    ///
    /// # Returns
    ///
    /// This method returns a `Result` containing a tuple with two elements:
    ///
    /// - The updated `XalAuthenticator` instance, with an incremented [`crate::cvlib::CorrelationVector`]
    /// - A `TokenStore` struct, with all the tokens necessary exchanged during the authorization flow.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use xal::{XalAuthenticator, Flows, CliCallbackHandler, Error, AccessTokenPrefix};
    /// use xal::response::WindowsLiveTokens;
    ///
    /// # async fn example() -> Result<(), Error> {
    /// let mut authenticator = XalAuthenticator::default();
    ///
    /// let token_store = Flows::ms_device_code_flow(
    ///     &mut authenticator,
    ///     CliCallbackHandler,
    ///     tokio::time::sleep
    /// )
    /// .await?;
    ///
    /// // Execute the Xbox Live authorization flow..
    /// let token_store = Flows::xbox_live_authorization_traditional_flow(
    ///     &mut authenticator,
    ///     token_store.live_token,
    ///     "rp://api.minecraftservices.com/".to_string(),
    ///     AccessTokenPrefix::D,
    ///     true,
    /// )
    /// .await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// # Notes
    ///
    /// - Requesting a Title Token *standalone* aka. without sisu-flow only works for very few clients,
    ///   currently only "Minecraft" is known.
    /// - Depending on the client an AccessToken prefix is necessary to have the User Token (XASU) request succeed
    /// - Success of authorizing (device, user, ?title?) tokens for XSTS relies on the target relying party
    pub async fn xbox_live_authorization_traditional_flow(
        authenticator: &mut XalAuthenticator,
        live_tokens: WindowsLiveTokens,
        xsts_relying_party: String,
        access_token_prefix: AccessTokenPrefix,
        request_title_token: bool,
    ) -> Result<TokenStore, Error> {
        debug!("Windows live tokens={:?}", &live_tokens);
        trace!("Getting device token");
        let device_token = authenticator.get_device_token().await?;
        debug!("Device token={:?}", device_token);

        trace!("Getting user token");
        let user_token = authenticator
            .get_user_token(&live_tokens, access_token_prefix)
            .await?;

        debug!("User token={:?}", user_token);

        let maybe_title_token = if request_title_token {
            trace!("Getting title token");
            let title_token = authenticator
                .get_title_token(&live_tokens, &device_token)
                .await?;
            debug!("Title token={:?}", title_token);

            Some(title_token)
        } else {
            debug!("Skipping title token request..");
            None
        };

        trace!("Getting XSTS token");
        let authorization_token = authenticator
            .get_xsts_token(
                Some(&device_token),
                maybe_title_token.as_ref(),
                Some(&user_token),
                &xsts_relying_party,
            )
            .await?;
        debug!("XSTS token={:?}", authorization_token);

        let ts = TokenStore {
            app_params: authenticator.app_params(),
            client_params: authenticator.client_params(),
            sandbox_id: authenticator.sandbox_id(),
            live_token: live_tokens,
            device_token: Some(device_token),
            user_token: Some(user_token),
            title_token: maybe_title_token,
            authorization_token: Some(authorization_token),

            updated: None,
        };

        Ok(ts)
    }

    /// The authorization part of Sisu
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use xal::{XalAuthenticator, Flows, CliCallbackHandler, Error, AccessTokenPrefix};
    /// use xal::response::WindowsLiveTokens;
    ///
    /// # async fn example() -> Result<(), Error> {
    /// let mut authenticator = XalAuthenticator::default();
    ///
    /// let token_store = Flows::ms_device_code_flow(
    ///     &mut authenticator,
    ///     CliCallbackHandler,
    ///     tokio::time::sleep
    /// )
    /// .await?;
    ///
    /// let token_store = Flows::xbox_live_sisu_authorization_flow(
    ///     &mut authenticator,
    ///     token_store.live_token,
    /// )
    /// .await?;
    ///
    /// // TokenStore will contain user/title/device/xsts tokens
    /// assert!(token_store.user_token.is_some());
    /// assert!(token_store.title_token.is_some());
    /// assert!(token_store.device_token.is_some());
    /// assert!(token_store.authorization_token.is_some());
    /// # Ok(())
    /// # }
    /// ```
    pub async fn xbox_live_sisu_authorization_flow(
        authenticator: &mut XalAuthenticator,
        live_tokens: WindowsLiveTokens,
    ) -> Result<TokenStore, Error> {
        debug!("Windows live tokens={:?}", &live_tokens);
        trace!("Getting device token");
        let device_token = authenticator.get_device_token().await?;
        debug!("Device token={:?}", device_token);

        trace!("Getting user token");
        let resp = authenticator
            .sisu_authorize(&live_tokens, &device_token, None)
            .await?;
        debug!("Sisu authorization response");

        let ts = TokenStore {
            app_params: authenticator.app_params(),
            client_params: authenticator.client_params(),
            sandbox_id: authenticator.sandbox_id(),
            live_token: live_tokens,
            device_token: Some(device_token),
            user_token: Some(resp.user_token),
            title_token: Some(resp.title_token),
            authorization_token: Some(resp.authorization_token),

            updated: None,
        };

        Ok(ts)
    }
}