You've already forked AstralRinth
forked from didirus/AstralRinth
Switch to official launcher auth (#1118)
* Switch to official launcher auth * add debug info * Fix build
This commit is contained in:
@@ -1,132 +0,0 @@
|
||||
//! Authentication flow interface
|
||||
use crate::{
|
||||
hydra::{self, init::DeviceLoginSuccess},
|
||||
launcher::auth as inner,
|
||||
State,
|
||||
};
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::state::AuthTask;
|
||||
pub use inner::Credentials;
|
||||
|
||||
/// Authenticate a user with Hydra - part 1
|
||||
/// This begins the authentication flow quasi-synchronously, returning a URL
|
||||
/// This can be used in conjunction with 'authenticate_await_complete_flow'
|
||||
/// to call authenticate and call the flow from the frontend.
|
||||
/// Visit the URL in a browser, then call and await 'authenticate_await_complete_flow'.
|
||||
pub async fn authenticate_begin_flow() -> crate::Result<DeviceLoginSuccess> {
|
||||
let url = AuthTask::begin_auth().await?;
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Authenticate a user with Hydra - part 2
|
||||
/// This completes the authentication flow quasi-synchronously, returning the credentials
|
||||
/// This can be used in conjunction with 'authenticate_begin_flow'
|
||||
/// to call authenticate and call the flow from the frontend.
|
||||
pub async fn authenticate_await_complete_flow() -> crate::Result<Credentials> {
|
||||
let credentials = AuthTask::await_auth_completion().await?;
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// Cancels the active authentication flow
|
||||
pub async fn cancel_flow() -> crate::Result<()> {
|
||||
AuthTask::cancel().await
|
||||
}
|
||||
|
||||
/// Refresh some credentials using Hydra, if needed
|
||||
/// This is the primary desired way to get credentials, as it will also refresh them.
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn refresh(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
let mut credentials = users.get(user).ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(
|
||||
"You are not logged in with a Minecraft account!".to_string(),
|
||||
)
|
||||
.as_error()
|
||||
})?;
|
||||
|
||||
let offline = *state.offline.read().await;
|
||||
|
||||
if !offline {
|
||||
let fetch_semaphore: &crate::util::fetch::FetchSemaphore =
|
||||
&state.fetch_semaphore;
|
||||
if Utc::now() > credentials.expires
|
||||
&& inner::refresh_credentials(&mut credentials, fetch_semaphore)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
users.remove(credentials.id).await?;
|
||||
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"Please re-authenticate with your Minecraft account!"
|
||||
.to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
|
||||
// Update player info from bearer token
|
||||
let player_info =
|
||||
hydra::stages::player_info::fetch_info(&credentials.access_token)
|
||||
.await
|
||||
.map_err(|_err| {
|
||||
crate::ErrorKind::HydraError(
|
||||
"No Minecraft account for your profile. Please try again or contact support in our Discord for help!".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
credentials.username = player_info.name;
|
||||
users.insert(&credentials).await?;
|
||||
}
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
/// Remove a user account from the database
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
if state.settings.read().await.default_user == Some(user) {
|
||||
let mut settings = state.settings.write().await;
|
||||
settings.default_user = users.0.values().next().map(|it| it.id);
|
||||
}
|
||||
|
||||
users.remove(user).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a user exists in Theseus
|
||||
#[tracing::instrument]
|
||||
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
|
||||
Ok(users.contains(user))
|
||||
}
|
||||
|
||||
/// Get a copy of the list of all user credentials
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Vec<Credentials>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
Ok(users.0.values().cloned().collect())
|
||||
}
|
||||
|
||||
/// Get a specific user by user ID
|
||||
/// Prefer to use 'refresh' instead of this function
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
let user = users.get(user).ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
})?;
|
||||
Ok(user)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
//! Main authentication flow for Hydra
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::prelude::Credentials;
|
||||
|
||||
use super::stages::{
|
||||
bearer_token, player_info, poll_response, xbl_signin, xsts_token,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OauthFailure {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
pub struct SuccessfulLogin {
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires_after: i64,
|
||||
}
|
||||
|
||||
pub async fn wait_finish(device_code: String) -> crate::Result<Credentials> {
|
||||
// Loop, polling for response from Microsoft
|
||||
let oauth = poll_response::poll_response(device_code).await?;
|
||||
|
||||
// Get xbl token from oauth token
|
||||
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
|
||||
|
||||
// Get xsts token from xbl token
|
||||
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
|
||||
|
||||
match xsts_response {
|
||||
xsts_token::XSTSResponse::Unauthorized(err) => {
|
||||
Err(crate::ErrorKind::HydraError(format!(
|
||||
"Error getting XBox Live token: {}",
|
||||
err
|
||||
))
|
||||
.as_error())
|
||||
}
|
||||
xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
||||
// Get xsts bearer token from xsts token
|
||||
let (bearer_token, expires_in) =
|
||||
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Error getting bearer token: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
// Get player info from bearer token
|
||||
let player_info = player_info::fetch_info(&bearer_token).await.map_err(|_err| {
|
||||
crate::ErrorKind::HydraError("No Minecraft account for profile. Make sure you own the game and have set a username through the official Minecraft launcher."
|
||||
.to_string())
|
||||
})?;
|
||||
|
||||
// Create credentials
|
||||
let credentials = Credentials::new(
|
||||
uuid::Uuid::parse_str(&player_info.id)?, // get uuid from player_info.id which is a String
|
||||
player_info.name,
|
||||
bearer_token,
|
||||
oauth.refresh_token,
|
||||
chrono::Utc::now() + chrono::Duration::seconds(expires_in),
|
||||
);
|
||||
|
||||
// Put credentials into state
|
||||
let state = crate::State::get().await?;
|
||||
{
|
||||
let mut users = state.users.write().await;
|
||||
users.insert(&credentials).await?;
|
||||
}
|
||||
|
||||
if state.settings.read().await.default_user.is_none() {
|
||||
let mut settings = state.settings.write().await;
|
||||
settings.default_user = Some(credentials.id);
|
||||
}
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
//! Login route for Hydra, redirects to the Microsoft login page before going to the redirect route
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{hydra::MicrosoftError, util::fetch::REQWEST_CLIENT};
|
||||
|
||||
use super::{stages::auth_retry, MICROSOFT_CLIENT_ID};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct DeviceLoginSuccess {
|
||||
pub device_code: String,
|
||||
pub user_code: String,
|
||||
pub verification_uri: String,
|
||||
pub expires_in: u64,
|
||||
pub interval: u64,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub async fn init() -> crate::Result<DeviceLoginSuccess> {
|
||||
// Get the initial URL
|
||||
// Get device code
|
||||
// Define the parameters
|
||||
|
||||
// urlencoding::encode("XboxLive.signin offline_access"));
|
||||
let resp = auth_retry(|| REQWEST_CLIENT.get("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode")
|
||||
.header("Content-Length", "0")
|
||||
.query(&[
|
||||
("client_id", MICROSOFT_CLIENT_ID),
|
||||
(
|
||||
"scope",
|
||||
"XboxLive.signin XboxLive.offline_access profile openid email",
|
||||
),
|
||||
])
|
||||
.send()
|
||||
).await?;
|
||||
|
||||
match resp.status() {
|
||||
reqwest::StatusCode::OK => Ok(resp.json().await?),
|
||||
_ => {
|
||||
let microsoft_error = resp.json::<MicrosoftError>().await?;
|
||||
Err(crate::ErrorKind::HydraError(format!(
|
||||
"Error from Microsoft: {:?}",
|
||||
microsoft_error.error_description
|
||||
))
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
pub mod complete;
|
||||
pub mod init;
|
||||
pub mod refresh;
|
||||
pub(crate) mod stages;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
const MICROSOFT_CLIENT_ID: &str = "c4502edb-87c6-40cb-b595-64a280cf8906";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MicrosoftError {
|
||||
pub error: String,
|
||||
pub error_description: String,
|
||||
pub error_codes: Vec<u64>,
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
|
||||
util::fetch::REQWEST_CLIENT,
|
||||
};
|
||||
|
||||
use super::stages::auth_retry;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OauthSuccess {
|
||||
pub token_type: String,
|
||||
pub scope: String,
|
||||
pub expires_in: i64,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
pub async fn refresh(refresh_token: String) -> crate::Result<OauthSuccess> {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("grant_type", "refresh_token");
|
||||
params.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
params.insert("refresh_token", &refresh_token);
|
||||
params.insert(
|
||||
"redirect_uri",
|
||||
"https://login.microsoftonline.com/common/oauth2/nativeclient",
|
||||
);
|
||||
|
||||
// Poll the URL in a loop until we are successful.
|
||||
// On an authorization_pending response, wait 5 seconds and try again.
|
||||
let resp =
|
||||
auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post("https://login.microsoftonline.com/consumers/oauth2/v2.0/token")
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.form(¶ms)
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
match resp.status() {
|
||||
StatusCode::OK => {
|
||||
let oauth = resp.json::<OauthSuccess>().await.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Could not decipher successful response: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
Ok(oauth)
|
||||
}
|
||||
_ => {
|
||||
let failure =
|
||||
resp.json::<MicrosoftError>().await.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Could not decipher failure response: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
Err(crate::ErrorKind::HydraError(format!(
|
||||
"Error refreshing token: {}",
|
||||
failure.error
|
||||
))
|
||||
.as_error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
|
||||
use super::auth_retry;
|
||||
|
||||
const MCSERVICES_AUTH_URL: &str =
|
||||
"https://api.minecraftservices.com/authentication/login_with_xbox";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BearerTokenResponse {
|
||||
access_token: String,
|
||||
expires_in: i64,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn fetch_bearer(
|
||||
token: &str,
|
||||
uhs: &str,
|
||||
) -> crate::Result<(String, i64)> {
|
||||
let body = auth_retry(|| {
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(MCSERVICES_AUTH_URL)
|
||||
.header("Accept", "application/json")
|
||||
.json(&json!({
|
||||
"identityToken": format!("XBL3.0 x={};{}", uhs, token),
|
||||
}))
|
||||
.send()
|
||||
})
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
serde_json::from_str::<BearerTokenResponse>(&body)
|
||||
.map(|x| (x.access_token, x.expires_in))
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Response didn't contain valid bearer token. body: {body}"
|
||||
))
|
||||
.into()
|
||||
})
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
//! MSA authentication stages
|
||||
|
||||
use futures::Future;
|
||||
use reqwest::Response;
|
||||
|
||||
const RETRY_COUNT: usize = 9; // Does command 3 times
|
||||
const RETRY_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
|
||||
|
||||
pub mod bearer_token;
|
||||
pub mod player_info;
|
||||
pub mod poll_response;
|
||||
pub mod xbl_signin;
|
||||
pub mod xsts_token;
|
||||
|
||||
#[tracing::instrument(skip(reqwest_request))]
|
||||
pub async fn auth_retry<F>(
|
||||
reqwest_request: impl Fn() -> F,
|
||||
) -> crate::Result<reqwest::Response>
|
||||
where
|
||||
F: Future<Output = Result<Response, reqwest::Error>>,
|
||||
{
|
||||
let mut resp = reqwest_request().await?;
|
||||
for i in 0..RETRY_COUNT {
|
||||
if resp.status().is_success() {
|
||||
break;
|
||||
}
|
||||
tracing::debug!(
|
||||
"Request failed with status code {}, retrying...",
|
||||
resp.status()
|
||||
);
|
||||
if i < RETRY_COUNT - 1 {
|
||||
tokio::time::sleep(RETRY_WAIT).await;
|
||||
}
|
||||
resp = reqwest_request().await?;
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
//! Fetch player info for display
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::util::fetch::REQWEST_CLIENT;
|
||||
|
||||
use super::auth_retry;
|
||||
|
||||
const PROFILE_URL: &str = "https://api.minecraftservices.com/minecraft/profile";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PlayerInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Default for PlayerInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: "606e2ff0ed7748429d6ce1d3321c7838".to_string(),
|
||||
name: String::from("Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn fetch_info(token: &str) -> crate::Result<PlayerInfo> {
|
||||
auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.get("https://api.minecraftservices.com/entitlements/mcstore")
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.get(PROFILE_URL)
|
||||
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
let resp = response.error_for_status()?.json().await?;
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
hydra::{MicrosoftError, MICROSOFT_CLIENT_ID},
|
||||
util::fetch::REQWEST_CLIENT,
|
||||
};
|
||||
|
||||
use super::auth_retry;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OauthSuccess {
|
||||
pub token_type: String,
|
||||
pub scope: String,
|
||||
pub expires_in: i64,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn poll_response(device_code: String) -> crate::Result<OauthSuccess> {
|
||||
let mut params = HashMap::new();
|
||||
params.insert("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
||||
params.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
params.insert("device_code", &device_code);
|
||||
params.insert(
|
||||
"scope",
|
||||
"XboxLive.signin XboxLive.offline_access profile openid email",
|
||||
);
|
||||
|
||||
// Poll the URL in a loop until we are successful.
|
||||
// On an authorization_pending response, wait 5 seconds and try again.
|
||||
loop {
|
||||
let resp = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post(
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token",
|
||||
)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
|
||||
match resp.status() {
|
||||
StatusCode::OK => {
|
||||
let oauth =
|
||||
resp.json::<OauthSuccess>().await.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Could not decipher successful response: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
return Ok(oauth);
|
||||
}
|
||||
_ => {
|
||||
let failure =
|
||||
resp.json::<MicrosoftError>().await.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Could not decipher failure response: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
match failure.error.as_str() {
|
||||
"authorization_pending" => {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2))
|
||||
.await;
|
||||
}
|
||||
"authorization_declined" => {
|
||||
return Err(crate::ErrorKind::HydraError(
|
||||
"Authorization declined".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
"expired_token" => {
|
||||
return Err(crate::ErrorKind::HydraError(
|
||||
"Device code expired".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
"bad_verification_code" => {
|
||||
return Err(crate::ErrorKind::HydraError(
|
||||
"Invalid device code".to_string(),
|
||||
)
|
||||
.as_error());
|
||||
}
|
||||
_ => {
|
||||
return Err(crate::ErrorKind::HydraError(format!(
|
||||
"Unknown error: {}",
|
||||
failure.error
|
||||
))
|
||||
.as_error());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::util::fetch::REQWEST_CLIENT;
|
||||
|
||||
use super::auth_retry;
|
||||
|
||||
const XBL_AUTH_URL: &str = "https://user.auth.xboxlive.com/user/authenticate";
|
||||
|
||||
// Deserialization
|
||||
pub struct XBLLogin {
|
||||
pub token: String,
|
||||
pub uhs: String,
|
||||
}
|
||||
|
||||
// Impl
|
||||
#[tracing::instrument]
|
||||
pub async fn login_xbl(token: &str) -> crate::Result<XBLLogin> {
|
||||
let response = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post(XBL_AUTH_URL)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
"AuthMethod": "RPS",
|
||||
"SiteName": "user.auth.xboxlive.com",
|
||||
"RpsTicket": format!("d={token}")
|
||||
},
|
||||
"RelyingParty": "http://auth.xboxlive.com",
|
||||
"TokenType": "JWT"
|
||||
}))
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
let body = response.text().await?;
|
||||
|
||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
||||
let token = Some(&json)
|
||||
.and_then(|it| it.get("Token")?.as_str().map(String::from))
|
||||
.ok_or(crate::ErrorKind::HydraError(
|
||||
"XBL response didn't contain valid token".to_string(),
|
||||
))?;
|
||||
let uhs = Some(&json)
|
||||
.and_then(|it| {
|
||||
it.get("DisplayClaims")?
|
||||
.get("xui")?
|
||||
.get(0)?
|
||||
.get("uhs")?
|
||||
.as_str()
|
||||
.map(String::from)
|
||||
})
|
||||
.ok_or(
|
||||
crate::ErrorKind::HydraError(
|
||||
"XBL response didn't contain valid user hash".to_string(),
|
||||
)
|
||||
.as_error(),
|
||||
)?;
|
||||
|
||||
Ok(XBLLogin { token, uhs })
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
use serde_json::json;
|
||||
|
||||
use crate::util::fetch::REQWEST_CLIENT;
|
||||
|
||||
use super::auth_retry;
|
||||
|
||||
const XSTS_AUTH_URL: &str = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
||||
|
||||
pub enum XSTSResponse {
|
||||
Unauthorized(String),
|
||||
Success { token: String },
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn fetch_token(token: &str) -> crate::Result<XSTSResponse> {
|
||||
let resp = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post(XSTS_AUTH_URL)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.json(&json!({
|
||||
"Properties": {
|
||||
"SandboxId": "RETAIL",
|
||||
"UserTokens": [
|
||||
token
|
||||
]
|
||||
},
|
||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||
"TokenType": "JWT"
|
||||
}))
|
||||
.send()
|
||||
})
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
|
||||
let body = resp.text().await?;
|
||||
let json = serde_json::from_str::<serde_json::Value>(&body)?;
|
||||
|
||||
if status.is_success() {
|
||||
Ok(json
|
||||
.get("Token")
|
||||
.and_then(|x| x.as_str().map(String::from))
|
||||
.map(|it| XSTSResponse::Success { token: it })
|
||||
.unwrap_or(XSTSResponse::Unauthorized(
|
||||
"XSTS response didn't contain valid token!".to_string(),
|
||||
)))
|
||||
} else {
|
||||
Ok(XSTSResponse::Unauthorized(
|
||||
#[allow(clippy::unreadable_literal)]
|
||||
match json.get("XErr").and_then(|x| x.as_i64()) {
|
||||
Some(2148916238) => {
|
||||
String::from("This Microsoft account is underage and is not linked to a family.")
|
||||
},
|
||||
Some(2148916235) => {
|
||||
String::from("XBOX Live/Minecraft is not available in your country.")
|
||||
},
|
||||
Some(2148916233) => String::from("This account does not have a valid XBOX Live profile. Please buy Minecraft and try again!"),
|
||||
Some(2148916236) | Some(2148916237) => String::from("This account needs adult verification on Xbox page."),
|
||||
_ => String::from("Unknown error code"),
|
||||
},
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -140,8 +140,14 @@ pub async fn get_output_by_filename(
|
||||
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
|
||||
let path = logs_folder.join(file_name);
|
||||
|
||||
let credentials: Vec<Credentials> =
|
||||
state.users.read().await.clone().0.into_values().collect();
|
||||
let credentials: Vec<Credentials> = state
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
|
||||
// Load .gz file into String
|
||||
if let Some(ext) = path.extension() {
|
||||
@@ -296,8 +302,14 @@ pub async fn get_generic_live_log_cursor(
|
||||
let output = String::from_utf8_lossy(&buffer).to_string(); // Convert to String
|
||||
let cursor = cursor + bytes_read as u64; // Update cursor
|
||||
|
||||
let credentials: Vec<Credentials> =
|
||||
state.users.read().await.clone().0.into_values().collect();
|
||||
let credentials: Vec<Credentials> = state
|
||||
.users
|
||||
.read()
|
||||
.await
|
||||
.users
|
||||
.clone()
|
||||
.into_values()
|
||||
.collect();
|
||||
let output = CensoredString::censor(output, &credentials);
|
||||
Ok(LatestLogCursor {
|
||||
cursor,
|
||||
|
||||
76
theseus/src/api/minecraft_auth.rs
Normal file
76
theseus/src/api/minecraft_auth.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
//! Authentication flow interface
|
||||
use crate::state::{Credentials, MinecraftLoginFlow};
|
||||
use crate::State;
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn begin_login() -> crate::Result<MinecraftLoginFlow> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.login_begin().await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn finish_login(
|
||||
code: &str,
|
||||
flow: MinecraftLoginFlow,
|
||||
) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
|
||||
users.login_finish(code, flow).await
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
Ok(users.default_user)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn set_default_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let user = get_user(user).await?;
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
users.default_user = Some(user.id);
|
||||
users.save().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a user account from the database
|
||||
#[tracing::instrument]
|
||||
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let mut users = state.users.write().await;
|
||||
users.remove(user).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a copy of the list of all user credentials
|
||||
#[tracing::instrument]
|
||||
pub async fn users() -> crate::Result<Vec<Credentials>> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
Ok(users.users.values().cloned().collect())
|
||||
}
|
||||
|
||||
/// Get a specific user by user ID
|
||||
/// Prefer to use 'refresh' instead of this function
|
||||
#[tracing::instrument]
|
||||
pub async fn get_user(user: uuid::Uuid) -> crate::Result<Credentials> {
|
||||
let state = State::get().await?;
|
||||
let users = state.users.read().await;
|
||||
let user = users
|
||||
.users
|
||||
.get(&user)
|
||||
.ok_or_else(|| {
|
||||
crate::ErrorKind::OtherError(format!(
|
||||
"Tried to get nonexistent user with ID {user}"
|
||||
))
|
||||
.as_error()
|
||||
})?
|
||||
.clone();
|
||||
Ok(user)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
//! API for interacting with Theseus
|
||||
pub mod auth;
|
||||
pub mod handler;
|
||||
pub mod hydra;
|
||||
pub mod jre;
|
||||
pub mod logs;
|
||||
pub mod metadata;
|
||||
pub mod minecraft_auth;
|
||||
pub mod mr_auth;
|
||||
pub mod pack;
|
||||
pub mod process;
|
||||
@@ -15,19 +14,19 @@ pub mod tags;
|
||||
|
||||
pub mod data {
|
||||
pub use crate::state::{
|
||||
DirectoryInfo, Hooks, JavaSettings, LinkedData, MemorySettings,
|
||||
ModLoader, ModrinthCredentials, ModrinthCredentialsResult,
|
||||
ModrinthProject, ModrinthTeamMember, ModrinthUser, ModrinthVersion,
|
||||
ProfileMetadata, ProjectMetadata, Settings, Theme, WindowSize,
|
||||
Credentials, DirectoryInfo, Hooks, JavaSettings, LinkedData,
|
||||
MemorySettings, ModLoader, ModrinthCredentials,
|
||||
ModrinthCredentialsResult, ModrinthProject, ModrinthTeamMember,
|
||||
ModrinthUser, ModrinthVersion, ProfileMetadata, ProjectMetadata,
|
||||
Settings, Theme, WindowSize,
|
||||
};
|
||||
}
|
||||
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
auth::{self, Credentials},
|
||||
data::*,
|
||||
event::CommandPayload,
|
||||
jre, metadata, pack, process,
|
||||
jre, metadata, minecraft_auth, pack, process,
|
||||
profile::{self, create, Profile},
|
||||
settings,
|
||||
state::JavaGlobals,
|
||||
|
||||
@@ -8,12 +8,13 @@ use crate::pack::install_from::{
|
||||
EnvType, PackDependency, PackFile, PackFileHash, PackFormat,
|
||||
};
|
||||
use crate::prelude::{JavaVersion, ProfilePathId, ProjectPathId};
|
||||
use crate::state::{InnerProjectPathUnix, ProjectMetadata, SideType};
|
||||
use crate::state::{
|
||||
Credentials, InnerProjectPathUnix, ProjectMetadata, SideType,
|
||||
};
|
||||
|
||||
use crate::util::fetch;
|
||||
use crate::util::io::{self, IOError};
|
||||
use crate::{
|
||||
auth::{self, refresh},
|
||||
event::{emit::emit_profile, ProfilePayloadType},
|
||||
state::MinecraftChild,
|
||||
};
|
||||
@@ -745,20 +746,16 @@ pub async fn run(
|
||||
let state = State::get().await?;
|
||||
|
||||
// Get default account and refresh credentials (preferred way to log in)
|
||||
let default_account = state.settings.read().await.default_user;
|
||||
let credentials = if let Some(default_account) = default_account {
|
||||
refresh(default_account).await?
|
||||
} else {
|
||||
// If no default account, try to use a logged in account
|
||||
let users = auth::users().await?;
|
||||
let last_account = users.first();
|
||||
if let Some(last_account) = last_account {
|
||||
refresh(last_account.id).await?
|
||||
} else {
|
||||
return Err(crate::ErrorKind::NoCredentialsError.as_error());
|
||||
}
|
||||
let default_account = {
|
||||
let mut write = state.users.write().await;
|
||||
|
||||
write
|
||||
.get_default_credential()
|
||||
.await?
|
||||
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?
|
||||
};
|
||||
run_credentials(path, &credentials).await
|
||||
|
||||
run_credentials(path, &default_account).await
|
||||
}
|
||||
|
||||
/// Run Minecraft using a profile, and credentials for authentication
|
||||
@@ -767,7 +764,7 @@ pub async fn run(
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn run_credentials(
|
||||
path: &ProfilePathId,
|
||||
credentials: &auth::Credentials,
|
||||
credentials: &Credentials,
|
||||
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
|
||||
let state = State::get().await?;
|
||||
let settings = state.settings.read().await;
|
||||
|
||||
@@ -25,11 +25,10 @@ pub enum ErrorKind {
|
||||
#[error("Metadata error: {0}")]
|
||||
MetadataError(#[from] daedalus::Error),
|
||||
|
||||
#[error("Minecraft authentication Hydra error: {0}")]
|
||||
HydraError(String),
|
||||
|
||||
#[error("Minecraft authentication task error: {0}")]
|
||||
AuthTaskError(#[from] crate::state::AuthTaskError),
|
||||
#[error("Minecraft authentication error: {0}")]
|
||||
MinecraftAuthenticationError(
|
||||
#[from] crate::state::MinecraftAuthenticationError,
|
||||
),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IOError(#[from] util::io::IOError),
|
||||
|
||||
@@ -43,7 +43,7 @@ impl EventState {
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.map(Arc::clone)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Minecraft CLI argument logic
|
||||
use super::auth::Credentials;
|
||||
use crate::launcher::parse_rules;
|
||||
use crate::state::Credentials;
|
||||
use crate::{
|
||||
state::{MemorySettings, WindowSize},
|
||||
util::{io::IOError, platform::classpath_separator},
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
//! Authentication flow based on Hydra
|
||||
|
||||
use crate::hydra;
|
||||
use crate::util::fetch::FetchSemaphore;
|
||||
|
||||
use chrono::{prelude::*, Duration};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::hydra::stages::{bearer_token, xbl_signin, xsts_token};
|
||||
|
||||
// Login information
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Credentials {
|
||||
pub id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires: DateTime<Utc>,
|
||||
_ctor_scope: std::marker::PhantomData<()>,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
pub fn new(
|
||||
id: uuid::Uuid,
|
||||
username: String,
|
||||
access_token: String,
|
||||
refresh_token: String,
|
||||
expires: DateTime<Utc>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
username,
|
||||
access_token,
|
||||
refresh_token,
|
||||
expires,
|
||||
_ctor_scope: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
self.expires < Utc::now()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh_credentials(
|
||||
credentials: &mut Credentials,
|
||||
_semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<()> {
|
||||
let oauth =
|
||||
hydra::refresh::refresh(credentials.refresh_token.clone()).await?;
|
||||
|
||||
let xbl_token = xbl_signin::login_xbl(&oauth.access_token).await?;
|
||||
|
||||
// Get xsts token from xbl token
|
||||
let xsts_response = xsts_token::fetch_token(&xbl_token.token).await?;
|
||||
|
||||
match xsts_response {
|
||||
xsts_token::XSTSResponse::Unauthorized(err) => {
|
||||
return Err(crate::ErrorKind::HydraError(format!(
|
||||
"Error getting XBox Live token: {}",
|
||||
err
|
||||
))
|
||||
.as_error())
|
||||
}
|
||||
xsts_token::XSTSResponse::Success { token: xsts_token } => {
|
||||
let (bearer_token, expires_in) =
|
||||
bearer_token::fetch_bearer(&xsts_token, &xbl_token.uhs)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
crate::ErrorKind::HydraError(format!(
|
||||
"Error getting bearer token: {}",
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
credentials.access_token = bearer_token;
|
||||
credentials.refresh_token = oauth.refresh_token;
|
||||
credentials.expires = Utc::now() + Duration::seconds(expires_in);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use crate::event::{LoadingBarId, LoadingBarType};
|
||||
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
|
||||
use crate::launcher::io::IOError;
|
||||
use crate::prelude::JavaVersion;
|
||||
use crate::state::ProfileInstallStage;
|
||||
use crate::state::{Credentials, ProfileInstallStage};
|
||||
use crate::util::io;
|
||||
use crate::{
|
||||
process,
|
||||
@@ -22,7 +22,6 @@ use uuid::Uuid;
|
||||
|
||||
mod args;
|
||||
|
||||
pub mod auth;
|
||||
pub mod download;
|
||||
|
||||
// All nones -> disallowed
|
||||
@@ -368,7 +367,7 @@ pub async fn launch_minecraft(
|
||||
wrapper: &Option<String>,
|
||||
memory: &st::MemorySettings,
|
||||
resolution: &st::WindowSize,
|
||||
credentials: &auth::Credentials,
|
||||
credentials: &Credentials,
|
||||
post_exit_hook: Option<String>,
|
||||
profile: &Profile,
|
||||
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
use crate::{
|
||||
hydra::{self, init::DeviceLoginSuccess},
|
||||
launcher::auth::Credentials,
|
||||
};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
// Authentication task
|
||||
// A wrapper over the authentication task that allows it to be called from the frontend
|
||||
// without caching the task handle in the frontend
|
||||
|
||||
pub struct AuthTask(
|
||||
#[allow(clippy::type_complexity)]
|
||||
Option<JoinHandle<crate::Result<Credentials>>>,
|
||||
);
|
||||
|
||||
impl AuthTask {
|
||||
pub fn new() -> AuthTask {
|
||||
AuthTask(None)
|
||||
}
|
||||
|
||||
pub async fn begin_auth() -> crate::Result<DeviceLoginSuccess> {
|
||||
let state = crate::State::get().await?;
|
||||
// Init task, get url
|
||||
let login = hydra::init::init().await?;
|
||||
|
||||
// Await completion
|
||||
let task = tokio::spawn(hydra::complete::wait_finish(
|
||||
login.device_code.clone(),
|
||||
));
|
||||
|
||||
// Flow is going, store in state and return
|
||||
let mut write = state.auth_flow.write().await;
|
||||
write.0 = Some(task);
|
||||
|
||||
Ok(login)
|
||||
}
|
||||
|
||||
pub async fn await_auth_completion() -> crate::Result<Credentials> {
|
||||
// Gets the task handle from the state, replacing with None
|
||||
let task = {
|
||||
let state = crate::State::get().await?;
|
||||
let mut write = state.auth_flow.write().await;
|
||||
|
||||
write.0.take()
|
||||
};
|
||||
|
||||
// Waits for the task to complete, and returns the credentials
|
||||
let credentials = task
|
||||
.ok_or(AuthTaskError::TaskMissing)?
|
||||
.await
|
||||
.map_err(AuthTaskError::from)??;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
pub async fn cancel() -> crate::Result<()> {
|
||||
// Gets the task handle from the state, replacing with None
|
||||
let task = {
|
||||
let state = crate::State::get().await?;
|
||||
let mut write = state.auth_flow.write().await;
|
||||
|
||||
write.0.take()
|
||||
};
|
||||
if let Some(task) = task {
|
||||
// Cancels the task
|
||||
task.abort();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AuthTask {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AuthTaskError {
|
||||
#[error("Authentication task was aborted or missing")]
|
||||
TaskMissing,
|
||||
#[error("Join handle error")]
|
||||
JoinHandleError(#[from] tokio::task::JoinError),
|
||||
}
|
||||
878
theseus/src/state/minecraft_auth.rs
Normal file
878
theseus/src/state/minecraft_auth.rs
Normal file
@@ -0,0 +1,878 @@
|
||||
use crate::data::DirectoryInfo;
|
||||
use crate::util::fetch::{read_json, write, IoSemaphore, REQWEST_CLIENT};
|
||||
use crate::State;
|
||||
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
|
||||
use base64::Engine;
|
||||
use byteorder::BigEndian;
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use p256::ecdsa::signature::Signer;
|
||||
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
|
||||
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::Rng;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::Response;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum MinecraftAuthStep {
|
||||
GetDeviceToken,
|
||||
SisuAuthenicate,
|
||||
GetOAuthToken,
|
||||
RefreshOAuthToken,
|
||||
SisuAuthorize,
|
||||
XstsAuthorize,
|
||||
MinecraftToken,
|
||||
MinecraftEntitlements,
|
||||
MinecraftProfile,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum MinecraftAuthenticationError {
|
||||
#[error("Failed to serialize private key to PEM: {0}")]
|
||||
PEMSerialize(#[from] p256::pkcs8::Error),
|
||||
#[error("Failed to serialize body to JSON during step {step:?}: {source}")]
|
||||
SerializeBody {
|
||||
step: MinecraftAuthStep,
|
||||
#[source]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
#[error(
|
||||
"Failed to deserialize response to JSON during step {step:?}: {source}"
|
||||
)]
|
||||
DeserializeResponse {
|
||||
step: MinecraftAuthStep,
|
||||
raw: String,
|
||||
#[source]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
#[error("Request failed during step {step:?}: {source}")]
|
||||
Request {
|
||||
step: MinecraftAuthStep,
|
||||
#[source]
|
||||
source: reqwest::Error,
|
||||
},
|
||||
#[error("Error creating signed request buffer {step:?}: {source}")]
|
||||
ConstructingSignedRequest {
|
||||
step: MinecraftAuthStep,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("Error reading user hash")]
|
||||
NoUserHash,
|
||||
}
|
||||
|
||||
const AUTH_JSON: &str = "minecraft_auth.json";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SaveDeviceToken {
|
||||
pub id: String,
|
||||
pub private_key: String,
|
||||
pub x: String,
|
||||
pub y: String,
|
||||
pub token: DeviceToken,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MinecraftLoginFlow {
|
||||
pub challenge: String,
|
||||
pub session_id: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct MinecraftAuthStore {
|
||||
pub users: HashMap<Uuid, Credentials>,
|
||||
pub token: Option<SaveDeviceToken>,
|
||||
pub default_user: Option<Uuid>,
|
||||
}
|
||||
|
||||
impl MinecraftAuthStore {
|
||||
#[tracing::instrument]
|
||||
pub async fn init(
|
||||
dirs: &DirectoryInfo,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let auth_path = dirs.caches_meta_dir().await.join(AUTH_JSON);
|
||||
let store = read_json(&auth_path, io_semaphore).await.ok();
|
||||
|
||||
if let Some(store) = store {
|
||||
Ok(store)
|
||||
} else {
|
||||
Ok(Self {
|
||||
users: HashMap::new(),
|
||||
token: None,
|
||||
default_user: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn save(&self) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let auth_path =
|
||||
state.directories.caches_meta_dir().await.join(AUTH_JSON);
|
||||
|
||||
write(&auth_path, &serde_json::to_vec(&self)?, &state.io_semaphore)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
async fn refresh_and_get_device_token(
|
||||
&mut self,
|
||||
) -> crate::Result<(DeviceTokenKey, DeviceToken)> {
|
||||
macro_rules! generate_key {
|
||||
($self:ident, $generate_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
|
||||
let key = generate_key()?;
|
||||
let token = device_token(&key).await?;
|
||||
|
||||
self.token = Some(SaveDeviceToken {
|
||||
id: key.id.clone(),
|
||||
private_key: key
|
||||
.key
|
||||
.to_pkcs8_pem(LineEnding::default())
|
||||
.map_err(|err| {
|
||||
MinecraftAuthenticationError::PEMSerialize(err)
|
||||
})?
|
||||
.to_string(),
|
||||
x: key.x.clone(),
|
||||
y: key.y.clone(),
|
||||
token: token.clone(),
|
||||
});
|
||||
self.save().await?;
|
||||
|
||||
(key, token)
|
||||
}};
|
||||
}
|
||||
|
||||
let (key, token) = if let Some(ref token) = self.token {
|
||||
if token.token.not_after > Utc::now() {
|
||||
if let Ok(private_key) =
|
||||
SigningKey::from_pkcs8_pem(&token.private_key)
|
||||
{
|
||||
(
|
||||
DeviceTokenKey {
|
||||
id: token.id.clone(),
|
||||
key: private_key,
|
||||
x: token.x.clone(),
|
||||
y: token.y.clone(),
|
||||
},
|
||||
token.token.clone(),
|
||||
)
|
||||
} else {
|
||||
generate_key!(
|
||||
self,
|
||||
generate_key,
|
||||
device_token,
|
||||
SaveDeviceToken
|
||||
)
|
||||
}
|
||||
} else {
|
||||
generate_key!(self, generate_key, device_token, SaveDeviceToken)
|
||||
}
|
||||
} else {
|
||||
generate_key!(self, generate_key, device_token, SaveDeviceToken)
|
||||
};
|
||||
|
||||
Ok((key, token))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> {
|
||||
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||
|
||||
let challenge = generate_oauth_challenge();
|
||||
let (session_id, redirect_uri) =
|
||||
sisu_authenticate(&token.token, &challenge, &key).await?;
|
||||
|
||||
Ok(MinecraftLoginFlow {
|
||||
challenge,
|
||||
session_id,
|
||||
redirect_uri: redirect_uri.msa_oauth_redirect,
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn login_finish(
|
||||
&mut self,
|
||||
code: &str,
|
||||
flow: MinecraftLoginFlow,
|
||||
) -> crate::Result<Credentials> {
|
||||
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||
|
||||
let oauth_token = oauth_token(code, &flow.challenge).await?;
|
||||
let sisu_authorize = sisu_authorize(
|
||||
Some(&flow.session_id),
|
||||
&oauth_token.access_token,
|
||||
&token.token,
|
||||
&key,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let xbox_token =
|
||||
xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||
|
||||
minecraft_entitlements(&minecraft_token.access_token).await?;
|
||||
|
||||
let profile = minecraft_profile(&minecraft_token.access_token).await?;
|
||||
|
||||
let profile_id = profile.id.unwrap_or_default();
|
||||
|
||||
let credentials = Credentials {
|
||||
id: profile_id,
|
||||
username: profile.name,
|
||||
access_token: minecraft_token.access_token,
|
||||
refresh_token: oauth_token.refresh_token,
|
||||
expires: Utc::now()
|
||||
+ Duration::seconds(oauth_token.expires_in as i64),
|
||||
};
|
||||
|
||||
self.users.insert(profile_id, credentials.clone());
|
||||
|
||||
if self.default_user.is_none() {
|
||||
self.default_user = Some(profile_id);
|
||||
}
|
||||
|
||||
self.save().await?;
|
||||
|
||||
Ok(credentials)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_default_credential(
|
||||
&mut self,
|
||||
) -> crate::Result<Option<Credentials>> {
|
||||
let credentials = if let Some(default_user) = self.default_user {
|
||||
if let Some(creds) = self.users.get(&default_user) {
|
||||
Some(creds)
|
||||
} else {
|
||||
self.users.values().next()
|
||||
}
|
||||
} else {
|
||||
self.users.values().next()
|
||||
};
|
||||
|
||||
if let Some(creds) = credentials {
|
||||
if self.default_user != Some(creds.id) {
|
||||
self.default_user = Some(creds.id);
|
||||
self.save().await?;
|
||||
}
|
||||
|
||||
if creds.expires < Utc::now() {
|
||||
let cred_id = creds.id;
|
||||
let profile_name = creds.username.clone();
|
||||
|
||||
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
|
||||
let (key, token) = self.refresh_and_get_device_token().await?;
|
||||
|
||||
let sisu_authorize = sisu_authorize(
|
||||
None,
|
||||
&oauth_token.access_token,
|
||||
&token.token,
|
||||
&key,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let xbox_token =
|
||||
xsts_authorize(sisu_authorize, &token.token, &key).await?;
|
||||
|
||||
let minecraft_token = minecraft_token(xbox_token).await?;
|
||||
|
||||
let val = Credentials {
|
||||
id: cred_id,
|
||||
username: profile_name,
|
||||
access_token: minecraft_token.access_token,
|
||||
refresh_token: oauth_token.refresh_token,
|
||||
expires: Utc::now()
|
||||
+ Duration::seconds(oauth_token.expires_in as i64),
|
||||
};
|
||||
|
||||
self.users.insert(val.id, val.clone());
|
||||
self.save().await?;
|
||||
|
||||
Ok(Some(val))
|
||||
} else {
|
||||
Ok(Some(creds.clone()))
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove(
|
||||
&mut self,
|
||||
id: Uuid,
|
||||
) -> crate::Result<Option<Credentials>> {
|
||||
let val = self.users.remove(&id);
|
||||
self.save().await?;
|
||||
Ok(val)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Credentials {
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
pub expires: DateTime<Utc>,
|
||||
}
|
||||
|
||||
const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
||||
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
// flow steps
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct DeviceToken {
|
||||
pub issue_instant: DateTime<Utc>,
|
||||
pub not_after: DateTime<Utc>,
|
||||
pub token: String,
|
||||
pub display_claims: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
pub async fn device_token(
|
||||
key: &DeviceTokenKey,
|
||||
) -> Result<DeviceToken, MinecraftAuthenticationError> {
|
||||
Ok(send_signed_request(
|
||||
None,
|
||||
"https://device.auth.xboxlive.com/device/authenticate",
|
||||
"/device/authenticate",
|
||||
json!({
|
||||
"Properties": {
|
||||
"AuthMethod": "ProofOfPossession",
|
||||
"Id": format!("{{{}}}", key.id),
|
||||
"DeviceType": "Win32",
|
||||
"Version": "10.16.0",
|
||||
"ProofKey": {
|
||||
"kty": "EC",
|
||||
"x": key.x,
|
||||
"y": key.y,
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"use": "sig"
|
||||
}
|
||||
},
|
||||
"RelyingParty": "http://auth.xboxlive.com",
|
||||
"TokenType": "JWT"
|
||||
|
||||
}),
|
||||
key,
|
||||
MinecraftAuthStep::GetDeviceToken,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct RedirectUri {
|
||||
pub msa_oauth_redirect: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
async fn sisu_authenticate(
|
||||
token: &str,
|
||||
challenge: &str,
|
||||
key: &DeviceTokenKey,
|
||||
) -> Result<(String, RedirectUri), MinecraftAuthenticationError> {
|
||||
let (headers, res) = send_signed_request(
|
||||
None,
|
||||
"https://sisu.xboxlive.com/authenticate",
|
||||
"/authenticate",
|
||||
json!({
|
||||
"AppId": MICROSOFT_CLIENT_ID,
|
||||
"DeviceToken": token,
|
||||
"Offers": [
|
||||
REQUESTED_SCOPES
|
||||
],
|
||||
"Query": {
|
||||
"code_challenge": challenge,
|
||||
"code_challenge_method": "plain",
|
||||
"state": "",
|
||||
"prompt": "select_account"
|
||||
},
|
||||
"RedirectUri": REDIRECT_URL,
|
||||
"Sandbox": "RETAIL",
|
||||
"TokenType": "code",
|
||||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthenicate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let session_id = headers
|
||||
.get("X-SessionId")
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
Ok((session_id, res))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OAuthToken {
|
||||
// pub token_type: String,
|
||||
pub expires_in: u64,
|
||||
// pub scope: String,
|
||||
pub access_token: String,
|
||||
pub refresh_token: String,
|
||||
// pub user_id: String,
|
||||
// pub foci: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn oauth_token(
|
||||
code: &str,
|
||||
challenge: &str,
|
||||
) -> Result<OAuthToken, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("code", code);
|
||||
query.insert("code_verifier", challenge);
|
||||
query.insert("grant_type", "authorization_code");
|
||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.header("Accept", "application/json")
|
||||
.form(&query)
|
||||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::GetOAuthToken,
|
||||
})?;
|
||||
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::GetOAuthToken,
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: text,
|
||||
step: MinecraftAuthStep::GetOAuthToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn oauth_refresh(
|
||||
refresh_token: &str,
|
||||
) -> Result<OAuthToken, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("refresh_token", refresh_token);
|
||||
query.insert("grant_type", "refresh_token");
|
||||
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
|
||||
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post("https://login.live.com/oauth20_token.srf")
|
||||
.header("Accept", "application/json")
|
||||
.form(&query)
|
||||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
})?;
|
||||
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: text,
|
||||
step: MinecraftAuthStep::RefreshOAuthToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct SisuAuthorize {
|
||||
// pub authorization_token: DeviceToken,
|
||||
// pub device_token: String,
|
||||
// pub sandbox: String,
|
||||
pub title_token: DeviceToken,
|
||||
pub user_token: DeviceToken,
|
||||
// pub web_page: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
async fn sisu_authorize(
|
||||
session_id: Option<&str>,
|
||||
access_token: &str,
|
||||
device_token: &str,
|
||||
key: &DeviceTokenKey,
|
||||
) -> Result<SisuAuthorize, MinecraftAuthenticationError> {
|
||||
Ok(send_signed_request(
|
||||
None,
|
||||
"https://sisu.xboxlive.com/authorize",
|
||||
"/authorize",
|
||||
json!({
|
||||
"AccessToken": format!("t={access_token}"),
|
||||
"AppId": "00000000402b5328",
|
||||
"DeviceToken": device_token,
|
||||
"ProofKey": {
|
||||
"kty": "EC",
|
||||
"x": key.x,
|
||||
"y": key.y,
|
||||
"crv": "P-256",
|
||||
"alg": "ES256",
|
||||
"use": "sig"
|
||||
},
|
||||
"Sandbox": "RETAIL",
|
||||
"SessionId": session_id,
|
||||
"SiteName": "user.auth.xboxlive.com",
|
||||
}),
|
||||
key,
|
||||
MinecraftAuthStep::SisuAuthorize,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
async fn xsts_authorize(
|
||||
authorize: SisuAuthorize,
|
||||
device_token: &str,
|
||||
key: &DeviceTokenKey,
|
||||
) -> Result<DeviceToken, MinecraftAuthenticationError> {
|
||||
Ok(send_signed_request(
|
||||
None,
|
||||
"https://xsts.auth.xboxlive.com/xsts/authorize",
|
||||
"/xsts/authorize",
|
||||
json!({
|
||||
"RelyingParty": "rp://api.minecraftservices.com/",
|
||||
"TokenType": "JWT",
|
||||
"Properties": {
|
||||
"SandboxId": "RETAIL",
|
||||
"UserTokens": [authorize.user_token.token],
|
||||
"DeviceToken": device_token,
|
||||
"TitleToken": authorize.title_token.token,
|
||||
},
|
||||
}),
|
||||
key,
|
||||
MinecraftAuthStep::XstsAuthorize,
|
||||
)
|
||||
.await?
|
||||
.1)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MinecraftToken {
|
||||
// pub username: String,
|
||||
pub access_token: String,
|
||||
// pub token_type: String,
|
||||
// pub expires_in: u64,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn minecraft_token(
|
||||
token: DeviceToken,
|
||||
) -> Result<MinecraftToken, MinecraftAuthenticationError> {
|
||||
let uhs = token
|
||||
.display_claims
|
||||
.get("xui")
|
||||
.and_then(|x| x.get(0))
|
||||
.and_then(|x| x.get("uhs"))
|
||||
.and_then(|x| x.as_str().map(String::from))
|
||||
.ok_or_else(|| MinecraftAuthenticationError::NoUserHash)?;
|
||||
|
||||
let token = token.token;
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.post("https://api.minecraftservices.com/launcher/login")
|
||||
.header("Accept", "application/json")
|
||||
.json(&json!({
|
||||
"platform": "PC_LAUNCHER",
|
||||
"xtoken": format!("XBL3.0 x={uhs};{token}"),
|
||||
}))
|
||||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
})?;
|
||||
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: text,
|
||||
step: MinecraftAuthStep::MinecraftToken,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MinecraftProfile {
|
||||
pub id: Option<Uuid>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn minecraft_profile(
|
||||
token: &str,
|
||||
) -> Result<MinecraftProfile, MinecraftAuthenticationError> {
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.get("https://api.minecraftservices.com/minecraft/profile")
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
})?;
|
||||
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: text,
|
||||
step: MinecraftAuthStep::MinecraftProfile,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct MinecraftEntitlements {}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn minecraft_entitlements(
|
||||
token: &str,
|
||||
) -> Result<MinecraftEntitlements, MinecraftAuthenticationError> {
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
.get(format!("https://api.minecraftservices.com/entitlements/license?requestId={}", Uuid::new_v4()))
|
||||
.header("Accept", "application/json")
|
||||
.bearer_auth(token)
|
||||
.send()
|
||||
})
|
||||
.await.map_err(|source| MinecraftAuthenticationError::Request { source, step: MinecraftAuthStep::MinecraftEntitlements })?;
|
||||
|
||||
let text = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request {
|
||||
source,
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
}
|
||||
})?;
|
||||
|
||||
serde_json::from_str(&text).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: text,
|
||||
step: MinecraftAuthStep::MinecraftEntitlements,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// auth utils
|
||||
#[tracing::instrument(skip(reqwest_request))]
|
||||
async fn auth_retry<F>(
|
||||
reqwest_request: impl Fn() -> F,
|
||||
) -> Result<reqwest::Response, reqwest::Error>
|
||||
where
|
||||
F: Future<Output = Result<Response, reqwest::Error>>,
|
||||
{
|
||||
const RETRY_COUNT: usize = 9; // Does command 9 times
|
||||
const RETRY_WAIT: std::time::Duration =
|
||||
std::time::Duration::from_millis(250);
|
||||
|
||||
let mut resp = reqwest_request().await?;
|
||||
for i in 0..RETRY_COUNT {
|
||||
if resp.status().is_success() {
|
||||
break;
|
||||
}
|
||||
tracing::debug!(
|
||||
"Request failed with status code {}, retrying...",
|
||||
resp.status()
|
||||
);
|
||||
if i < RETRY_COUNT - 1 {
|
||||
tokio::time::sleep(RETRY_WAIT).await;
|
||||
}
|
||||
resp = reqwest_request().await?;
|
||||
}
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
pub struct DeviceTokenKey {
|
||||
pub id: String,
|
||||
pub key: SigningKey,
|
||||
pub x: String,
|
||||
pub y: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
|
||||
let id = Uuid::new_v4().to_string();
|
||||
|
||||
let signing_key = SigningKey::random(&mut OsRng);
|
||||
let public_key = VerifyingKey::from(&signing_key);
|
||||
|
||||
let encoded_point = public_key.to_encoded_point(false);
|
||||
|
||||
Ok(DeviceTokenKey {
|
||||
id,
|
||||
key: signing_key,
|
||||
x: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.x().unwrap()),
|
||||
y: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.y().unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(key))]
|
||||
async fn send_signed_request<T: DeserializeOwned>(
|
||||
authorization: Option<&str>,
|
||||
url: &str,
|
||||
url_path: &str,
|
||||
raw_body: serde_json::Value,
|
||||
key: &DeviceTokenKey,
|
||||
step: MinecraftAuthStep,
|
||||
) -> Result<(HeaderMap, T), MinecraftAuthenticationError> {
|
||||
let auth = authorization.map_or(Vec::new(), |v| v.as_bytes().to_vec());
|
||||
|
||||
let body = serde_json::to_vec(&raw_body).map_err(|source| {
|
||||
MinecraftAuthenticationError::SerializeBody { source, step }
|
||||
})?;
|
||||
let time: u128 =
|
||||
{ ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 };
|
||||
|
||||
use byteorder::WriteBytesExt;
|
||||
let mut buffer = Vec::new();
|
||||
buffer.write_u32::<BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer
|
||||
.write_u64::<BigEndian>(time as u64)
|
||||
.map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||
source,
|
||||
step,
|
||||
}
|
||||
})?;
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.extend_from_slice("POST".as_bytes());
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.extend_from_slice(url_path.as_bytes());
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.extend_from_slice(&auth);
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
buffer.extend_from_slice(&body);
|
||||
buffer.write_u8(0).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
|
||||
let ecdsa_sig: Signature = key.key.sign(&buffer);
|
||||
|
||||
let mut sig_buffer = Vec::new();
|
||||
sig_buffer.write_i32::<BigEndian>(1).map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest { source, step }
|
||||
})?;
|
||||
sig_buffer
|
||||
.write_u64::<BigEndian>(time as u64)
|
||||
.map_err(|source| {
|
||||
MinecraftAuthenticationError::ConstructingSignedRequest {
|
||||
source,
|
||||
step,
|
||||
}
|
||||
})?;
|
||||
sig_buffer.extend_from_slice(&ecdsa_sig.r().to_bytes());
|
||||
sig_buffer.extend_from_slice(&ecdsa_sig.s().to_bytes());
|
||||
|
||||
let signature = BASE64_STANDARD.encode(&sig_buffer);
|
||||
|
||||
let res = auth_retry(|| {
|
||||
let mut request = REQWEST_CLIENT
|
||||
.post(url)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Accept", "application/json")
|
||||
.header("x-xbl-contract-version", "1")
|
||||
.header("signature", &signature);
|
||||
|
||||
if let Some(auth) = authorization {
|
||||
request = request.header("Authorization", auth);
|
||||
}
|
||||
|
||||
request.body(body.clone()).send()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| MinecraftAuthenticationError::Request { source, step })?;
|
||||
|
||||
let headers = res.headers().clone();
|
||||
let res = res.text().await.map_err(|source| {
|
||||
MinecraftAuthenticationError::Request { source, step }
|
||||
})?;
|
||||
|
||||
let body = serde_json::from_str(&res).map_err(|source| {
|
||||
MinecraftAuthenticationError::DeserializeResponse {
|
||||
source,
|
||||
raw: res,
|
||||
step,
|
||||
}
|
||||
})?;
|
||||
Ok((headers, body))
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn generate_oauth_challenge() -> String {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
|
||||
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
|
||||
}
|
||||
@@ -5,7 +5,6 @@ use std::path::PathBuf;
|
||||
use crate::event::LoadingBarType;
|
||||
use crate::loading_join;
|
||||
|
||||
use crate::state::users::Users;
|
||||
use crate::util::fetch::{self, FetchSemaphore, IoSemaphore};
|
||||
use notify::RecommendedWatcher;
|
||||
use notify_debouncer_mini::{new_debouncer, DebounceEventResult, Debouncer};
|
||||
@@ -32,14 +31,9 @@ pub use self::settings::*;
|
||||
mod projects;
|
||||
pub use self::projects::*;
|
||||
|
||||
mod users;
|
||||
|
||||
mod children;
|
||||
pub use self::children::*;
|
||||
|
||||
mod auth_task;
|
||||
pub use self::auth_task::*;
|
||||
|
||||
mod tags;
|
||||
pub use self::tags::*;
|
||||
|
||||
@@ -52,6 +46,9 @@ pub use self::safe_processes::*;
|
||||
mod discord;
|
||||
pub use self::discord::*;
|
||||
|
||||
mod minecraft_auth;
|
||||
pub use self::minecraft_auth::*;
|
||||
|
||||
mod mr_auth;
|
||||
pub use self::mr_auth::*;
|
||||
|
||||
@@ -87,9 +84,7 @@ pub struct State {
|
||||
/// Launcher processes that should be safely exited on shutdown
|
||||
pub(crate) safety_processes: RwLock<SafeProcesses>,
|
||||
/// Launcher user account info
|
||||
pub(crate) users: RwLock<Users>,
|
||||
/// Authentication flow
|
||||
pub auth_flow: RwLock<AuthTask>,
|
||||
pub(crate) users: RwLock<MinecraftAuthStore>,
|
||||
/// Modrinth Credentials Store
|
||||
pub credentials: RwLock<CredentialsStore>,
|
||||
/// Modrinth auth flow
|
||||
@@ -172,7 +167,7 @@ impl State {
|
||||
&fetch_semaphore,
|
||||
&CredentialsStore(None),
|
||||
);
|
||||
let users_fut = Users::init(&directories, &io_semaphore);
|
||||
let users_fut = MinecraftAuthStore::init(&directories, &io_semaphore);
|
||||
let creds_fut = CredentialsStore::init(&directories, &io_semaphore);
|
||||
// Launcher data
|
||||
let (metadata, profiles, tags, users, creds) = loading_join! {
|
||||
@@ -184,7 +179,6 @@ impl State {
|
||||
creds_fut,
|
||||
}?;
|
||||
|
||||
let auth_flow = AuthTask::new();
|
||||
let safety_processes = SafeProcesses::new();
|
||||
|
||||
let discord_rpc = DiscordGuard::init(is_offline).await?;
|
||||
@@ -217,7 +211,6 @@ impl State {
|
||||
profiles: RwLock::new(profiles),
|
||||
users: RwLock::new(users),
|
||||
children: RwLock::new(children),
|
||||
auth_flow: RwLock::new(auth_flow),
|
||||
credentials: RwLock::new(creds),
|
||||
tags: RwLock::new(tags),
|
||||
discord_rpc,
|
||||
@@ -253,9 +246,8 @@ impl State {
|
||||
let res4 = Profiles::update_projects();
|
||||
let res5 = Settings::update_java();
|
||||
let res6 = CredentialsStore::update_creds();
|
||||
let res7 = Settings::update_default_user();
|
||||
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6, res7);
|
||||
let _ = join!(res1, res2, res3, res4, res5, res6);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct Settings {
|
||||
pub custom_java_args: Vec<String>,
|
||||
pub custom_env_args: Vec<(String, String)>,
|
||||
pub java_globals: JavaGlobals,
|
||||
pub default_user: Option<uuid::Uuid>,
|
||||
pub hooks: Hooks,
|
||||
pub max_concurrent_downloads: usize,
|
||||
pub max_concurrent_writes: usize,
|
||||
@@ -93,7 +92,6 @@ impl Settings {
|
||||
custom_java_args: Vec::new(),
|
||||
custom_env_args: Vec::new(),
|
||||
java_globals: JavaGlobals::new(),
|
||||
default_user: None,
|
||||
hooks: Hooks::default(),
|
||||
max_concurrent_downloads: 10,
|
||||
max_concurrent_writes: 10,
|
||||
@@ -152,32 +150,6 @@ impl Settings {
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
#[theseus_macros::debug_pin]
|
||||
pub async fn update_default_user() {
|
||||
let res = async {
|
||||
let state = State::get().await?;
|
||||
let settings_read = state.settings.read().await;
|
||||
|
||||
if settings_read.default_user.is_none() {
|
||||
drop(settings_read);
|
||||
let users = state.users.read().await;
|
||||
let user = users.0.iter().next().map(|(id, _)| *id);
|
||||
state.settings.write().await.default_user = user;
|
||||
}
|
||||
|
||||
Ok::<(), crate::Error>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
tracing::warn!("Unable to update default user: {err}")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||
fs::write(to, serde_json::to_vec(self)?)
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
//! User login info
|
||||
use crate::auth::Credentials;
|
||||
use crate::data::DirectoryInfo;
|
||||
use crate::util::fetch::{read_json, write, IoSemaphore};
|
||||
use crate::State;
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
const USERS_JSON: &str = "users.json";
|
||||
|
||||
/// The set of users stored in the launcher
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Users(pub(crate) HashMap<Uuid, Credentials>);
|
||||
|
||||
impl Users {
|
||||
pub async fn init(
|
||||
dirs: &DirectoryInfo,
|
||||
io_semaphore: &IoSemaphore,
|
||||
) -> crate::Result<Self> {
|
||||
let users_path = dirs.caches_meta_dir().await.join(USERS_JSON);
|
||||
let users = read_json(&users_path, io_semaphore).await.ok();
|
||||
|
||||
if let Some(users) = users {
|
||||
Ok(Self(users))
|
||||
} else {
|
||||
Ok(Self(HashMap::new()))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save(&self) -> crate::Result<()> {
|
||||
let state = State::get().await?;
|
||||
let users_path =
|
||||
state.directories.caches_meta_dir().await.join(USERS_JSON);
|
||||
write(
|
||||
&users_path,
|
||||
&serde_json::to_vec(&self.0)?,
|
||||
&state.io_semaphore,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn insert(
|
||||
&mut self,
|
||||
credentials: &Credentials,
|
||||
) -> crate::Result<&Self> {
|
||||
self.0.insert(credentials.id, credentials.clone());
|
||||
self.save().await?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn contains(&self, id: Uuid) -> bool {
|
||||
self.0.contains_key(&id)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub fn get(&self, id: Uuid) -> Option<Credentials> {
|
||||
self.0.get(&id).cloned()
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn remove(&mut self, id: Uuid) -> crate::Result<&Self> {
|
||||
self.0.remove(&id);
|
||||
self.save().await?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user