minecraft_msa_auth/
lib.rs1use std::collections::HashMap;
55use std::fmt::Debug;
56
57use getset::{CopyGetters, Getters};
58use nutype::nutype;
59use reqwest::{Client, StatusCode};
60use serde::{Deserialize, Serialize};
61use serde_json::json;
62use thiserror::Error;
63
64const MINECRAFT_LOGIN_WITH_XBOX: &str = "https://api.minecraftservices.com/authentication/login_with_xbox";
65const XBOX_USER_AUTHENTICATE: &str = "https://user.auth.xboxlive.com/user/authenticate";
66const XBOX_XSTS_AUTHORIZE: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
67
68#[nutype(
70 validate(not_empty),
71 derive(Clone, PartialEq, Eq, Hash, Deserialize, Serialize, AsRef, Into)
72)]
73pub struct MinecraftAccessToken(String);
74
75impl Debug for MinecraftAccessToken {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_tuple("MinecraftAccessToken").field(&"[redacted]").finish()
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
83#[serde(rename_all = "PascalCase")]
84pub enum MinecraftTokenType {
85 Bearer,
86}
87
88#[derive(Error, Debug)]
90pub enum MinecraftAuthorizationError {
91 #[error(transparent)]
93 Reqwest(#[from] reqwest::Error),
94
95 #[error("Minor must be added to microsoft family")]
97 AddToFamily,
98
99 #[error("Account does not have xbox")]
101 NoXbox,
102
103 #[error("missing claims from response")]
105 MissingClaims,
106}
107
108#[derive(Deserialize, Serialize, Debug, Getters, CopyGetters, Clone)]
111pub struct MinecraftAuthenticationResponse {
112 #[getset(get = "pub")]
115 username: String,
116
117 #[getset(get = "pub")]
119 access_token: MinecraftAccessToken,
120
121 #[getset(get = "pub")]
123 token_type: MinecraftTokenType,
124
125 #[getset(get_copy = "pub")]
127 expires_in: u32,
128}
129
130#[derive(Deserialize, Debug)]
132#[serde(rename_all = "PascalCase")]
133struct XboxLiveAuthenticationResponse {
134 token: String,
136
137 display_claims: HashMap<String, Vec<HashMap<String, String>>>,
140}
141
142#[derive(Serialize, Deserialize)]
144#[serde(rename_all = "PascalCase")]
145pub struct XboxLiveAuthenticationResponseError {
146 identity: String,
148
149 x_err: i64,
153
154 message: String,
156
157 redirect: String,
159}
160
161pub struct MinecraftAuthorizationFlow {
164 http_client: Client,
165}
166
167impl MinecraftAuthorizationFlow {
168 pub const fn new(http_client: Client) -> Self {
171 Self { http_client }
172 }
173
174 pub async fn exchange_microsoft_token(
178 &self, microsoft_access_token: impl AsRef<str>,
179 ) -> Result<MinecraftAuthenticationResponse, MinecraftAuthorizationError> {
180 let (xbox_token, user_hash) = self.xbox_token(microsoft_access_token).await?;
181 let xbox_security_token = self.xbox_security_token(xbox_token).await?;
182
183 let response = self
184 .http_client
185 .post(MINECRAFT_LOGIN_WITH_XBOX)
186 .json(&json!({
187 "identityToken":
188 format!(
189 "XBL3.0 x={user_hash};{xsts_token}",
190 user_hash = user_hash,
191 xsts_token = xbox_security_token.token
192 )
193 }))
194 .send()
195 .await?;
196 response.error_for_status_ref()?;
197
198 let response = response.json().await?;
199 Ok(response)
200 }
201
202 async fn xbox_security_token(
203 &self, xbox_token: String,
204 ) -> Result<XboxLiveAuthenticationResponse, MinecraftAuthorizationError> {
205 let response = self
206 .http_client
207 .post(XBOX_XSTS_AUTHORIZE)
208 .json(&json!({
209 "Properties": {
210 "SandboxId": "RETAIL",
211 "UserTokens": [xbox_token]
212 },
213 "RelyingParty": "rp://api.minecraftservices.com/",
214 "TokenType": "JWT"
215 }))
216 .send()
217 .await?;
218 if response.status() == StatusCode::UNAUTHORIZED {
219 let xbox_security_token_err_resp_res = response.json().await;
220 if xbox_security_token_err_resp_res.is_err() {
221 return Err(MinecraftAuthorizationError::MissingClaims);
222 }
223 let xbox_security_token_err_resp: XboxLiveAuthenticationResponseError =
224 xbox_security_token_err_resp_res.expect("This should succeed always");
225 match xbox_security_token_err_resp.x_err {
226 2148916238 => Err(MinecraftAuthorizationError::AddToFamily),
227 2148916233 => Err(MinecraftAuthorizationError::NoXbox),
228 _ => Err(MinecraftAuthorizationError::MissingClaims),
229 }
230 } else {
231 response.error_for_status_ref()?;
232 let xbox_security_token_resp: XboxLiveAuthenticationResponse = response.json().await?;
233 Ok(xbox_security_token_resp)
234 }
235 }
236
237 async fn xbox_token(
238 &self, microsoft_access_token: impl AsRef<str>,
239 ) -> Result<(String, String), MinecraftAuthorizationError> {
240 let xbox_authenticate_json = json!({
241 "Properties": {
242 "AuthMethod": "RPS",
243 "SiteName": "user.auth.xboxlive.com",
244 "RpsTicket": &format!("d={}", microsoft_access_token.as_ref())
245 },
246 "RelyingParty": "http://auth.xboxlive.com",
247 "TokenType": "JWT"
248 });
249 let response = self
250 .http_client
251 .post(XBOX_USER_AUTHENTICATE)
252 .json(&xbox_authenticate_json)
253 .send()
254 .await?;
255 response.error_for_status_ref()?;
256 let xbox_resp: XboxLiveAuthenticationResponse = response.json().await?;
257 let xbox_token = xbox_resp.token;
258 let user_hash = xbox_resp
259 .display_claims
260 .get("xui")
261 .ok_or(MinecraftAuthorizationError::MissingClaims)?
262 .first()
263 .ok_or(MinecraftAuthorizationError::MissingClaims)?
264 .get("uhs")
265 .ok_or(MinecraftAuthorizationError::MissingClaims)?
266 .to_owned();
267 Ok((xbox_token, user_hash))
268 }
269}