1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
//! 传统微软登录模块,通过模仿 Minecraft 官方启动器来接收授权令牌回调链接完成登录验证

use serde::Deserialize;

use crate::{
    auth::{parse_head_skin, structs::AuthMethod},
    password::Password,
    prelude::*,
};

/**
    Minecraft 官方启动器的微软登录链接

    通过捕获从此链接跳转过来的
    `https://login.live.com/oauth20_desktop.srf?code=[ANYCODE]&lc=1033`
    链接并传入 [`start_auth`] 来获取登录令牌
*/
pub const MICROSOFT_URL: &str = "https://login.live.com/oauth20_authorize.srf?client_id=00000000402b5328&response_type=code&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf";

/**
  微软的登录令牌 API 接口
  用来请求或续期登录令牌
*/
pub const MICROSOFT_TOKEN_URL: &str = "https://login.live.com/oauth20_token.srf";

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
struct OAuth20TokenResponse {
    // token_type: String,
    // expires_in: usize,
    // scope: String,
    pub error: String,
    pub access_token: Password,
    pub refresh_token: String,
    // user_id: String,
    // foci: String,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct XBoxAuthResponse {
    #[serde(rename = "Token")]
    pub token: String,
    #[serde(rename = "DisplayClaims")]
    pub display_claims: XBoxAuthResponse1,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct XBoxAuthResponse1 {
    pub xui: Vec<XBoxAuthResponse2>,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct XBoxAuthResponse2 {
    pub uhs: String,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct MinecraftStoreResponse {
    pub items: Vec<serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct MinecraftXBoxLoginResponse {
    // pub username: String,
    pub access_token: Password,
    // pub token_type: String,
}

#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub(super) struct MinecraftXBoxProfileResponse {
    pub id: String,
    pub name: String,
    pub error: String,
    pub skins: Vec<MinecraftXBoxProfileResponse1>,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct MinecraftXBoxProfileResponse1 {
    // pub id: String,
    pub state: String,
    pub url: String,
}

#[derive(Debug, Clone, Deserialize)]
pub(super) struct XBoxPresenceRescord {
    // pub xuid: String,
}

/// 获取 XUID,用途不明,但是在新版本的 Minecraft 有发现需要使用这个 XUID 的地方
pub async fn get_xuid(userhash: &str, token: &str) -> DynResult<String> {
    let res = crate::http::get("https://userpresence.xboxlive.com/users/me?level=user")
        .header("Authorization", format!("XBL3.0 x={userhash};{token}"))
        .header("x-xbl-contract-version", "3.2")
        .header("Accept", "application/json")
        .header("Accept-Language", "zh-CN")
        .header("Host", "userpresence.xboxlive.com")
        .recv_string()
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    Ok(res)
}

/// 请求一个新令牌,或者续期一个令牌
///
/// 如果请求一个新令牌,则 credit 为从登录页面里传来的 code 请求字符串
///
/// 如果续期一个令牌,则 credit 为需要续期的旧令牌
pub async fn request_token(credit: &str, is_refresh: bool) -> DynResult<(Password, String)> {
    let body = format!(
        "client_id=00000000402b5328&{}={}&grant_type={}&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf&scope=service%3A%3Auser.auth.xboxlive.com%3A%3AMBI_SSL",
        if is_refresh { "refresh_token" } else { "code" }, // Grant Tag
        credit,
        if is_refresh { "refresh_token" } else { "authorization_code" }, // Grant Type
    );
    let res: OAuth20TokenResponse = crate::http::post(MICROSOFT_TOKEN_URL)
        .header("Content-Type", "application/x-www-form-urlencoded")
        .body(body.as_bytes())
        .recv_json()
        .await
        .map_err(|e| anyhow::anyhow!(e))?;
    anyhow::ensure!(
        res.error.is_empty(),
        "{}令牌失败: {}",
        if is_refresh { "刷新" } else { "请求" },
        res.error
    );
    Ok((res.access_token, res.refresh_token))
}

/// 根据微软登录传回的访问令牌 access_token 返回 user_hash 和 xsts_token
///
/// 传递给 [`get_mojang_access_token`] 进行下一步验证
pub async fn get_userhash_and_token(access_token: &str) -> DynResult<(String, String)> {
    // tracing::trace!("Getting xbox auth body");
    let xbox_auth_body = format!("{{\"Properties\":{{\"AuthMethod\":\"RPS\",\"SiteName\":\"user.auth.xboxlive.com\",\"RpsTicket\":\"{access_token}\"}},\"RelyingParty\":\"http://auth.xboxlive.com\",\"TokenType\":\"JWT\"}}");
    let xbox_auth_resp: XBoxAuthResponse =
        crate::http::post("https://user.auth.xboxlive.com/user/authenticate")
            .header("Content-Type", "application/json")
            .header("Accept", "application/json")
            .body(xbox_auth_body.as_bytes())
            .await
            .map_err(|e| anyhow::anyhow!(e))?
            .body_json()
            .await
            .map_err(|e| anyhow::anyhow!(e))?;
    let token = xbox_auth_resp.token.to_owned();
    if let Some(uhs) = xbox_auth_resp.display_claims.xui.first() {
        let uhs = uhs.uhs.to_owned();
        let xsts_body = format!("{{\"Properties\":{{\"SandboxId\":\"RETAIL\",\"UserTokens\":[\"{token}\"]}},\"RelyingParty\":\"rp://api.minecraftservices.com/\",\"TokenType\":\"JWT\"}}");
        tracing::trace!("Getting xbox xsts token");
        let xsts_resp: XBoxAuthResponse =
            crate::http::post("https://xsts.auth.xboxlive.com/xsts/authorize")
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .body(xsts_body.as_bytes())
                .await
                .map_err(|e| anyhow::anyhow!(e))?
                .body_json()
                .await
                .map_err(|e| anyhow::anyhow!(e))?;
        let xsts_token = xsts_resp.token;
        Ok((uhs, xsts_token))
    } else {
        anyhow::bail!("获取 UserHash 失败")
    }
}

/// 通过 [`get_userhash_and_token`] 返回的 `userhash` 和 `xsts_token` 获取 Mojang 的访问令牌
///
/// 在拥有 Minecraft 游戏的情况下,此令牌可用于正版启动游戏
pub async fn get_mojang_access_token(uhs: &str, xsts_token: &str) -> DynResult<Password> {
    if !uhs.is_empty() && !xsts_token.is_empty() {
        // tracing::trace!("Getting mojang access token");
        let minecraft_xbox_body = format!("{{\"identityToken\":\"XBL3.0 x={uhs};{xsts_token}\"}}");
        let minecraft_xbox_resp =
            crate::http::post("https://api.minecraftservices.com/authentication/login_with_xbox")
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .body(minecraft_xbox_body.as_bytes())
                .await
                .map_err(|e| anyhow::anyhow!(e))?
                .body_string()
                .await
                .map_err(|e| anyhow::anyhow!(e))?;
        let minecraft_xbox_resp: MinecraftXBoxLoginResponse =
            serde_json::from_str(&minecraft_xbox_resp)?;
        // tracing::trace!("Getting minecraft access token");
        let access_token = minecraft_xbox_resp.access_token;
        Ok(access_token)
    } else {
        Ok(Password::default())
    }
}

/// 刷新登录令牌,如刷新成功则可将更新后的用户继续用于正版启动
pub async fn refresh_auth(method: &mut AuthMethod) -> DynResult {
    match method {
        AuthMethod::Microsoft {
            access_token,
            refresh_token,
            ..
        } => {
            let (new_access_token, new_refresh_token) =
                request_token(refresh_token.as_str(), true).await?;
            let (uhs, xsts_token) = get_userhash_and_token(&new_access_token).await?;
            let new_access_token = get_mojang_access_token(&uhs, &xsts_token).await?;
            anyhow::ensure!(
                !new_access_token.is_empty(),
                "刷新令牌失败: {}",
                new_access_token
            );
            *access_token = new_access_token;
            *refresh_token = new_refresh_token.into();
        }
        _ => {
            anyhow::bail!("不支持的方法");
        }
    }
    Ok(())
}

/// 执行微软登录,需要形如 `https://login.live.com/oauth20_desktop.srf?code=[ANYCODE]&lc=1033` 的链接作为参数
pub async fn start_auth(_ctx: Option<impl Reporter>, url: &str) -> DynResult<AuthMethod> {
    let url = url.parse::<url::Url>()?;
    if let Some((_, code)) = url.query_pairs().find(|a| a.0 == "code") {
        let (access_token, refresh_token) = request_token(&code, false).await?;
        let (uhs, xsts_token) = get_userhash_and_token(&access_token).await?;
        let xuid = get_xuid(&uhs, &xsts_token).await?;
        let access_token = get_mojang_access_token(&uhs, &xsts_token).await?;
        if access_token.is_empty() {
            return Err(anyhow::anyhow!("获取令牌失败"));
        } else {
            let mcstore_resp: MinecraftStoreResponse =
                crate::http::get("https://api.minecraftservices.com/entitlements/mcstore")
                    .header(
                        "Authorization",
                        &format!("Bearer {}", access_token.as_string()),
                    )
                    .await
                    .map_err(|e| anyhow::anyhow!(e))?
                    .body_json()
                    .await
                    .map_err(|e| anyhow::anyhow!(e))?;
            if mcstore_resp.items.is_empty() {
                anyhow::bail!(
                    "没有在已购项目中找到 Minecraft!请检查你的账户是否已购买 Minecraft!"
                );
            }
            let profile_resp: MinecraftXBoxProfileResponse =
                crate::http::get("https://api.minecraftservices.com/minecraft/profile")
                    .header(
                        "Authorization",
                        &format!("Bearer {}", access_token.as_string()),
                    )
                    .await
                    .map_err(|e| anyhow::anyhow!(e))?
                    .body_json()
                    .await
                    .map_err(|e| anyhow::anyhow!(e))?;
            if profile_resp.error.is_empty() {
                if let Some(skin) = profile_resp.skins.iter().find(|a| a.state == "ACTIVE") {
                    let skin_data = crate::http::get(&skin.url)
                        .await
                        .map_err(|e| anyhow::anyhow!(e))?
                        .body_bytes()
                        .await
                        .map_err(|e| anyhow::anyhow!(e))?;
                    let (head_skin, hat_skin) = parse_head_skin(skin_data)?;
                    tracing::trace!("Successfully authed!");
                    return Ok(AuthMethod::Microsoft {
                        access_token,
                        refresh_token: refresh_token.into(),
                        xuid,
                        head_skin,
                        hat_skin,
                        player_name: profile_resp.name,
                        uuid: profile_resp.id,
                    });
                }
            } else {
                anyhow::bail!(
                    "没有在账户中找到 Minecraft 账户信息!请检查你的账户是否已购买 Minecraft!"
                );
            }
        }
    }
    anyhow::bail!("链接不合法");
}