helixlauncher_core/
auth.rs1mod account;
2mod request_structs;
3
4use reqwest::{Client, StatusCode};
5use serde_json::json;
6use std::time::Duration;
7use thiserror::Error;
8
9use account::Account;
10use request_structs::*;
11
12#[derive(Error, Debug)]
13pub enum AuthenticationError {
14 #[error(transparent)]
15 ReqwestError(#[from] reqwest::Error),
16
17 #[error("OAuth request was declined by the user.")]
18 OAuthDeclined,
19
20 #[error("OAuth request timed out, the user took too long.")]
21 OAuthExpired,
22
23 #[error("A request returned an unexpected result.")]
24 Unexpected,
25}
26
27pub struct MinecraftAuthenticator {
28 client_id: String,
29 reqwest_client: Client,
30}
31
32impl MinecraftAuthenticator {
33 pub fn new<I: Into<String>>(client_id: I) -> Self {
34 Self {
35 client_id: client_id.into(),
36 reqwest_client: Client::new(),
37 }
38 }
39
40 pub async fn initial_auth(
41 &self,
42 callback: fn(code: String, uri: String, message: String),
43 ) -> Result<Account, AuthenticationError> {
44 let code_response: CodeResponse = self
45 .reqwest_client
46 .get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
47 .form(&[
48 ("client_id", self.client_id.as_str()),
49 ("scope", "XboxLive.signin offline_access"),
50 ])
51 .send()
52 .await?
53 .json()
54 .await?;
55
56 callback(
57 code_response.user_code,
58 code_response.verification_uri,
59 code_response.message,
60 );
61
62 loop {
63 tokio::time::sleep(Duration::from_secs(code_response.interval as u64)).await;
64
65 let grant_response = self
66 .reqwest_client
67 .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
68 .form(&[
69 ("client_id", self.client_id.as_str()),
70 ("device_code", code_response.device_code.as_str()),
71 (
72 "grant_type",
73 &"urn:ietf:params:oauth:grant-type:device_code",
74 ),
75 ])
76 .send()
77 .await?;
78
79 return if grant_response.status() == StatusCode::OK {
80 let poll_success: GrantSuccessResponse = grant_response.json().await?;
81
82 self.authenticate(poll_success.access_token, poll_success.refresh_token)
83 .await
84 } else {
85 let poll_error: GrantFailureResponse = grant_response.json().await?;
86
87 match poll_error.error.as_str() {
88 "authorization_pending" => continue,
89 "authorization_declined" => Err(AuthenticationError::OAuthDeclined),
90 "expired_token" => Err(AuthenticationError::OAuthExpired),
91 _ => Err(AuthenticationError::Unexpected),
92 }
93 };
94 }
95 }
96
97 pub async fn refresh(&self, account: Account) -> Result<Account, AuthenticationError> {
98 let grant_response: GrantSuccessResponse = self
99 .reqwest_client
100 .post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
101 .form(&[
102 ("client_id", self.client_id.as_str()),
103 ("grant_type", "refresh_token"),
104 ("scope", "XboxLive.signin offline_access"),
105 ("refresh_token", account.refresh_token.as_str()),
106 ])
107 .send()
108 .await?
109 .json()
110 .await?;
111
112 self.authenticate(grant_response.access_token, grant_response.refresh_token)
113 .await
114 }
115
116 async fn authenticate(
117 &self,
118 access_token: String,
119 refresh_token: String,
120 ) -> Result<Account, AuthenticationError> {
121 let xbox_live_response: XboxLiveResponse = self
122 .reqwest_client
123 .post("https://user.auth.xboxlive.com/user/authenticate")
124 .json(&json!({
125 "Properties": {
126 "AuthMethod": "RPS",
127 "SiteName": "user.auth.xboxlive.com",
128 "RpsTicket": &format!("d={access_token}")
129 },
130 "RelyingParty": "http://auth.xboxlive.com",
131 "TokenType": "JWT"
132 }))
133 .send()
134 .await?
135 .json()
136 .await?;
137
138 let xsts_response: XboxLiveResponse = self
140 .reqwest_client
141 .post("https://xsts.auth.xboxlive.com/xsts/authorize")
142 .json(&json!({
143 "Properties": {
144 "SandboxId": "RETAIL",
145 "UserTokens": [ xbox_live_response.token ]
146 },
147 "RelyingParty": "rp://api.minecraftservices.com/",
148 "TokenType": "JWT"
149 }))
150 .send()
151 .await?
152 .json()
153 .await?;
154
155 let identity_token = format!(
156 "XBL3.0 x={};{}",
157 xsts_response.display_claims["xui"][0]["uhs"], xsts_response.token
158 );
159 let minecraft_response: MinecraftResponse = self
160 .reqwest_client
161 .post("https://api.minecraftservices.com/authentication/login_with_xbox")
162 .json(&json!({ "identityToken": &identity_token }))
163 .send()
164 .await?
165 .json()
166 .await?;
167
168 let profile_response: ProfileResponse = self
169 .reqwest_client
170 .get("https://api.minecraftservices.com/minecraft/profile")
171 .header(
172 "Authorization",
173 format!("Bearer {}", minecraft_response.access_token),
174 )
175 .send()
176 .await?
177 .json()
178 .await?;
179
180 Ok(Account {
181 uuid: profile_response.id,
182 username: profile_response.name,
183 refresh_token,
184 token: minecraft_response.access_token,
185 })
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use crate::auth::MinecraftAuthenticator;
192
193 #[tokio::test]
194 async fn test() {
195 let authenticator = MinecraftAuthenticator::new("1d644380-5a23-4a84-89c3-5d29615fbac2");
196
197 let account = authenticator
198 .initial_auth(|_, _, message| println!("{}", message))
199 .await
200 .unwrap();
201
202 println!("{}", serde_json::to_string(&account).unwrap());
203
204 let account = authenticator.refresh(account).await.unwrap();
205
206 println!("{}", serde_json::to_string(&account).unwrap());
207 }
208}