use std::borrow::Cow; use std::collections::HashMap; use std::ops::Add; use std::time::{Duration, SystemTime}; use chrono::{DateTime, TimeDelta, Utc}; use log::debug; use oauth2::AccessToken; use reqwest::{IntoUrl, Method, RequestBuilder}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::auth::AuthError; use crate::auth::types::Token; use crate::util::USER_AGENT; const XBOX_LIVE_AUTH: &str = "https://user.auth.xboxlive.com/user/authenticate"; const XBOX_LIVE_XSTS: &str = "https://xsts.auth.xboxlive.com/xsts/authorize"; #[derive(Debug, Serialize)] #[serde(rename_all = "PascalCase")] struct XboxLiveAuthRequestProperties<'a> { auth_method: &'a str, site_name: &'a str, rps_ticket: &'a str } #[derive(Debug, Serialize)] #[serde(rename_all = "PascalCase")] struct XboxLiveAuthRequest<'a> { properties: XboxLiveAuthRequestProperties<'a>, relying_party: &'a str, token_type: &'a str } #[derive(Debug, Deserialize)] #[serde(rename_all = "PascalCase")] struct XboxLiveAuthResponse { token: String, not_after: DateTime } pub async fn xbox_live_login(client: &reqwest::Client, access_token: &AccessToken, azure: bool) -> Result { debug!("MSA performing xbox live login ({azure})"); let ticket = match azure { true => Cow::Owned(format!("d={}", access_token.secret())), _ => Cow::Borrowed(access_token.secret().as_str()) }; let request = XboxLiveAuthRequest { properties: XboxLiveAuthRequestProperties { auth_method: "RPS", site_name: "user.auth.xboxlive.com", rps_ticket: ticket.as_ref() }, relying_party: "http://auth.xboxlive.com", token_type: "JWT" }; let res: XboxLiveAuthResponse = super::build_json_request(client, XBOX_LIVE_AUTH, Method::POST).json(&request).send().await .and_then(|r| r.error_for_status()) .map_err(|e| AuthError::Request { what: "xbox live auth", error: e })? .json().await.map_err(|e| AuthError::Request { what: "xbox live auth (decode)", error: e })?; Ok(Token { value: res.token, expire: Some(res.not_after) }) } #[derive(Serialize, Debug)] #[serde(rename_all = "PascalCase")] struct XSTSAuthRequest<'a> { properties: XSTSAuthRequestProperties<'a>, relying_party: &'a str, token_type: &'a str } #[derive(Serialize, Debug)] #[serde(rename_all = "PascalCase")] struct XSTSAuthRequestProperties<'a> { sandbox_id: &'a str, user_tokens: &'a[&'a str] } #[derive(Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub(super) struct XSTSAuthSuccessResponse { token: String, #[serde(default)] display_claims: XSTSAuthResponseDisplayClaims } #[derive(Deserialize, Debug)] #[serde(rename_all = "PascalCase")] pub(super) struct XSTSAuthErrorResponse { x_err: u64, message: Option } #[derive(Deserialize, Debug)] #[serde(rename_all = "PascalCase", untagged)] pub(super) enum XSTSAuthResponse { Success(XSTSAuthSuccessResponse), Error(XSTSAuthErrorResponse) } #[derive(Deserialize, Debug, Default)] pub(super) struct XSTSAuthResponseDisplayClaims { xui: Vec> } impl XSTSAuthSuccessResponse { pub(super) fn into_token(self) -> String { self.token } fn get_display_claim(&self, name: &str) -> Option<&str> { self.display_claims.xui.iter().filter(|m| m.contains_key(name)) .next().map_or(None, |f| f.get(name).map(|s| s.as_str())) } pub(super) fn get_user_hash(&self) -> Option<&str> { self.get_display_claim("uhs") } pub(super) fn get_xuid(&self) -> Option { self.get_display_claim("xid") .map_or(None, |s| Some(Uuid::from_u64_pair(0, s.parse().ok()?))) } pub(super) fn get_gamertag(&self) -> Option<&str> { self.get_display_claim("gtg") } } impl Into for XSTSAuthErrorResponse { fn into(self) -> AuthError { AuthError::AuthXError { // some well-known error values what: match self.x_err { 2148916238u64 => "Microsoft account held by a minor outside of a family.", 2148916233u64 => "Account is not on Xbox.", _ => "Unknown error." }, x_error: self.x_err, message: self.message } } } pub(super) const XSTS_RP_MINECRAFT_SERVICES: &str = "rp://api.minecraftservices.com/"; pub(super) const XSTS_RP_XBOX_LIVE: &str = "http://xboxlive.com"; pub async fn xsts_request(client: &reqwest::Client, xbl_token: &str, relying_party: &str) -> Result { debug!("Performing XSTS auth {relying_party}"); let token_array = [xbl_token]; let req = XSTSAuthRequest { properties: XSTSAuthRequestProperties { sandbox_id: "RETAIL", user_tokens: token_array.as_slice() }, relying_party, token_type: "JWT" }; let res: XSTSAuthResponse = super::build_json_request(client, XBOX_LIVE_XSTS, Method::POST).json(&req).send().await .and_then(|r| r.error_for_status()) .map_err(|e| AuthError::Request { what: "xsts", error: e })? .json().await .map_err(|e| AuthError::Request { what: "xsts (decode)", error: e })?; Ok(res) }