You've already forked AstralRinth
forked from didirus/AstralRinth
feat(app): better external browser Modrinth login flow (#4033)
* fix(app-frontend): do not emit exceptions when no loaders are available * refactor(app): simplify Microsoft login code without functional changes * feat(app): external browser auth flow for Modrinth account login * chore: address Clippy lint * chore(app/oauth_utils): simplify `handle_reply` error handling according to review * chore(app-lib): simplify `Url` usage out of MC auth module
This commit is contained in:
committed by
GitHub
parent
0e0ca1971a
commit
32793c50e1
@@ -85,21 +85,18 @@ pub struct MinecraftLoginFlow {
|
||||
pub verifier: String,
|
||||
pub challenge: String,
|
||||
pub session_id: String,
|
||||
pub redirect_uri: String,
|
||||
pub auth_request_uri: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn login_begin(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<MinecraftLoginFlow> {
|
||||
let (pair, current_date, valid_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
||||
.await?;
|
||||
let (pair, current_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||
|
||||
let verifier = generate_oauth_challenge();
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(&verifier);
|
||||
let result = hasher.finalize();
|
||||
let result = sha2::Sha256::digest(&verifier);
|
||||
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
|
||||
|
||||
match sisu_authenticate(
|
||||
@@ -110,46 +107,15 @@ pub async fn login_begin(
|
||||
)
|
||||
.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 (pair, current_date, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(
|
||||
Utc::now(),
|
||||
false,
|
||||
exec,
|
||||
)
|
||||
.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 (session_id, redirect_uri) = sisu_authenticate(
|
||||
&pair.token.token,
|
||||
&challenge,
|
||||
&pair.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())
|
||||
}
|
||||
Ok((session_id, redirect_uri)) => {
|
||||
return Ok(MinecraftLoginFlow {
|
||||
verifier,
|
||||
challenge,
|
||||
session_id,
|
||||
auth_request_uri: redirect_uri.value.msa_oauth_redirect,
|
||||
});
|
||||
}
|
||||
Err(err) => return Err(crate::ErrorKind::from(err).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,9 +125,8 @@ pub async fn login_finish(
|
||||
flow: MinecraftLoginFlow,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<Credentials> {
|
||||
let (pair, _, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), false, exec)
|
||||
.await?;
|
||||
let (pair, _) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(Utc::now(), exec).await?;
|
||||
|
||||
let oauth_token = oauth_token(code, &flow.verifier).await?;
|
||||
let sisu_authorize = sisu_authorize(
|
||||
@@ -267,10 +232,9 @@ impl Credentials {
|
||||
}
|
||||
|
||||
let oauth_token = oauth_refresh(&self.refresh_token).await?;
|
||||
let (pair, current_date, _) =
|
||||
let (pair, current_date) =
|
||||
DeviceTokenPair::refresh_and_get_device_token(
|
||||
oauth_token.date,
|
||||
false,
|
||||
exec,
|
||||
)
|
||||
.await?;
|
||||
@@ -633,21 +597,20 @@ impl DeviceTokenPair {
|
||||
#[tracing::instrument(skip(exec))]
|
||||
async fn refresh_and_get_device_token(
|
||||
current_date: DateTime<Utc>,
|
||||
force_generate: bool,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||
) -> crate::Result<(Self, DateTime<Utc>, bool)> {
|
||||
) -> crate::Result<(Self, DateTime<Utc>)> {
|
||||
let pair = Self::get(exec).await?;
|
||||
|
||||
if let Some(mut pair) = pair {
|
||||
if pair.token.not_after > Utc::now() && !force_generate {
|
||||
Ok((pair, current_date, false))
|
||||
if pair.token.not_after > current_date {
|
||||
Ok((pair, current_date))
|
||||
} else {
|
||||
let res = device_token(&pair.key, current_date).await?;
|
||||
|
||||
pair.token = res.value;
|
||||
pair.upsert(exec).await?;
|
||||
|
||||
Ok((pair, res.date, true))
|
||||
Ok((pair, res.date))
|
||||
}
|
||||
} else {
|
||||
let key = generate_key()?;
|
||||
@@ -660,7 +623,7 @@ impl DeviceTokenPair {
|
||||
|
||||
pair.upsert(exec).await?;
|
||||
|
||||
Ok((pair, res.date, true))
|
||||
Ok((pair, res.date))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,8 +721,8 @@ impl DeviceTokenPair {
|
||||
}
|
||||
|
||||
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";
|
||||
const AUTH_REPLY_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||
const REQUESTED_SCOPE: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||
|
||||
struct RequestWithDate<T> {
|
||||
pub date: DateTime<Utc>,
|
||||
@@ -838,7 +801,7 @@ async fn sisu_authenticate(
|
||||
"AppId": MICROSOFT_CLIENT_ID,
|
||||
"DeviceToken": token,
|
||||
"Offers": [
|
||||
REQUESTED_SCOPES
|
||||
REQUESTED_SCOPE
|
||||
],
|
||||
"Query": {
|
||||
"code_challenge": challenge,
|
||||
@@ -846,7 +809,7 @@ async fn sisu_authenticate(
|
||||
"state": generate_oauth_challenge(),
|
||||
"prompt": "select_account"
|
||||
},
|
||||
"RedirectUri": REDIRECT_URL,
|
||||
"RedirectUri": AUTH_REPLY_URL,
|
||||
"Sandbox": "RETAIL",
|
||||
"TokenType": "code",
|
||||
"TitleId": "1794566092",
|
||||
@@ -890,12 +853,12 @@ async fn oauth_token(
|
||||
verifier: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
query.insert("code", code);
|
||||
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");
|
||||
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||
query.insert("scope", REQUESTED_SCOPE);
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
@@ -939,11 +902,11 @@ async fn oauth_refresh(
|
||||
refresh_token: &str,
|
||||
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
|
||||
let mut query = HashMap::new();
|
||||
query.insert("client_id", "00000000402b5328");
|
||||
query.insert("client_id", MICROSOFT_CLIENT_ID);
|
||||
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");
|
||||
query.insert("redirect_uri", AUTH_REPLY_URL);
|
||||
query.insert("scope", REQUESTED_SCOPE);
|
||||
|
||||
let res = auth_retry(|| {
|
||||
REQWEST_CLIENT
|
||||
@@ -1007,7 +970,7 @@ async fn sisu_authorize(
|
||||
"/authorize",
|
||||
json!({
|
||||
"AccessToken": format!("t={access_token}"),
|
||||
"AppId": "00000000402b5328",
|
||||
"AppId": MICROSOFT_CLIENT_ID,
|
||||
"DeviceToken": device_token,
|
||||
"ProofKey": {
|
||||
"kty": "EC",
|
||||
|
||||
@@ -190,7 +190,7 @@ impl ModrinthCredentials {
|
||||
}
|
||||
|
||||
pub const fn get_login_url() -> &'static str {
|
||||
concat!(env!("MODRINTH_URL"), "auth/sign-in?launcher=true")
|
||||
concat!(env!("MODRINTH_URL"), "auth/sign-in")
|
||||
}
|
||||
|
||||
pub async fn finish_login_flow(
|
||||
@@ -198,6 +198,12 @@ pub async fn finish_login_flow(
|
||||
semaphore: &FetchSemaphore,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
|
||||
) -> crate::Result<ModrinthCredentials> {
|
||||
// The authorization code actually is the access token, since Labrinth doesn't
|
||||
// issue separate authorization codes. Therefore, this is equivalent to an
|
||||
// implicit OAuth grant flow, and no additional exchanging or finalization is
|
||||
// needed. TODO not do this for the reasons outlined at
|
||||
// https://oauth.net/2/grant-types/implicit/
|
||||
|
||||
let info = fetch_info(code, semaphore, exec).await?;
|
||||
|
||||
Ok(ModrinthCredentials {
|
||||
|
||||
Reference in New Issue
Block a user