1use std::collections::HashMap;
72use std::fmt::Debug;
73
74use getset::{CopyGetters, Getters};
75use http::header::{ACCEPT, CONTENT_TYPE};
76use http::{Method, Request, StatusCode};
77use nutype::nutype;
78use serde::{Deserialize, Serialize};
79use thiserror::Error;
80
81const MINECRAFT_LOGIN_WITH_XBOX: &str = "https://api.minecraftservices.com/authentication/login_with_xbox";
82const XBOX_USER_AUTHENTICATE: &str = "https://user.auth.xboxlive.com/user/authenticate";
83const XBOX_XSTS_AUTHORIZE: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
84
85pub type HttpRequest = Request<Vec<u8>>;
87
88pub type HttpResponse = http::Response<Vec<u8>>;
90
91#[maybe_async::maybe_async]
102pub trait HttpClient {
103 type Error: std::error::Error + Send + Sync + 'static;
105
106 async fn call(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error>;
108}
109
110#[nutype(
112 validate(not_empty),
113 derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize, AsRef, Into)
114)]
115pub struct MinecraftAccessToken(String);
116
117impl Debug for MinecraftAccessToken {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 f.debug_tuple("MinecraftAccessToken").field(&"[redacted]").finish()
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
125#[serde(rename_all = "PascalCase")]
126pub enum MinecraftTokenType {
127 Bearer,
128}
129
130#[derive(Error, Debug)]
132pub enum MinecraftAuthorizationError<E: std::error::Error> {
133 #[error(transparent)]
135 Http(E),
136
137 #[error("HTTP status error: {0}")]
139 HttpStatus(StatusCode),
140
141 #[error(transparent)]
143 Json(#[from] serde_json::Error),
144
145 #[error("Minor must be added to microsoft family")]
147 AddToFamily,
148
149 #[error("Account does not have xbox")]
151 NoXbox,
152
153 #[error("missing claims from response")]
155 MissingClaims,
156
157 #[error("Xbox Live authentication failed with error code {code}")]
159 XboxLive { code: u32 },
160}
161
162#[derive(Deserialize, Serialize, Debug, Getters, CopyGetters, Clone)]
165pub struct MinecraftAuthenticationResponse {
166 #[getset(get = "pub")]
169 username: String,
170
171 #[getset(get = "pub")]
173 access_token: MinecraftAccessToken,
174
175 #[getset(get = "pub")]
177 token_type: MinecraftTokenType,
178
179 #[getset(get_copy = "pub")]
181 expires_in: u32,
182}
183
184#[derive(Deserialize, Debug)]
186#[serde(rename_all = "PascalCase")]
187struct XboxLiveAuthenticationResponse {
188 token: String,
190
191 display_claims: HashMap<String, Vec<HashMap<String, String>>>,
194}
195
196#[derive(Debug, Deserialize)]
198#[serde(rename_all = "PascalCase")]
199struct XboxLiveErrorResponse {
200 #[serde(rename = "XErr")]
201 code: XboxLiveErrorCode,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
205#[serde(from = "u32")]
206enum XboxLiveErrorCode {
207 XboxAccountRequired,
208 FamilyMembershipRequired,
209 Unknown(u32),
210}
211
212impl From<u32> for XboxLiveErrorCode {
213 fn from(code: u32) -> Self {
214 match code {
215 2_148_916_233 => Self::XboxAccountRequired,
216 2_148_916_238 => Self::FamilyMembershipRequired,
217 code => Self::Unknown(code),
218 }
219 }
220}
221
222pub struct MinecraftAuthorizationFlow<C> {
225 http_client: C,
226}
227
228impl<C> MinecraftAuthorizationFlow<C> {
229 pub const fn new(http_client: C) -> Self {
232 Self { http_client }
233 }
234}
235
236#[maybe_async::maybe_async]
237impl<C: HttpClient> MinecraftAuthorizationFlow<C> {
238 pub async fn exchange_microsoft_token(
242 &self, microsoft_access_token: impl AsRef<str>,
243 ) -> Result<MinecraftAuthenticationResponse, MinecraftAuthorizationError<C::Error>> {
244 #[derive(Serialize)]
245 struct MinecraftAuthenticationRequest {
246 #[serde(rename = "identityToken")]
247 identity_token: String,
248 }
249
250 let (xbox_token, user_hash) = self.xbox_token(microsoft_access_token).await?;
251 let xbox_security_token = self.xbox_security_token(xbox_token).await?;
252
253 let response = self
254 .post_json(MINECRAFT_LOGIN_WITH_XBOX, &MinecraftAuthenticationRequest {
255 identity_token: format!(
256 "XBL3.0 x={user_hash};{xsts_token}",
257 user_hash = user_hash,
258 xsts_token = xbox_security_token.token
259 ),
260 })
261 .await?;
262 if !response.status().is_success() {
263 return Err(MinecraftAuthorizationError::HttpStatus(response.status()));
264 }
265
266 let response = serde_json::from_slice(response.body())?;
267 Ok(response)
268 }
269
270 async fn xbox_security_token(
271 &self, xbox_token: String,
272 ) -> Result<XboxLiveAuthenticationResponse, MinecraftAuthorizationError<C::Error>> {
273 #[derive(Serialize)]
274 struct Properties {
275 #[serde(rename = "SandboxId")]
276 sandbox_id: &'static str,
277 #[serde(rename = "UserTokens")]
278 user_tokens: [String; 1],
279 }
280
281 #[derive(Serialize)]
282 struct XboxSecurityTokenRequest {
283 #[serde(rename = "Properties")]
284 properties: Properties,
285 #[serde(rename = "RelyingParty")]
286 relying_party: &'static str,
287 #[serde(rename = "TokenType")]
288 token_type: &'static str,
289 }
290
291 let response = self
292 .post_json(XBOX_XSTS_AUTHORIZE, &XboxSecurityTokenRequest {
293 properties: Properties {
294 sandbox_id: "RETAIL",
295 user_tokens: [xbox_token],
296 },
297 relying_party: "rp://api.minecraftservices.com/",
298 token_type: "JWT",
299 })
300 .await?;
301 if response.status() == StatusCode::UNAUTHORIZED {
302 let error: XboxLiveErrorResponse = serde_json::from_slice(response.body())?;
303 Err(match error.code {
304 XboxLiveErrorCode::XboxAccountRequired => MinecraftAuthorizationError::NoXbox,
305 XboxLiveErrorCode::FamilyMembershipRequired => MinecraftAuthorizationError::AddToFamily,
306 XboxLiveErrorCode::Unknown(code) => MinecraftAuthorizationError::XboxLive { code },
307 })
308 } else if !response.status().is_success() {
309 Err(MinecraftAuthorizationError::HttpStatus(response.status()))
310 } else {
311 let xbox_security_token_resp: XboxLiveAuthenticationResponse = serde_json::from_slice(response.body())?;
312 Ok(xbox_security_token_resp)
313 }
314 }
315
316 async fn xbox_token(
317 &self, microsoft_access_token: impl AsRef<str>,
318 ) -> Result<(String, String), MinecraftAuthorizationError<C::Error>> {
319 #[derive(Serialize)]
320 struct Properties {
321 #[serde(rename = "AuthMethod")]
322 auth_method: &'static str,
323 #[serde(rename = "SiteName")]
324 site_name: &'static str,
325 #[serde(rename = "RpsTicket")]
326 rps_ticket: String,
327 }
328
329 #[derive(Serialize)]
330 struct XboxTokenRequest {
331 #[serde(rename = "Properties")]
332 properties: Properties,
333 #[serde(rename = "RelyingParty")]
334 relying_party: &'static str,
335 #[serde(rename = "TokenType")]
336 token_type: &'static str,
337 }
338
339 let response = self
340 .post_json(XBOX_USER_AUTHENTICATE, &XboxTokenRequest {
341 properties: Properties {
342 auth_method: "RPS",
343 site_name: "user.auth.xboxlive.com",
344 rps_ticket: format!("d={}", microsoft_access_token.as_ref()),
345 },
346 relying_party: "http://auth.xboxlive.com",
347 token_type: "JWT",
348 })
349 .await?;
350 if !response.status().is_success() {
351 return Err(MinecraftAuthorizationError::HttpStatus(response.status()));
352 }
353
354 let xbox_resp: XboxLiveAuthenticationResponse = serde_json::from_slice(response.body())?;
355 let xbox_token = xbox_resp.token;
356 let user_hash = xbox_resp
357 .display_claims
358 .get("xui")
359 .ok_or(MinecraftAuthorizationError::MissingClaims)?
360 .first()
361 .ok_or(MinecraftAuthorizationError::MissingClaims)?
362 .get("uhs")
363 .ok_or(MinecraftAuthorizationError::MissingClaims)?
364 .to_owned();
365 Ok((xbox_token, user_hash))
366 }
367
368 async fn post_json<T: Serialize>(
369 &self, url: &str, body: &T,
370 ) -> Result<HttpResponse, MinecraftAuthorizationError<C::Error>> {
371 let request = Request::builder()
372 .method(Method::POST)
373 .uri(url)
374 .header(CONTENT_TYPE, "application/json")
375 .header(ACCEPT, "application/json")
376 .body(serde_json::to_vec(body)?)
377 .expect("static request parts should be valid");
378 self.http_client
379 .call(request)
380 .await
381 .map_err(MinecraftAuthorizationError::Http)
382 }
383}
384
385#[cfg(feature = "reqwest")]
386mod reqwest_client {
387 use super::{HttpClient, HttpRequest, HttpResponse};
388
389 #[maybe_async::async_impl]
390 impl HttpClient for reqwest::Client {
391 type Error = reqwest::Error;
392
393 async fn call(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
394 let response = self.execute(reqwest::Request::try_from(request)?).await?;
395 let status = response.status();
396 let headers = response.headers().clone();
397 let body = response.bytes().await?.to_vec();
398
399 let mut response = HttpResponse::new(body);
400 *response.status_mut() = status;
401 *response.headers_mut() = headers;
402 Ok(response)
403 }
404 }
405
406 #[maybe_async::sync_impl]
407 impl HttpClient for reqwest::blocking::Client {
408 type Error = reqwest::Error;
409
410 fn call(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
411 let response = self.execute(reqwest::blocking::Request::try_from(request)?)?;
412 let status = response.status();
413 let headers = response.headers().clone();
414 let body = response.bytes()?.to_vec();
415
416 let mut response = HttpResponse::new(body);
417 *response.status_mut() = status;
418 *response.headers_mut() = headers;
419 Ok(response)
420 }
421 }
422}
423
424#[cfg(feature = "ureq")]
425pub use ureq_client::UreqClient;
426
427#[cfg(feature = "ureq")]
428mod ureq_client {
429 use super::{HttpClient, HttpRequest, HttpResponse};
430
431 #[derive(Debug, Clone)]
446 pub struct UreqClient(ureq::Agent);
447
448 impl UreqClient {
449 pub fn with_agent(agent: ureq::Agent) -> Self {
458 Self(agent)
459 }
460 }
461
462 impl Default for UreqClient {
463 fn default() -> Self {
464 Self(ureq::Agent::config_builder().http_status_as_error(false).build().into())
465 }
466 }
467
468 #[maybe_async::maybe_async]
469 impl HttpClient for UreqClient {
470 type Error = ureq::Error;
471
472 async fn call(&self, request: HttpRequest) -> Result<HttpResponse, Self::Error> {
473 let response = self.0.run(request)?;
474 let (parts, mut body) = response.into_parts();
475 Ok(HttpResponse::from_parts(parts, body.read_to_vec()?))
476 }
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::{XboxLiveErrorCode, XboxLiveErrorResponse};
483
484 #[test]
485 fn deserializes_known_xbox_live_error_codes() {
486 let no_xbox: XboxLiveErrorResponse =
487 serde_json::from_str(r#"{"Identity":"0","XErr":2148916233,"Message":"","Redirect":""}"#).unwrap();
488 let add_to_family: XboxLiveErrorResponse =
489 serde_json::from_str(r#"{"Identity":"0","XErr":2148916238,"Message":"","Redirect":""}"#).unwrap();
490
491 assert_eq!(no_xbox.code, XboxLiveErrorCode::XboxAccountRequired);
492 assert_eq!(add_to_family.code, XboxLiveErrorCode::FamilyMembershipRequired);
493 }
494
495 #[test]
496 fn preserves_unknown_xbox_live_error_codes() {
497 let response: XboxLiveErrorResponse = serde_json::from_str(r#"{"XErr":42}"#).unwrap();
498
499 assert_eq!(response.code, XboxLiveErrorCode::Unknown(42));
500 }
501
502 #[test]
503 fn rejects_malformed_xbox_live_error_responses() {
504 let response = serde_json::from_str::<XboxLiveErrorResponse>(r#"{"XErr":"invalid"}"#);
505
506 assert!(response.is_err());
507 }
508}