From deda4d3196a6732e1bc19af3c63f5b5ceda45c12 Mon Sep 17 00:00:00 2001 From: tuxuser <462620+tuxuser@users.noreply.github.com> Date: Tue, 19 Dec 2023 01:07:16 +0100 Subject: [PATCH] Extend documentation, fix code examples --- examples/Cargo.toml | 10 +- examples/src/bin/auth_azure.rs | 28 ++-- examples/src/bin/auth_cli.rs | 9 +- examples/src/bin/auth_minecraft.rs | 67 +++++++++ examples/src/bin/auth_webview.rs | 8 +- examples/src/lib.rs | 40 +++--- src/authenticator.rs | 168 +++++++++++----------- src/flows.rs | 214 +++++++++++++++++++++++++---- src/models.rs | 28 ++-- src/request_signer.rs | 166 +++++++++++++++------- 10 files changed, 533 insertions(+), 205 deletions(-) create mode 100644 examples/src/bin/auth_minecraft.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f334473..971a640 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -14,23 +14,21 @@ env_logger = "0.10.1" log = "0.4.20" clap = { version = "4.4.8", features = ["derive"] } chrono = "0.4.31" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +reqwest = { version = "0.11", features = ["json"] } # Optional dependencies -tokio = { version = "1", features = ["full"], optional = true } wry = { version = "0.34.2", optional = true } [features] -webview = ["dep:wry"] -tokio = ["dep:tokio"] +webview = ["wry"] [[bin]] name = "auth_cli" -required-features = ["tokio"] [[bin]] name = "auth_azure" -required-features = ["tokio"] [[bin]] name = "auth_webview" -required-features = ["webview","tokio"] +required-features = ["webview"] diff --git a/examples/src/bin/auth_azure.rs b/examples/src/bin/auth_azure.rs index 16bf3a1..aaae60a 100644 --- a/examples/src/bin/auth_azure.rs +++ b/examples/src/bin/auth_azure.rs @@ -1,11 +1,16 @@ use std::str::from_utf8; use async_trait::async_trait; -use tokio::{net::TcpListener, io::{AsyncReadExt, AsyncWriteExt}}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; use xal::{ + client_params::CLIENT_ANDROID, flows::{AuthPromptCallback, AuthPromptData}, + oauth2::{RedirectUrl, Scope}, url::Url, - Error, client_params::CLIENT_ANDROID, Constants, XalAppParameters, oauth2::{Scope, RedirectUrl, ResourceOwnerUsername, ResourceOwnerPassword}, XalAuthenticator, + Error, XalAppParameters, }; use xal_examples::auth_main; @@ -40,10 +45,13 @@ impl AuthPromptCallback for HttpCallbackHandler { let http_req = from_utf8(&buf)?; println!("HTTP REQ: {http_req}"); - let path = http_req.split(" ").nth(1).unwrap(); + let path = http_req.split(' ').nth(1).unwrap(); println!("Path: {path}"); - Ok(Some(Url::parse(&format!("{}{}", self.redirect_url_base, path))?)) + Ok(Some(Url::parse(&format!( + "{}{}", + self.redirect_url_base, path + ))?)) } } @@ -54,7 +62,8 @@ async fn main() -> Result<(), Error> { app_id: "388ea51c-0b25-4029-aae2-17df49d23905".into(), title_id: None, auth_scopes: vec![ - Scope::new("Xboxlive.signin".into()), Scope::new("Xboxlive.offline_access".into()) + Scope::new("Xboxlive.signin".into()), + Scope::new("Xboxlive.offline_access".into()), ], redirect_uri: Some( RedirectUrl::new("http://localhost:8080/auth/callback".into()).unwrap(), @@ -62,11 +71,14 @@ async fn main() -> Result<(), Error> { }, CLIENT_ANDROID(), "RETAIL".into(), - Constants::RELYING_PARTY_XBOXLIVE.into(), xal::AccessTokenPrefix::D, HttpCallbackHandler { bind_host: "127.0.0.1:8080".into(), redirect_url_base: "http://localhost:8080".into(), - } - ).await + }, + ) + .await + .ok(); + + Ok(()) } diff --git a/examples/src/bin/auth_cli.rs b/examples/src/bin/auth_cli.rs index 378752f..c5d4387 100644 --- a/examples/src/bin/auth_cli.rs +++ b/examples/src/bin/auth_cli.rs @@ -3,8 +3,9 @@ use xal_examples::auth_main_default; #[tokio::main] async fn main() -> Result<(), Error> { - auth_main_default( - AccessTokenPrefix::None, - flows::CliCallbackHandler - ).await + auth_main_default(AccessTokenPrefix::None, flows::CliCallbackHandler) + .await + .ok(); + + Ok(()) } diff --git a/examples/src/bin/auth_minecraft.rs b/examples/src/bin/auth_minecraft.rs new file mode 100644 index 0000000..afcd57a --- /dev/null +++ b/examples/src/bin/auth_minecraft.rs @@ -0,0 +1,67 @@ +use serde_json::json; +use xal::{ + extensions::JsonExDeserializeMiddleware, flows, oauth2::TokenResponse, AccessTokenPrefix, + Error, XalAuthenticator, +}; +use xal_examples::auth_main; + +#[tokio::main] +async fn main() -> Result<(), Error> { + let app_params = xal::app_params::MC_BEDROCK_SWITCH(); + let client_params = xal::client_params::CLIENT_NINTENDO(); + + let ts = auth_main( + app_params, + client_params, + "RETAIL".into(), + AccessTokenPrefix::None, + flows::CliCallbackHandler, + ) + .await?; + + let mut authenticator = XalAuthenticator::from(ts.clone()); + let xsts_mc_services = authenticator + .get_xsts_token( + ts.device_token.as_ref(), + ts.title_token.as_ref(), + ts.user_token.as_ref(), + "rp://api.minecraftservices.com/", + ) + .await?; + + let identity_token = xsts_mc_services.authorization_header_value(); + println!("identityToken: {identity_token}"); + + /* Minecraft stuff */ + // Fetch minecraft token + let mc_token = reqwest::Client::new() + .post("https://api.minecraftservices.com/authentication/login_with_xbox") + .json(&json!({"identityToken": identity_token})) + .send() + .await? + .json_ex::() + .await?; + println!("MC: {mc_token:?}"); + + // Get minecraft entitlements + let entitlements = reqwest::Client::new() + .get("https://api.minecraftservices.com/entitlements/mcstore") + .bearer_auth(mc_token.access_token().secret()) + .send() + .await? + .text() + .await?; + println!("Entitlements: {entitlements}"); + + // Get minecraft profile + let profile = reqwest::Client::new() + .get("https://api.minecraftservices.com/minecraft/profile") + .bearer_auth(mc_token.access_token().secret()) + .send() + .await? + .text() + .await?; + println!("Profile: {profile}"); + + Ok(()) +} diff --git a/examples/src/bin/auth_webview.rs b/examples/src/bin/auth_webview.rs index ffdfeb0..e82ea45 100644 --- a/examples/src/bin/auth_webview.rs +++ b/examples/src/bin/auth_webview.rs @@ -16,7 +16,7 @@ use wry::{ use xal::{ flows::{AuthPromptCallback, AuthPromptData}, url::Url, - Error, XalAuthenticator, AccessTokenPrefix, + AccessTokenPrefix, Error, XalAuthenticator, }; use xal_examples::auth_main_default; @@ -111,5 +111,9 @@ async fn main() -> Result<(), Error> { .to_owned(), }; - auth_main_default(AccessTokenPrefix::None, callback_handler).await + auth_main_default(AccessTokenPrefix::None, callback_handler) + .await + .ok(); + + Ok(()) } diff --git a/examples/src/lib.rs b/examples/src/lib.rs index 612c2b9..31374a8 100644 --- a/examples/src/lib.rs +++ b/examples/src/lib.rs @@ -1,7 +1,10 @@ use clap::{Parser, ValueEnum}; use env_logger::Env; use log::info; -use xal::{flows, Error, XalAppParameters, XalAuthenticator, XalClientParameters, AccessTokenPrefix, Constants}; +use xal::{ + flows, tokenstore::TokenStore, AccessTokenPrefix, Constants, Error, XalAppParameters, + XalAuthenticator, XalClientParameters, +}; /// Common cli arguments #[derive(Parser, Debug)] @@ -54,13 +57,12 @@ pub enum AuthFlow { pub async fn auth_main_default( access_token_prefix: AccessTokenPrefix, - auth_cb: impl flows::AuthPromptCallback -) -> Result<(), Error> { + auth_cb: impl flows::AuthPromptCallback, +) -> Result { auth_main( XalAppParameters::default(), XalClientParameters::default(), "RETAIL".to_owned(), - Constants::RELYING_PARTY_XBOXLIVE.into(), access_token_prefix, auth_cb, ) @@ -72,31 +74,28 @@ pub async fn auth_main( app_params: XalAppParameters, client_params: XalClientParameters, sandbox_id: String, - xsts_relying_party: String, access_token_prefix: AccessTokenPrefix, auth_cb: impl flows::AuthPromptCallback, -) -> Result<(), Error> { +) -> Result { let args = handle_args(); - let mut ts = match flows::try_refresh_tokens_from_file(&args.token_filepath).await { + let mut ts = match flows::try_refresh_live_tokens_from_file(&args.token_filepath).await { Ok((mut authenticator, ts)) => { info!("Tokens refreshed succesfully, proceeding with Xbox Live Authorization"); match args.flow { AuthFlow::Sisu => { info!("Authorize and gather rest of xbox live tokens via sisu"); - flows::xbox_live_sisu_authorization_flow( - &mut authenticator, ts.live_token - ) - .await? - }, + flows::xbox_live_sisu_authorization_flow(&mut authenticator, ts.live_token) + .await? + } _ => { info!("Authorize Xbox Live the traditional way, via individual requests"); flows::xbox_live_authorization_traditional_flow( &mut authenticator, ts.live_token, - xsts_relying_party, + Constants::RELYING_PARTY_XBOXLIVE.into(), access_token_prefix, - false + false, ) .await? } @@ -108,9 +107,12 @@ pub async fn auth_main( info!("Authentication via flow={:?}", args.flow); let ts = match args.flow { - AuthFlow::Sisu => flows::xbox_live_sisu_full_flow(&mut authenticator, auth_cb).await?, + AuthFlow::Sisu => { + flows::xbox_live_sisu_full_flow(&mut authenticator, auth_cb).await? + } AuthFlow::DeviceCode => { - flows::ms_device_code_flow(&mut authenticator, auth_cb, tokio::time::sleep).await? + flows::ms_device_code_flow(&mut authenticator, auth_cb, tokio::time::sleep) + .await? } AuthFlow::Implicit => { flows::ms_authorization_flow(&mut authenticator, auth_cb, true).await? @@ -129,12 +131,12 @@ pub async fn auth_main( flows::xbox_live_authorization_traditional_flow( &mut authenticator, ts.live_token, - xsts_relying_party, + Constants::RELYING_PARTY_XBOXLIVE.into(), access_token_prefix, false, ) .await? - }, + } } } }; @@ -142,5 +144,5 @@ pub async fn auth_main( ts.update_timestamp(); ts.save_to_file(&args.token_filepath)?; - Ok(()) + Ok(ts) } diff --git a/src/authenticator.rs b/src/authenticator.rs index 105fc71..99dd6d3 100644 --- a/src/authenticator.rs +++ b/src/authenticator.rs @@ -1,9 +1,9 @@ //! Authentication functionality. -use crate::{RequestSigner, AccessTokenPrefix}; use crate::extensions::{ CorrelationVectorReqwestBuilder, JsonExDeserializeMiddleware, LoggingReqwestRequestHandler, LoggingReqwestResponseHandler, SigningReqwestBuilder, }; +use crate::{AccessTokenPrefix, RequestSigner}; use crate::request::{ XADProperties, XASTProperties, XASUProperties, XSTSProperties, XTokenRequest, @@ -140,7 +140,7 @@ impl XalAuthenticator { /// ``` /// use xal::XalAuthenticator; /// use xal::oauth2::UserCode; - /// + /// /// let user_code = UserCode::new("abc123".to_string()); /// let verification_uri = XalAuthenticator::get_device_code_verification_uri(&user_code); /// println!("{:?}", verification_uri); @@ -178,7 +178,7 @@ impl XalAuthenticator { .query_pairs() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(); - + if let Some(state) = expected_state { if let Some(state_resp) = query_map.get("state") { if state.secret() != state_resp { @@ -194,24 +194,23 @@ impl XalAuthenticator { )); } } - + if let Some(error) = query_map.get("error") { - let error_resp: StandardErrorResponse = serde_json::from_value( - serde_json::json!({ + let error_resp: StandardErrorResponse = + serde_json::from_value(serde_json::json!({ "error": error, "error_description": query_map.get("error_description").map(|x| x.to_string()), "error_uri": query_map.get("error_uri").map(|x| x.to_string()), - }), - ) - .map_err(|e|Error::JsonError(e))?; - + })) + .map_err(Error::JsonError)?; + return Err(Error::OAuthExecutionError( oauth2::RequestTokenError::ServerResponse(error_resp), )); } else if let Some(code) = query_map.get("code") { return Ok(AuthorizationCode::new(code.to_owned())); } - + Err(Error::GeneralError( "Response neither had 'code' nor 'error' field".into(), )) @@ -237,7 +236,7 @@ impl XalAuthenticator { /// use xal::XalAuthenticator; /// use xal::oauth2::CsrfToken; /// use xal::url::Url; - /// + /// /// let url = Url::parse("https://example.com/callback#access_token=token123&token_type=Bearer&expires_in=3600&state=123abc") /// .unwrap(); /// let state = "123abc".to_string(); @@ -252,10 +251,10 @@ impl XalAuthenticator { let fragment = url .fragment() .ok_or(Error::InvalidRedirectUrl("No fragment found".to_string()))?; - + let mut kv_pairs: HashMap = HashMap::new(); let mut state_resp = None; - + for (k, v) in form_urlencoded::parse(fragment.as_bytes()) { match k.as_ref() { "expires_in" => { @@ -270,7 +269,7 @@ impl XalAuthenticator { } } } - + if let Some(state) = expected_state { if let Some(s) = state_resp { if state.secret() != &s.to_string() { @@ -283,7 +282,7 @@ impl XalAuthenticator { return Err(Error::InvalidRedirectUrl("No state found".to_string())); } } - + Ok(serde_json::from_value(json!(kv_pairs))?) } } @@ -371,13 +370,13 @@ impl XalAuthenticator { /// /// Defining a `redirect_url` in [`crate::XalAppParameters`] is mandatory /// for this authentication flow - /// + /// /// # Examples - /// + /// /// ``` /// use xal::{XalAuthenticator, XalAppParameters, client_params}; /// use xal::oauth2::{RedirectUrl, Scope}; - /// + /// /// # async fn demo_code() { /// let mut authenticator = XalAuthenticator::new( /// XalAppParameters { @@ -392,10 +391,10 @@ impl XalAuthenticator { /// client_params::CLIENT_ANDROID(), /// "RETAIL".into() /// ); - /// + /// /// let (url, state) = authenticator.get_authorization_url(false) /// .unwrap(); - /// + /// /// assert!(url.as_str().starts_with("https://login.live.com/oauth20_desktop.srf")); /// # } /// ``` @@ -422,10 +421,10 @@ impl XalAuthenticator { } /// Initiates the Device Code Authentication Flow. - /// + /// /// After presenting the returned [`crate::oauth2:: EndUserVerificationUrl`] and [`crate::oauth2::UserCode`] /// to the user, call `poll_device_code_auth`. - /// + /// /// You can transform the returned value into [`crate::oauth2::VerificationUriComplete`] by calling `get_device_code_verification_uri`. pub async fn initiate_device_code_auth( &mut self, @@ -441,11 +440,11 @@ impl XalAuthenticator { } /// Poll for device code. - /// + /// /// To be called after presenting the result of `start_device_code_auth` to the user. - /// + /// /// # Arguments - /// + /// /// - `sleep_fn` is the impl of an async sleep function pub async fn poll_device_code_auth( &mut self, @@ -464,16 +463,16 @@ impl XalAuthenticator { } /// Exchange OAuth2 Authorization Token for Windows Live Access Token. - /// + /// /// This method utilizes the PKCE extension to securely obtain an access token from the Microsoft Identity Platform. - /// + /// /// # Arguments - /// + /// /// * `authorization_code` - The authorization code received from the user authentication step. /// * `code_verifier` - The code verifier that was generated earlier in the PKCE process. This parameter is optional. - /// + /// /// # Examples - /// + /// /// ``` /// use xal::XalAuthenticator; /// use xal::oauth2::{AuthorizationCode, TokenResponse}; @@ -483,7 +482,7 @@ impl XalAuthenticator { /// let live_tokens = authenticator /// .exchange_code_for_token(code, None) /// .await?; - /// + /// /// assert!(!live_tokens.access_token().secret().is_empty()); /// # Ok(()) /// # } @@ -495,8 +494,7 @@ impl XalAuthenticator { ) -> Result { let client = self.oauth_client(None)?; - let mut req = client - .exchange_code(authorization_code); + let mut req = client.exchange_code(authorization_code); if let Some(redirect_url) = &self.app_params.redirect_uri { req = req.set_redirect_uri(std::borrow::Cow::Owned(redirect_url.clone())); @@ -512,16 +510,16 @@ impl XalAuthenticator { } /// Refresh an OAuth2 Refresh Token for specific scope(s) and deserialize into custom response type - /// + /// /// This is used when the token response does not align with the standard. - /// + /// /// # Examples - /// + /// /// ``` /// use xal::XalAuthenticator; /// use xal::oauth2::{RefreshToken, Scope}; /// use serde::{Deserialize, Serialize}; - /// + /// /// // Custom JSON response body /// #[derive(Debug, Serialize, Deserialize)] /// pub struct XCloudTokenResponse { @@ -529,7 +527,7 @@ impl XalAuthenticator { /// pub refresh_token: String, /// pub user_id: String, /// } - /// + /// /// # async fn demo_code() { /// # let refresh_token = RefreshToken::new("...refresh token...".into()); /// let mut authenticator = XalAuthenticator::default(); @@ -538,7 +536,7 @@ impl XalAuthenticator { /// "service::http://Passport.NET/purpose::PURPOSE_XBOX_CLOUD_CONSOLE_TRANSFER_TOKEN".into() /// ) /// ]; - /// + /// /// let token_response = authenticator /// .refresh_token_for_scope::( /// &refresh_token, @@ -548,7 +546,7 @@ impl XalAuthenticator { /// .unwrap(); /// # } /// ``` - /// + /// pub async fn refresh_token_for_scope( &mut self, refresh_token: &RefreshToken, @@ -576,17 +574,17 @@ impl XalAuthenticator { } /// Refresh a Windows Live Refresh & Access Token by providing a Refresh Token - /// + /// /// # Arguments - /// + /// /// * `refresh_token` - The refresh token to use for obtaining a new access token - /// + /// /// # Examples - /// + /// /// ``` /// use xal::XalAuthenticator; /// use xal::oauth2::RefreshToken; - /// + /// /// let authenticator = XalAuthenticator::default(); /// let refresh_token = RefreshToken::new("old_refresh_token".to_string()); /// /* @@ -594,7 +592,7 @@ impl XalAuthenticator { /// .refresh_token(&refresh_token) /// .await /// .unwrap(); - /// + /// /// println!("Refreshed tokens: {refreshed_live_tokens:?}"); /// */ /// ``` @@ -610,25 +608,25 @@ impl XalAuthenticator { /// Xbox Live token functionality impl XalAuthenticator { /// Initiate authentication via SISU flow - /// + /// /// # Parameters - /// + /// /// * `device_token`: A [`response::DeviceToken`] object representing the device token. /// * `code_challenge`: A [`PkceCodeChallenge`] object representing the code challenge. /// * `state`: A [`CsrfToken`] object representing the CSRF token. - /// + /// /// # Errors - /// + /// /// * If `device_token` is missing. /// * If `redirect_uri` is missing. /// * If the Sisu Authentication request fails. /// /// # Examples - /// + /// /// ``` /// use xal::XalAuthenticator; /// use xal::url::Url; - /// + /// /// # async fn demo_code() { /// let mut authenticator = XalAuthenticator::default(); /// let state = XalAuthenticator::generate_random_state(); @@ -636,7 +634,7 @@ impl XalAuthenticator { /// let device_token = authenticator.get_device_token() /// .await /// .unwrap(); - /// + /// /// let (resp, session_id) = authenticator.sisu_authenticate( /// &device_token, /// &pkce_challenge, @@ -644,23 +642,23 @@ impl XalAuthenticator { /// ) /// .await /// .unwrap(); - /// + /// /// println!( /// "Visit this url and pass back the redirect url containing the authorization code {}", /// resp.msa_oauth_redirect /// ); /// let redirect_url = Url::parse("https://example.com/?code=123").unwrap(); - /// + /// /// let authorization_code = XalAuthenticator::parse_authorization_code_response( /// &redirect_url, Some(&state) /// ).unwrap(); - /// + /// /// let live_tokens = authenticator.exchange_code_for_token( /// authorization_code, Some(pkce_verifier) /// ) /// .await /// .unwrap(); - /// + /// /// let sisu_authorization_resp = authenticator.sisu_authorize( /// &live_tokens, &device_token, Some(session_id) /// ) @@ -668,9 +666,9 @@ impl XalAuthenticator { /// .unwrap(); /// # } /// ``` - /// + /// /// # Notes - /// + /// /// It is mandatory to have [`XalAppParameters`] setup with a `redirect_uri` and `title_id`. pub async fn sisu_authenticate( &mut self, @@ -684,9 +682,13 @@ impl XalAuthenticator { ), Error, > { - let title_id = self.app_params.title_id + let title_id = self + .app_params + .title_id .clone() - .ok_or(Error::InvalidRequest("Sisu authentication not possible without title Id (check XalAppParameters)".into()))?; + .ok_or(Error::InvalidRequest( + "Sisu authentication not possible without title Id (check XalAppParameters)".into(), + ))?; let json_body = request::SisuAuthenticationRequest { app_id: &self.app_params.app_id, @@ -732,16 +734,16 @@ impl XalAuthenticator { } /// Authorize via SISU flow after completing OAuth2 Authentication - /// + /// /// This function handles the second step of the SISU flow. /// The response from the server contains a collection of tokens, which can be used for further interaction with the Xbox Live service. - /// + /// /// # Examples - /// + /// /// ``` /// use xal::XalAuthenticator; /// use xal::url::Url; - /// + /// /// # async fn demo_code() { /// let mut authenticator = XalAuthenticator::default(); /// let state = XalAuthenticator::generate_random_state(); @@ -749,7 +751,7 @@ impl XalAuthenticator { /// let device_token = authenticator.get_device_token() /// .await /// .unwrap(); - /// + /// /// let (resp, session_id) = authenticator.sisu_authenticate( /// &device_token, /// &pkce_challenge, @@ -757,23 +759,23 @@ impl XalAuthenticator { /// ) /// .await /// .unwrap(); - /// + /// /// println!( /// "Visit this url and pass back the redirect url containing the authorization code {}", /// resp.msa_oauth_redirect /// ); /// let redirect_url = Url::parse("https://example.com/?code=123").unwrap(); - /// + /// /// let authorization_code = XalAuthenticator::parse_authorization_code_response( /// &redirect_url, Some(&state) /// ).unwrap(); - /// + /// /// let live_tokens = authenticator.exchange_code_for_token( /// authorization_code, Some(pkce_verifier) /// ) /// .await /// .unwrap(); - /// + /// /// let sisu_authorization_resp = authenticator.sisu_authorize( /// &live_tokens, &device_token, Some(session_id) /// ) @@ -818,29 +820,29 @@ impl XalAuthenticator { /// This method returns an `Error` if the POST request fails or the JSON response cannot be parsed. /// /// # Examples - /// + /// /// ``` /// # async fn demo_code() { /// use xal::XalAuthenticator; - /// + /// /// let mut authenticator = XalAuthenticator::default(); /// let device_token = authenticator.get_device_token() /// .await /// .unwrap(); - /// + /// /// assert!(!device_token.token.is_empty()); /// # } /// ``` - /// + /// /// # Notes - /// + /// /// Device tokens can only be requested for devices of the following type: - /// + /// /// - Android /// - iOS /// - Nintendo /// - Win32 - /// + /// /// Xbox devices use a much more sophisticated request method. pub async fn get_device_token(&mut self) -> Result { let device_id = self.device_id.hyphenated().to_string(); @@ -882,7 +884,7 @@ impl XalAuthenticator { /// /// This method sends a POST request to the Xbox Live User Authentication URL, using the provided /// `access_token` and `prefix`. - /// + /// /// The resulting User Token is then used to retrieve the final *XSTS* token to access Xbox Live services. /// /// # Arguments @@ -934,7 +936,7 @@ impl XalAuthenticator { /// /// This method sends a POST request to the Xbox Live Title Authentication URL, using the provided /// `access_token` and `device_token`. - /// + /// /// The resulting Title Token is then used to retrieve the final *XSTS* token to access Xbox Live services. /// /// # Arguments @@ -983,7 +985,7 @@ impl XalAuthenticator { /// /// This method sends a POST request to the Xbox Live XSTS Authentication URL, using the provided `relying_party` /// and optionally `device_token`, `title_token`, and `user_token`. - /// + /// /// The resulting XSTS token can be used to authenticate with various Xbox Live services. /// /// # Arguments @@ -1041,9 +1043,9 @@ impl XalAuthenticator { #[cfg(test)] mod test { - use std::time::Duration; - use oauth2::{basic::BasicTokenType, RequestTokenError}; use super::*; + use oauth2::{basic::BasicTokenType, RequestTokenError}; + use std::time::Duration; fn parse_authorization_code_response( url: &'static str, diff --git a/src/flows.rs b/src/flows.rs index 8adeb80..68537c1 100644 --- a/src/flows.rs +++ b/src/flows.rs @@ -10,7 +10,7 @@ use url::Url; use crate::{ response::{SisuAuthenticationResponse, WindowsLiveTokens}, tokenstore::TokenStore, - Error, XalAuthenticator, AccessTokenPrefix, + AccessTokenPrefix, Error, XalAuthenticator, }; /// Argument passed into [`crate::flows::AuthPromptCallback`] @@ -170,7 +170,7 @@ pub trait AuthPromptCallback { /// /// 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. /// @@ -219,11 +219,33 @@ impl AuthPromptCallback for CliCallbackHandler { /// This function may return an error if the file cannot be read, fails to deserialize or the /// tokens cannot be refreshed. /// +/// # Examples +/// +/// ``` +/// # use xal::{Error, tokenstore::TokenStore}; +/// use xal::flows; +/// +/// # 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_tokens_from_file( +pub async fn try_refresh_live_tokens_from_file( filepath: &str, ) -> Result<(XalAuthenticator, TokenStore), Error> { let mut ts = TokenStore::load_from_file(filepath)?; @@ -233,12 +255,32 @@ pub async fn try_refresh_tokens_from_file( /// Try to read tokens from the token store and refresh the Windows Live tokens if needed. /// +/// # Examples +/// +/// ``` +/// use std::fs::File; +/// use serde_json; +/// use xal::{flows, tokenstore::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::TokenStore`] /// is returned. TokenStore will contain the refreshed `live_tokens`. pub async fn try_refresh_live_tokens_from_tokenstore( @@ -259,6 +301,34 @@ pub async fn try_refresh_live_tokens_from_tokenstore( } /// Shorthand for Windows Live device code flow +/// +/// # Examples +/// +/// ``` +/// use xal::{XalAuthenticator, flows, Error, AccessTokenPrefix}; +/// use xal::response::WindowsLiveTokens; +/// +/// # async fn async_sleep_fn(_: std::time::Duration) {} +/// +/// # async fn example() -> Result<(), Error> { +/// let do_implicit_flow = true; +/// let mut authenticator = XalAuthenticator::default(); +/// +/// let token_store = flows::ms_device_code_flow( +/// &mut authenticator, +/// flows::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( authenticator: &mut XalAuthenticator, cb: impl AuthPromptCallback, @@ -269,9 +339,7 @@ where SF: std::future::Future, { trace!("Initiating device code flow"); - let device_code_flow = authenticator - .initiate_device_code_auth() - .await?; + 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"); @@ -302,6 +370,32 @@ where /// Shorthand for Windows Live authorization flow /// - Depending on the argument `implicit` the /// methods `implicit grant` or `authorization code` are chosen +/// +/// # Examples +/// +/// ``` +/// use xal::{XalAuthenticator, flows, Error, AccessTokenPrefix}; +/// 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, +/// flows::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, @@ -309,8 +403,7 @@ pub async fn ms_authorization_flow( ) -> Result { trace!("Starting implicit authorization flow"); - let (url, state) = - authenticator.get_authorization_url(implicit)?; + let (url, state) = authenticator.get_authorization_url(implicit)?; trace!("Reaching into callback to receive authentication redirect URL"); let redirect_url = cb @@ -354,6 +447,30 @@ pub async fn ms_authorization_flow( } /// Shorthand for sisu authentication flow +/// +/// # Examples +/// +/// ``` +/// use xal::{XalAuthenticator, flows, Error, AccessTokenPrefix}; +/// use xal::response::WindowsLiveTokens; +/// +/// # async fn example() -> Result<(), Error> { +/// let mut authenticator = XalAuthenticator::default(); +/// +/// let token_store = flows::xbox_live_sisu_full_flow( +/// &mut authenticator, +/// flows::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, @@ -425,7 +542,7 @@ pub async fn xbox_live_sisu_full_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. /// @@ -451,26 +568,36 @@ pub async fn xbox_live_sisu_full_flow( /// # Examples /// /// ``` +/// use xal::{XalAuthenticator, flows, Error, AccessTokenPrefix}; +/// use xal::response::WindowsLiveTokens; +/// +/// # async fn async_sleep_fn(_: std::time::Duration) {} +/// /// # async fn example() -> Result<(), Error> { -/// // Assume the following authenticator and tokens have been obtained.. /// let mut authenticator = XalAuthenticator::default(); -/// let live_tokens = WindowsLiveTokens { /*...*/ }; +/// +/// let token_store = flows::ms_device_code_flow( +/// &mut authenticator, +/// flows::CliCallbackHandler, +/// async_sleep_fn +/// ) +/// .await?; /// /// // Execute the Xbox Live authorization flow.. -/// let (authenticator, token_store) = authenticator -/// .do_xbox_live_authorization_flow( -/// live_tokens, -/// "rp://api.minecraftservices.com/".to_string(), -/// AccessTokenPrefix::MSAL, -/// true, -/// ) -/// .await?; +/// 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 @@ -534,7 +661,40 @@ pub async fn xbox_live_authorization_traditional_flow( Ok(ts) } -/// bla +/// The authorization part of Sisu +/// +/// # Examples +/// +/// ``` +/// use xal::{XalAuthenticator, flows, Error, AccessTokenPrefix}; +/// 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, +/// flows::CliCallbackHandler, +/// async_sleep_fn +/// ) +/// .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, @@ -545,7 +705,9 @@ pub async fn xbox_live_sisu_authorization_flow( debug!("Device token={:?}", device_token); trace!("Getting user token"); - let resp = authenticator.sisu_authorize(&live_tokens, &device_token, None).await?; + let resp = authenticator + .sisu_authorize(&live_tokens, &device_token, None) + .await?; debug!("Sisu authorization response"); let ts = TokenStore { @@ -562,4 +724,4 @@ pub async fn xbox_live_sisu_authorization_flow( }; Ok(ts) -} \ No newline at end of file +} diff --git a/src/models.rs b/src/models.rs index 81a5cf7..49df472 100644 --- a/src/models.rs +++ b/src/models.rs @@ -312,7 +312,7 @@ pub mod response { issue_instant: "2020-12-15T00:00:00.0000000Z".into(), not_after: "2199-12-15T00:00:00.0000000Z".into(), token: s.to_owned(), - display_claims: None + display_claims: None, } } } @@ -405,9 +405,9 @@ pub mod response { } /// Access Token prefix -/// +/// /// Relevant for fetching the UserToken -/// +/// /// Exact conditions are still unknown, when to use which format. #[derive(Debug, Eq, PartialEq, Clone)] pub enum AccessTokenPrefix { @@ -416,7 +416,7 @@ pub enum AccessTokenPrefix { /// Prefix access token with "t=" T, /// Use token string as-is - None + None, } impl ToString for AccessTokenPrefix { @@ -455,7 +455,7 @@ impl FromStr for DeviceType { "ios" => DeviceType::IOS, "win32" => DeviceType::WIN32, "nintendo" => DeviceType::NINTENDO, - val => DeviceType::Custom(val.to_owned()) + val => DeviceType::Custom(val.to_owned()), }; Ok(enm) } @@ -496,8 +496,8 @@ pub struct XalAppParameters { pub mod app_params { use oauth2::{RedirectUrl, Scope}; - use crate::Constants; use super::XalAppParameters; + use crate::Constants; /// Xbox Beta App pub fn APP_XBOX_BETA() -> XalAppParameters { @@ -583,7 +583,9 @@ pub mod app_params { app_id: "00000000402b5328".into(), title_id: None, auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())], - redirect_uri: None, + redirect_uri: Some( + RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(), + ), } } @@ -593,7 +595,9 @@ pub mod app_params { app_id: "00000000441cc96b".into(), title_id: Some("2047319603".into()), auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())], - redirect_uri: None, + redirect_uri: Some( + RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(), + ), } } @@ -603,7 +607,9 @@ pub mod app_params { app_id: "0000000048183522".into(), title_id: Some("1739947436".into()), auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())], - redirect_uri: None, + redirect_uri: Some( + RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(), + ), } } @@ -613,7 +619,9 @@ pub mod app_params { app_id: "000000004c17c01a".into(), title_id: Some("1810924247".into()), auth_scopes: vec![Scope::new(Constants::SCOPE_SERVICE_USER_AUTH.to_string())], - redirect_uri: None, + redirect_uri: Some( + RedirectUrl::new(crate::Constants::OAUTH20_DESKTOP_REDIRECT_URL.into()).unwrap(), + ), } } diff --git a/src/request_signer.rs b/src/request_signer.rs index 486224d..b337656 100644 --- a/src/request_signer.rs +++ b/src/request_signer.rs @@ -3,13 +3,15 @@ use crate::{ error::Error, + extensions::JsonExDeserializeMiddleware, models::{self, SigningPolicy}, - ProofKey, response::{self, TitleEndpointsResponse}, Constants, extensions::JsonExDeserializeMiddleware, + response::{self, TitleEndpointsResponse}, + Constants, ProofKey, }; use base64ct::{self, Base64, Encoding}; use chrono::prelude::*; -use nt_time::FileTime; use log::warn; +use nt_time::FileTime; use p256::{ ecdsa::{ signature::hazmat::{PrehashSigner, PrehashVerifier}, @@ -17,7 +19,7 @@ use p256::{ }, SecretKey, }; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::{ convert::{TryFrom, TryInto}, @@ -333,7 +335,7 @@ impl RequestSigner { let signing_key: SigningKey = self.keypair.clone().into(); let filetime_bytes = FileTime::try_from(timestamp) - .map_err(|e|Error::GeneralError(format!("{e}")))? + .map_err(|e| Error::GeneralError(format!("{e}")))? .to_be_bytes(); let signing_policy_version_bytes = signing_policy_version.to_be_bytes(); @@ -443,8 +445,8 @@ pub async fn get_endpoints() -> Result } /// Signature policy cache -/// -/// +/// +/// #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SignaturePolicyCache { endpoints: TitleEndpointsResponse, @@ -453,9 +455,7 @@ pub struct SignaturePolicyCache { impl SignaturePolicyCache { /// Create a new SignaturePolicyCache. pub fn new(endpoints: TitleEndpointsResponse) -> Self { - Self { - endpoints - } + Self { endpoints } } /// Retrieve the stored TitleEndpointsResponse. @@ -464,46 +464,55 @@ impl SignaturePolicyCache { } /// Find the policy for the given URL. - /// + /// /// If a matching policy is found, returns the corresponding SigningPolicy. Otherwise, returns None. pub fn find_policy_for_url(&self, url: &str) -> Result, Error> { let url = url::Url::parse(url)?; - - if !["http","https"].contains(&url.scheme()) { - return Err(Error::GeneralError(format!("Url with invalid protocol passed, expected http or https, url={url}"))) + + if !["http", "https"].contains(&url.scheme()) { + return Err(Error::GeneralError(format!( + "Url with invalid protocol passed, expected http or https, url={url}" + ))); } - - let matching_endpoint = self.endpoints.end_points + + let matching_endpoint = self + .endpoints + .end_points .iter() .filter(|e| { - e.protocol.eq_ignore_ascii_case(url.scheme()) && - url.host_str().map(|host| { - match e.host_type.as_str() { - "fqdn" => host == e.host, - "wildcard" => host.ends_with(e.host.trim_start_matches('*')), - _ => false, - } - }).unwrap_or(false) && - e.path.as_ref().map(|path| url.path() == path).unwrap_or(true) && - e.signature_policy_index.is_some() + e.protocol.eq_ignore_ascii_case(url.scheme()) + && url + .host_str() + .map(|host| match e.host_type.as_str() { + "fqdn" => host == e.host, + "wildcard" => host.ends_with(e.host.trim_start_matches('*')), + _ => false, + }) + .unwrap_or(false) + && e.path + .as_ref() + .map(|path| url.path() == path) + .unwrap_or(true) + && e.signature_policy_index.is_some() }) .max_by_key(|e| e.host.len()); - + match matching_endpoint { Some(ep) => { println!("Identified Title endpoint={ep:?} for URL={url} {url:?}"); let policy_index = ep.signature_policy_index.unwrap() as usize; - let policy = self.endpoints - .signature_policies - .get(policy_index) - .ok_or(Error::GeneralError(format!("SignaturePolicy at index {policy_index} not found!")))?; - + let policy = self.endpoints.signature_policies.get(policy_index).ok_or( + Error::GeneralError(format!( + "SignaturePolicy at index {policy_index} not found!" + )), + )?; + Ok(Some(policy.to_owned())) - }, + } None => { warn!("No matched SigningPolicy for url={url:?} found"); Ok(None) - }, + } } } } @@ -532,27 +541,90 @@ mod test { #[test] fn find_matching_signing_policy() { - let policy_0: SigningPolicy = SigningPolicy { version: 1, supported_algorithms: vec![SigningAlgorithm::ES256], max_body_bytes: 8192 }; - let policy_1: SigningPolicy = SigningPolicy { version: 1, supported_algorithms: vec![SigningAlgorithm::ES256], max_body_bytes: 4294967295 }; + let policy_0: SigningPolicy = SigningPolicy { + version: 1, + supported_algorithms: vec![SigningAlgorithm::ES256], + max_body_bytes: 8192, + }; + let policy_1: SigningPolicy = SigningPolicy { + version: 1, + supported_algorithms: vec![SigningAlgorithm::ES256], + max_body_bytes: 4294967295, + }; let title_endpoints = serde_json::from_str::( - include_str!("../testdata/title_endpoints.json") - ).unwrap(); + include_str!("../testdata/title_endpoints.json"), + ) + .unwrap(); let cache = SignaturePolicyCache::new(title_endpoints); - assert!(cache.find_policy_for_url("https://unhandled.example.com").unwrap().is_none()); - assert!(cache.find_policy_for_url("https://unhandled.microsoft.com").unwrap().is_none()); + assert!(cache + .find_policy_for_url("https://unhandled.example.com") + .unwrap() + .is_none()); + assert!(cache + .find_policy_for_url("https://unhandled.microsoft.com") + .unwrap() + .is_none()); - assert_eq!(cache.find_policy_for_url("https://experimentation.xboxlive.com").unwrap().unwrap(), policy_0); - assert_eq!(cache.find_policy_for_url("https://xoobe.xboxlive.com").unwrap().unwrap(), policy_0); - assert_eq!(cache.find_policy_for_url("https://xaaa.bbtv.cn/xboxsms/OOBEService/AuthorizationStatus").unwrap().unwrap(), policy_0); + assert_eq!( + cache + .find_policy_for_url("https://experimentation.xboxlive.com") + .unwrap() + .unwrap(), + policy_0 + ); + assert_eq!( + cache + .find_policy_for_url("https://xoobe.xboxlive.com") + .unwrap() + .unwrap(), + policy_0 + ); + assert_eq!( + cache + .find_policy_for_url("https://xaaa.bbtv.cn/xboxsms/OOBEService/AuthorizationStatus") + .unwrap() + .unwrap(), + policy_0 + ); - assert_eq!(cache.find_policy_for_url("https://hello.experimentation.xboxlive.com").unwrap().unwrap(), policy_1); - assert_eq!(cache.find_policy_for_url("https://data-vef.xboxlive.com").unwrap().unwrap(), policy_1); - assert_eq!(cache.find_policy_for_url("https://settings.xboxlive.com").unwrap().unwrap(), policy_1); - assert_eq!(cache.find_policy_for_url("https://device.mgt.xboxlive.com").unwrap().unwrap(), policy_1); - assert_eq!(cache.find_policy_for_url("https://device.mgt.xboxlive.com/devices/current/unlock").unwrap().unwrap(), policy_1); + assert_eq!( + cache + .find_policy_for_url("https://hello.experimentation.xboxlive.com") + .unwrap() + .unwrap(), + policy_1 + ); + assert_eq!( + cache + .find_policy_for_url("https://data-vef.xboxlive.com") + .unwrap() + .unwrap(), + policy_1 + ); + assert_eq!( + cache + .find_policy_for_url("https://settings.xboxlive.com") + .unwrap() + .unwrap(), + policy_1 + ); + assert_eq!( + cache + .find_policy_for_url("https://device.mgt.xboxlive.com") + .unwrap() + .unwrap(), + policy_1 + ); + assert_eq!( + cache + .find_policy_for_url("https://device.mgt.xboxlive.com/devices/current/unlock") + .unwrap() + .unwrap(), + policy_1 + ); } #[test]