Fix auth timestamps (#1184)

* Fix auth timestamps

* Update error message, get valid timestamp on token refresh

* fix lint
This commit is contained in:
Geometrically
2024-05-10 10:31:34 -07:00
committed by GitHub
parent a4f133eb46
commit 7394fdc162
3 changed files with 264 additions and 120 deletions

View File

@@ -106,7 +106,8 @@ pub enum ErrorKind {
#[derive(Debug)]
pub struct Error {
source: tracing_error::TracedError<ErrorKind>,
pub raw: std::sync::Arc<ErrorKind>,
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
}
impl std::error::Error for Error {
@@ -123,8 +124,12 @@ impl std::fmt::Display for Error {
impl<E: Into<ErrorKind>> From<E> for Error {
fn from(source: E) -> Self {
let error = Into::<ErrorKind>::into(source);
let boxed_error = std::sync::Arc::new(error);
Self {
source: Into::<ErrorKind>::into(source).in_current_span(),
raw: boxed_error.clone(),
source: boxed_error.in_current_span(),
}
}
}

View File

@@ -1,17 +1,17 @@
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write, IoSemaphore, REQWEST_CLIENT};
use crate::State;
use crate::{ErrorKind, State};
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
use base64::Engine;
use byteorder::BigEndian;
use chrono::{DateTime, Duration, NaiveDate, Utc};
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::{Error, Response};
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -82,8 +82,6 @@ pub struct SaveDeviceToken {
pub x: String,
pub y: String,
pub token: DeviceToken,
#[serde(default)]
modern: bool,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -136,11 +134,13 @@ impl MinecraftAuthStore {
#[tracing::instrument(skip(self))]
async fn refresh_and_get_device_token(
&mut self,
) -> crate::Result<(DeviceTokenKey, DeviceToken)> {
current_date: DateTime<Utc>,
force_generate: bool,
) -> crate::Result<(DeviceTokenKey, DeviceToken, DateTime<Utc>, bool)> {
macro_rules! generate_key {
($self:ident, $generate_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
let key = generate_key()?;
let token = device_token(&key).await?;
let res = device_token(&key, current_date).await?;
self.token = Some(SaveDeviceToken {
id: key.id.clone(),
@@ -153,30 +153,20 @@ impl MinecraftAuthStore {
.to_string(),
x: key.x.clone(),
y: key.y.clone(),
token: token.clone(),
modern: true,
token: res.value.clone(),
});
self.save().await?;
(key, token)
(key, res.value, res.date, true)
}};
}
let (key, token) = if let Some(ref token) = self.token {
// reset device token for legacy launcher versions with broken values
if self.users.is_empty() && !token.modern {
return Ok(generate_key!(
self,
generate_key,
device_token,
SaveDeviceToken
));
}
if token.token.not_after > Utc::now() {
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key)
{
let (key, token, date, valid_date) = if let Some(ref token) = self.token
{
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key)
{
if token.token.not_after > Utc::now() && !force_generate {
(
DeviceTokenKey {
id: token.id.clone(),
@@ -185,14 +175,20 @@ impl MinecraftAuthStore {
y: token.y.clone(),
},
token.token.clone(),
current_date,
false,
)
} else {
generate_key!(
self,
generate_key,
device_token,
SaveDeviceToken
)
let key = DeviceTokenKey {
id: token.id.clone(),
key: private_key,
x: token.x.clone(),
y: token.y.clone(),
};
let res = device_token(&key, current_date).await?;
(key, res.value, res.date, true)
}
} else {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
@@ -201,28 +197,60 @@ impl MinecraftAuthStore {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
};
Ok((key, token))
Ok((key, token, date, valid_date))
}
#[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 (key, token, current_date, valid_date) =
self.refresh_and_get_device_token(Utc::now(), false).await?;
let verifier = generate_oauth_challenge();
let mut hasher = sha2::Sha256::new();
hasher.update(&verifier);
let result = hasher.finalize();
let challenge = BASE64_URL_SAFE_NO_PAD.encode(&result);
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
let (session_id, redirect_uri) =
sisu_authenticate(&token.token, &challenge, &key).await?;
match sisu_authenticate(&token.token, &challenge, &key, current_date)
.await
{
Ok((session_id, redirect_uri)) => Ok(MinecraftLoginFlow {
verifier,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
}),
Err(err) => {
if !valid_date {
let (key, token, current_date, _) = self
.refresh_and_get_device_token(Utc::now(), false)
.await?;
Ok(MinecraftLoginFlow {
verifier,
challenge,
session_id,
redirect_uri: redirect_uri.msa_oauth_redirect,
})
let verifier = generate_oauth_challenge();
let mut hasher = sha2::Sha256::new();
hasher.update(&verifier);
let result = hasher.finalize();
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
let (session_id, redirect_uri) = sisu_authenticate(
&token.token,
&challenge,
&key,
current_date,
)
.await?;
Ok(MinecraftLoginFlow {
verifier,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
})
} else {
Err(crate::ErrorKind::from(err).into())
}
}
}
}
#[tracing::instrument(skip(self))]
@@ -231,20 +259,27 @@ impl MinecraftAuthStore {
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let (key, token) = self.refresh_and_get_device_token().await?;
let (key, token, _, _) =
self.refresh_and_get_device_token(Utc::now(), false).await?;
let oauth_token = oauth_token(code, &flow.verifier).await?;
let sisu_authorize = sisu_authorize(
Some(&flow.session_id),
&oauth_token.access_token,
&oauth_token.value.access_token,
&token.token,
&key,
oauth_token.date,
)
.await?;
let xbox_token =
xsts_authorize(sisu_authorize, &token.token, &key).await?;
let minecraft_token = minecraft_token(xbox_token).await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
minecraft_entitlements(&minecraft_token.access_token).await?;
@@ -256,9 +291,9 @@ impl MinecraftAuthStore {
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),
refresh_token: oauth_token.value.refresh_token,
expires: oauth_token.date
+ Duration::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(profile_id, credentials.clone());
@@ -272,6 +307,52 @@ impl MinecraftAuthStore {
Ok(credentials)
}
async fn refresh_token(
&mut self,
creds: &Credentials,
) -> crate::Result<Option<Credentials>> {
let cred_id = creds.id;
let profile_name = creds.username.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token, current_date, _) = self
.refresh_and_get_device_token(oauth_token.date, false)
.await?;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.value.access_token,
&token.token,
&key,
current_date,
)
.await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
let val = Credentials {
id: cred_id,
username: profile_name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.value.refresh_token,
expires: oauth_token.date
+ Duration::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(val.id, val.clone());
self.save().await?;
Ok(Some(val))
}
#[tracing::instrument(skip(self))]
pub async fn get_default_credential(
&mut self,
@@ -293,38 +374,28 @@ impl MinecraftAuthStore {
}
if creds.expires < Utc::now() {
let cred_id = creds.id;
let profile_name = creds.username.clone();
let old_credentials = creds.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token) = self.refresh_and_get_device_token().await?;
let res = self.refresh_token(&old_credentials).await;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.access_token,
&token.token,
&key,
)
.await?;
match res {
Ok(val) => Ok(val),
Err(err) => {
if let ErrorKind::MinecraftAuthenticationError(
MinecraftAuthenticationError::Request {
ref source,
..
},
) = *err.raw
{
if source.is_connect() || source.is_timeout() {
return Ok(Some(old_credentials));
}
}
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))
Err(err)
}
}
} else {
Ok(Some(creds.clone()))
}
@@ -357,6 +428,11 @@ 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";
struct RequestWithDate<T> {
pub date: DateTime<Utc>,
pub value: T,
}
// flow steps
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
@@ -370,8 +446,9 @@ pub struct DeviceToken {
#[tracing::instrument(skip(key))]
pub async fn device_token(
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://device.auth.xboxlive.com/device/authenticate",
"/device/authenticate",
@@ -396,9 +473,14 @@ pub async fn device_token(
}),
key,
MinecraftAuthStep::GetDeviceToken,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
@@ -412,8 +494,10 @@ async fn sisu_authenticate(
token: &str,
challenge: &str,
key: &DeviceTokenKey,
) -> Result<(String, RedirectUri), MinecraftAuthenticationError> {
let (headers, res) = send_signed_request(
current_date: DateTime<Utc>,
) -> Result<(String, RequestWithDate<RedirectUri>), MinecraftAuthenticationError>
{
let res = send_signed_request::<RedirectUri>(
None,
"https://sisu.xboxlive.com/authenticate",
"/authenticate",
@@ -436,16 +520,24 @@ async fn sisu_authenticate(
}),
key,
MinecraftAuthStep::SisuAuthenicate,
current_date,
)
.await?;
let session_id = headers
let session_id = res
.headers
.get("X-SessionId")
.and_then(|x| x.to_str().ok())
.ok_or_else(|| MinecraftAuthenticationError::NoSessionId)?
.to_string();
Ok((session_id, res))
Ok((
session_id,
RequestWithDate {
date: res.current_date,
value: res.body,
},
))
}
#[derive(Deserialize)]
@@ -463,11 +555,11 @@ struct OAuthToken {
async fn oauth_token(
code: &str,
verifier: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("code", code);
query.insert("code_verifier", &*verifier);
query.insert("code_verifier", verifier);
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");
@@ -486,6 +578,7 @@ async fn oauth_token(
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -493,20 +586,25 @@ async fn oauth_token(
}
})?;
serde_json::from_str(&text).map_err(|source| {
let body = serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::GetOAuthToken,
status_code: status,
}
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
#[tracing::instrument]
async fn oauth_refresh(
refresh_token: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("refresh_token", refresh_token);
@@ -528,6 +626,7 @@ async fn oauth_refresh(
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -535,13 +634,18 @@ async fn oauth_refresh(
}
})?;
serde_json::from_str(&text).map_err(|source| {
let body = serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::RefreshOAuthToken,
status_code: status,
}
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
@@ -562,8 +666,9 @@ async fn sisu_authorize(
access_token: &str,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<SisuAuthorize, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<SisuAuthorize>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://sisu.xboxlive.com/authorize",
"/authorize",
@@ -587,9 +692,14 @@ async fn sisu_authorize(
}),
key,
MinecraftAuthStep::SisuAuthorize,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[tracing::instrument(skip(key))]
@@ -597,8 +707,9 @@ async fn xsts_authorize(
authorize: SisuAuthorize,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://xsts.auth.xboxlive.com/xsts/authorize",
"/xsts/authorize",
@@ -614,9 +725,14 @@ async fn xsts_authorize(
}),
key,
MinecraftAuthStep::XstsAuthorize,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
@@ -820,6 +936,12 @@ fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
})
}
struct SignedRequestResponse<T> {
pub headers: HeaderMap,
pub current_date: DateTime<Utc>,
pub body: T,
}
#[tracing::instrument(skip(key))]
async fn send_signed_request<T: DeserializeOwned>(
authorization: Option<&str>,
@@ -828,14 +950,15 @@ async fn send_signed_request<T: DeserializeOwned>(
raw_body: serde_json::Value,
key: &DeviceTokenKey,
step: MinecraftAuthStep,
) -> Result<(HeaderMap, T), MinecraftAuthenticationError> {
current_date: DateTime<Utc>,
) -> Result<SignedRequestResponse<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 };
{ ((current_date.timestamp() as u128) + 11644473600) * 10000000 };
use byteorder::WriteBytesExt;
let mut buffer = Vec::new();
@@ -914,6 +1037,9 @@ async fn send_signed_request<T: DeserializeOwned>(
let status = res.status();
let headers = res.headers().clone();
let current_date = get_date_header(&headers);
let body = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request { source, step }
})?;
@@ -926,7 +1052,21 @@ async fn send_signed_request<T: DeserializeOwned>(
status_code: status,
}
})?;
Ok((headers, body))
Ok(SignedRequestResponse {
headers,
current_date,
body,
})
}
#[tracing::instrument]
fn get_date_header(headers: &HeaderMap) -> DateTime<Utc> {
headers
.get(reqwest::header::DATE)
.and_then(|x| x.to_str().ok())
.and_then(|x| DateTime::parse_from_rfc2822(x).ok())
.map(|x| x.with_timezone(&Utc))
.unwrap_or(Utc::now())
}
#[tracing::instrument]

View File

@@ -73,10 +73,6 @@ async function loginMinecraft() {
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
<p>
Signing into Microsoft account is a complex task for the launchers, and there are a lot
of things can go wrong.
</p>
<template v-if="metadata.network">
<h3>Network issues</h3>
<p>
@@ -106,23 +102,26 @@ async function loginMinecraft() {
</p>
</template>
<template v-else>
<h3>Make sure you are signing into the right Microsoft account</h3>
<h3>Try another Microsoft account</h3>
<p>
More often than not, this error is caused by you signing into an incorrect Microsoft
account which isn't linked to Minecraft. Double check and try again!
Double check you've signed in with the right account. You may own Minecraft on a
different Microsoft account.
</p>
<h3>Try signing in and launching through the official launcher first</h3>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Try another account
</button>
</div>
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
<p>
If you just bought Minecraft, are coming from the Bedrock Edition world and have never
played Java before, or just subscribed to PC Game Pass, you would need to start the
game at least once using the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>.
Once you're done, come back here and sign in!
Try signing in with the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
first. Once you're done, come back here and sign in!
</p>
</template>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Sign in to Minecraft
<LogInIcon /> Try signing in again
</button>
</div>
<hr />
@@ -168,7 +167,7 @@ async function loginMinecraft() {
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
<button class="btn" @clicdck="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>