Switch to PKSE OAuth impl (#1146)

* Auth pkse

* add additional fields

* fix actions

* fix lint

* Purge broken auth + bump version
This commit is contained in:
Geometrically 2024-04-24 21:27:25 -07:00 committed by GitHub
parent e9e99956ad
commit deedf4fc8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 50 additions and 17 deletions

View File

@ -23,7 +23,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt, clippy components: rustfmt, clippy
targets: aarch64-apple-darwin targets: aarch64-apple-darwin, x86_64-apple-darwin
- name: Rust setup - name: Rust setup
if: "!startsWith(matrix.platform, 'macos')" if: "!startsWith(matrix.platform, 'macos')"

4
Cargo.lock generated
View File

@ -5074,7 +5074,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus" name = "theseus"
version = "0.7.1" version = "0.7.2"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"async-tungstenite", "async-tungstenite",
@ -5126,7 +5126,7 @@ dependencies = [
[[package]] [[package]]
name = "theseus_gui" name = "theseus_gui"
version = "0.7.1" version = "0.7.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"cocoa 0.25.0", "cocoa 0.25.0",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "theseus" name = "theseus"
version = "0.7.1" version = "0.7.2"
authors = ["Jai A <jaiagr+gpg@pm.me>"] authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018" edition = "2018"

View File

@ -4,7 +4,7 @@ use crate::State;
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD}; use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
use base64::Engine; use base64::Engine;
use byteorder::BigEndian; use byteorder::BigEndian;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, NaiveDate, Utc};
use p256::ecdsa::signature::Signer; use p256::ecdsa::signature::Signer;
use p256::ecdsa::{Signature, SigningKey, VerifyingKey}; use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding}; use p256::pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
@ -15,6 +15,7 @@ use reqwest::Response;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use sha2::Digest;
use std::collections::HashMap; use std::collections::HashMap;
use std::future::Future; use std::future::Future;
use uuid::Uuid; use uuid::Uuid;
@ -84,6 +85,7 @@ pub struct SaveDeviceToken {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct MinecraftLoginFlow { pub struct MinecraftLoginFlow {
pub verifier: String,
pub challenge: String, pub challenge: String,
pub session_id: String, pub session_id: String,
pub redirect_uri: String, pub redirect_uri: String,
@ -157,6 +159,22 @@ impl MinecraftAuthStore {
} }
let (key, token) = if let Some(ref token) = self.token { 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.token.issue_instant
< DateTime::parse_from_rfc3339(
"2024-04-25T23:59:59.999999999Z",
)
.unwrap()
{
return Ok(generate_key!(
self,
generate_key,
device_token,
SaveDeviceToken
));
}
if token.token.not_after > Utc::now() { if token.token.not_after > Utc::now() {
if let Ok(private_key) = if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key) SigningKey::from_pkcs8_pem(&token.private_key)
@ -192,11 +210,17 @@ impl MinecraftAuthStore {
pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> { pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> {
let (key, token) = self.refresh_and_get_device_token().await?; let (key, token) = self.refresh_and_get_device_token().await?;
let challenge = generate_oauth_challenge(); 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) = let (session_id, redirect_uri) =
sisu_authenticate(&token.token, &challenge, &key).await?; sisu_authenticate(&token.token, &challenge, &key).await?;
Ok(MinecraftLoginFlow { Ok(MinecraftLoginFlow {
verifier,
challenge, challenge,
session_id, session_id,
redirect_uri: redirect_uri.msa_oauth_redirect, redirect_uri: redirect_uri.msa_oauth_redirect,
@ -211,7 +235,7 @@ impl MinecraftAuthStore {
) -> crate::Result<Credentials> { ) -> crate::Result<Credentials> {
let (key, token) = self.refresh_and_get_device_token().await?; let (key, token) = self.refresh_and_get_device_token().await?;
let oauth_token = oauth_token(code, &flow.challenge).await?; let oauth_token = oauth_token(code, &flow.verifier).await?;
let sisu_authorize = sisu_authorize( let sisu_authorize = sisu_authorize(
Some(&flow.session_id), Some(&flow.session_id),
&oauth_token.access_token, &oauth_token.access_token,
@ -403,13 +427,14 @@ async fn sisu_authenticate(
], ],
"Query": { "Query": {
"code_challenge": challenge, "code_challenge": challenge,
"code_challenge_method": "plain", "code_challenge_method": "S256",
"state": "", "state": generate_oauth_challenge(),
"prompt": "select_account" "prompt": "select_account"
}, },
"RedirectUri": REDIRECT_URL, "RedirectUri": REDIRECT_URL,
"Sandbox": "RETAIL", "Sandbox": "RETAIL",
"TokenType": "code", "TokenType": "code",
"TitleId": 1794566092,
}), }),
key, key,
MinecraftAuthStep::SisuAuthenicate, MinecraftAuthStep::SisuAuthenicate,
@ -439,12 +464,12 @@ struct OAuthToken {
#[tracing::instrument] #[tracing::instrument]
async fn oauth_token( async fn oauth_token(
code: &str, code: &str,
challenge: &str, verifier: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> { ) -> Result<OAuthToken, MinecraftAuthenticationError> {
let mut query = HashMap::new(); let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328"); query.insert("client_id", "00000000402b5328");
query.insert("code", code); query.insert("code", code);
query.insert("code_verifier", challenge); query.insert("code_verifier", &*verifier);
query.insert("grant_type", "authorization_code"); query.insert("grant_type", "authorization_code");
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf"); query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL"); query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
@ -555,6 +580,8 @@ async fn sisu_authorize(
"Sandbox": "RETAIL", "Sandbox": "RETAIL",
"SessionId": session_id, "SessionId": session_id,
"SiteName": "user.auth.xboxlive.com", "SiteName": "user.auth.xboxlive.com",
"RelyingParty": "http://xboxlive.com",
"UseModernGamertag": "true"
}), }),
key, key,
MinecraftAuthStep::SisuAuthorize, MinecraftAuthStep::SisuAuthorize,
@ -854,9 +881,12 @@ async fn send_signed_request<T: DeserializeOwned>(
.post(url) .post(url)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Accept", "application/json") .header("Accept", "application/json")
.header("x-xbl-contract-version", "1")
.header("signature", &signature); .header("signature", &signature);
if url != "https://sisu.xboxlive.com/authorize" {
request = request.header("x-xbl-contract-version", "1");
}
if let Some(auth) = authorization { if let Some(auth) = authorization {
request = request.header("Authorization", auth); request = request.header("Authorization", auth);
} }
@ -885,6 +915,6 @@ async fn send_signed_request<T: DeserializeOwned>(
fn generate_oauth_challenge() -> String { fn generate_oauth_challenge() -> String {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect(); let bytes: Vec<u8> = (0..64).map(|_| rng.gen()).collect();
bytes.iter().map(|byte| format!("{:02x}", byte)).collect() bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
} }

View File

@ -1,7 +1,7 @@
{ {
"name": "theseus_gui", "name": "theseus_gui",
"private": true, "private": true,
"version": "0.7.1", "version": "0.7.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "theseus_gui" name = "theseus_gui"
version = "0.7.1" version = "0.7.2"
description = "A Tauri App" description = "A Tauri App"
authors = ["you"] authors = ["you"]
license = "" license = ""

View File

@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Modrinth App", "productName": "Modrinth App",
"version": "0.7.1" "version": "0.7.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {

View File

@ -23,7 +23,10 @@ defineExpose({
supportLink.value = supportLink.value =
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues' 'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
if (errorVal.message.includes('existing connection was forcibly closed')) { if (
errorVal.message.includes('existing connection was forcibly closed') ||
errorVal.message.includes('error sending request for url')
) {
metadata.value.network = true metadata.value.network = true
} }
if (errorVal.message.includes('because the target machine actively refused it')) { if (errorVal.message.includes('because the target machine actively refused it')) {