OneLauncher-Cracked/core/src/auth/microsoft_auth.rs
2024-03-24 11:58:54 +01:00

303 lines
7.9 KiB
Rust

use std::{
io::{Read, Write},
net::{SocketAddr, TcpListener, TcpStream},
};
use serde_json::json;
use thiserror::Error;
use crate::{constants::{self, CLIENT_ID}, utils::http::create_client, PolyError, PolyResult};
use super::{Account, AuthenticationError, AuthenticationMethod};
// TODO: store auth keys with https://beta.tauri.app/features/stronghold/
// TODO: store auth key public in prisma for easy access
#[derive(Debug, Error)]
pub enum MicrosoftAuthError {
#[error("failed to get Minecraft access token")]
MSAccessToken,
#[error("failed to get XSTS token")]
XSTSToken,
#[error("failed to get XBL token")]
XBLToken,
#[error("failed to get XBL user hash")]
XBLUserHash,
#[error("this account doesn't have an xbox account, sign in through minecraft.net and try again")]
NoXBoxAccount,
#[error("this account is from a country where xbox live is not available")]
XBoxLiveBlocked,
#[error("this account needs adult verification on the xbox page")]
AdultVerification,
#[error("this account is a child, ask an adult to add this account to a Family")]
ChildAccount,
#[error("unknown xbox live error: {0}")]
UnknownError(u64),
}
pub struct MicrosoftAuthenticationMethod;
impl AuthenticationMethod for MicrosoftAuthenticationMethod {
async fn auth<F>(stage: F) -> PolyResult<Account>
where
F: Fn(String, u8, bool) -> (),
{
stage("Authenticating with Microsoft".into(), 0, false);
let msa_code: String = msa_code().await?;
stage("Authenticating with Microsoft".into(), 1, false);
let msa_token: String = msa_code_to_token(msa_code).await?;
stage("Authenticating with Xbox Live".into(), 2, false);
let (xbl_token, user_hash): (String, String) = auth_xbl(msa_token).await?;
stage("Retrieving XSTS token".into(), 3, false);
let xsts_token: String = auth_xsts(xbl_token).await?;
stage("Authenticating with Minecraft".into(), 4, false);
let access_token: String = auth_minecraft(xsts_token, user_hash).await?;
stage("Retrieving Minecraft profile".into(), 5, true);
let account: Account = Self::get_profile(access_token).await?;
Ok(account)
}
}
async fn auth_minecraft(xsts_token: String, user_hash: String) -> PolyResult<String> {
let response = create_client()?
.post("https://api.minecraftservices.com/authentication/login_with_xbox")
.json(&json!({
"identityToken": format!("XBL3.0 x={};{}", user_hash, xsts_token)
}))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
let access_token = match response.get("access_token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::MSAccessToken,
)))
}
};
Ok(access_token.to_string())
}
async fn auth_xsts(token: String) -> PolyResult<String> {
let response = create_client()?
.post("https://xsts.auth.xboxlive.com/xsts/authorize")
.json(&json!({
"Properties": {
"SandboxId": "RETAIL",
"UserTokens": [
token
]
},
"RelyingParty": "rp://api.minecraftservices.com/",
"TokenType": "JWT"
}))
.send()
.await?;
if response.status().as_u16() == 401 {
let error_code = response.json::<serde_json::Value>().await?["XErr"]
.as_u64()
.unwrap();
match error_code {
2148916233 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::NoXBoxAccount,
)))
}
2148916235 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBoxLiveBlocked,
)))
}
2148916236 | 2148916237 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::AdultVerification,
)))
}
2148916238 => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::ChildAccount,
)))
}
_ => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::UnknownError(error_code),
)))
}
}
}
let response = response.json::<serde_json::Value>().await?;
let xsts_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XSTSToken,
)))
}
};
Ok(xsts_token.to_string())
}
async fn auth_xbl(code: String) -> PolyResult<(String, String)> {
let response = create_client()?
.post("https://user.auth.xboxlive.com/user/authenticate")
.json(&json!({
"Properties": {
"AuthMethod": "RPS",
"SiteName": "user.auth.xboxlive.com",
"RpsTicket": &format!("d={}", code)
},
"RelyingParty": "http://auth.xboxlive.com",
"TokenType": "JWT"
}))
.send()
.await?;
let response = response.json::<serde_json::Value>().await?;
let xbl_token = match response.get("Token") {
Some(token) => token.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBLToken,
)))
}
};
let user_hash = match response.get("DisplayClaims").and_then(|claims| {
claims
.get("xui")
.and_then(|xui| xui.get(0).and_then(|xui| xui.get("uhs")))
}) {
Some(hash) => hash.as_str().unwrap(),
None => {
return Err(PolyError::AuthError(AuthenticationError::MicrosoftError(
MicrosoftAuthError::XBLUserHash,
)))
}
};
Ok((xbl_token.to_string(), user_hash.to_string()))
}
async fn msa_code_to_token(code: String) -> PolyResult<String> {
let token = create_client()?
.post("https://login.live.com/oauth20_token.srf")
.form(&[
("client_id", CLIENT_ID),
("redirect_uri", &format!("http://localhost:{}/", constants::MSA_PORT)),
("code", &code),
("grant_type", "authorization_code"),
])
.send()
.await?
.json::<serde_json::Value>()
.await?;
Ok(token["access_token"].as_str().unwrap().to_string())
}
async fn msa_code() -> PolyResult<String> {
let url = format!(
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={}&response_type=code&redirect_uri=http://localhost:{}/&response_mode=query&scope=XboxLive.signin%20offline_access&prompt=consent",
CLIENT_ID,
constants::MSA_PORT
);
open::that(url)?;
let mut token: String = String::new();
let listener = TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], constants::MSA_PORT)))?;
println!("Started local server for MSA on port {}", constants::MSA_PORT);
for conn in listener.incoming() {
match conn {
Err(err) => return Err(err.into()),
Ok(stream) => {
if let Some(code) = handle_connection(stream) {
token = code;
break;
}
}
}
}
Ok(token)
}
fn handle_connection(mut stream: TcpStream) -> Option<String> {
let mut buffer = [0; 512];
if let Err(err) = stream.read(&mut buffer) {
eprintln!("Failed to read from stream: {}", err);
return None;
}
let request = String::from_utf8_lossy(&buffer[..]);
let url = request.split_whitespace().nth(1)?;
let code = url
.split('?')
.nth(1)?
.split('&')
.find(|s| s.starts_with("code="))?
.split_once('=')
.unwrap()
.1;
let response = r"
<!DOCTYPE html>
<html>
<head>
<title>Microsoft Authentication</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
div {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
opacity: 0;
animation: fade-in 0.5s ease-in-out 0.5s forwards;
}
@keyframes fade-in {
0% { opacity: 0; }
100% { opacity: 1; }
}
</style>
</head>
<body>
<div>
<h1>Microsoft Authentication</h1>
<p>Authentication successful. You can close this window now.</p>
</div>
</body>
</html>
";
stream
.write_all(
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
response.len(),
response
)
.as_bytes(),
)
.unwrap();
Some(code.to_string())
}