use authorize oauth2 instead of devicecode oauth2
This commit is contained in:
parent
c27688c8d4
commit
41801010a4
12 changed files with 652 additions and 182 deletions
|
|
@ -45,10 +45,13 @@ impl Component for Accounts {
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let Some(login) = login else { return };
|
let Some(login) = login else { return };
|
||||||
if login_code_already_handled(&login.user_code) {
|
if login_code_already_handled(login.dedupe_key()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handled_code.set(Some(login.user_code.clone()));
|
handled_code.set(Some(login.dedupe_key().to_string()));
|
||||||
|
if let Some(url) = login.browser_url() {
|
||||||
|
platform::open_url(url);
|
||||||
|
}
|
||||||
finish.mutate(login.clone());
|
finish.mutate(login.clone());
|
||||||
pending_login.set(Some(login));
|
pending_login.set(Some(login));
|
||||||
});
|
});
|
||||||
|
|
@ -511,9 +514,18 @@ fn microsoft_dialog(
|
||||||
mut pending_login: State<Option<MinecraftLogin>>,
|
mut pending_login: State<Option<MinecraftLogin>>,
|
||||||
mut handled_code: State<Option<String>>,
|
mut handled_code: State<Option<String>>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let code = login.user_code.clone();
|
let body = match &login {
|
||||||
let copy_code = code.clone();
|
MinecraftLogin::DeviceCode(flow) => {
|
||||||
let verification_uri = login.verification_uri.clone();
|
device_code_dialog_body(flow.user_code.clone(), flow.verification_uri.clone())
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
MinecraftLogin::Browser(flow) => {
|
||||||
|
browser_dialog_body(flow.auth_url.clone()).into_element()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut close_pending = pending_login;
|
||||||
|
let mut close_handled = handled_code;
|
||||||
|
|
||||||
OverlayPopup::new()
|
OverlayPopup::new()
|
||||||
.on_close(move |()| {
|
.on_close(move |()| {
|
||||||
|
|
@ -543,76 +555,14 @@ fn microsoft_dialog(
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(body)
|
||||||
label()
|
.child(waiting_row())
|
||||||
.text("Enter this code on the Microsoft sign-in page:")
|
|
||||||
.font_size(13.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::fill())
|
|
||||||
.center()
|
|
||||||
.padding(Gaps::new_symmetric(20., 16.))
|
|
||||||
.corner_radius(CornerRadius::new_all(14.))
|
|
||||||
.background(colors::component_bg())
|
|
||||||
.border(border_all_color(1., colors::brand()))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(code)
|
|
||||||
.font_size(48.)
|
|
||||||
.font_weight(FontWeight::BOLD)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.main_align(Alignment::Center)
|
|
||||||
.spacing(8.)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
|
||||||
tracing::warn!("clipboard copy failed: {err:?}");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Copy01).size(16.))
|
|
||||||
.text("Copy code"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.primary()
|
|
||||||
.on_press(move |_| platform::open_url(&verification_uri))
|
|
||||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
|
||||||
.text("Open in browser"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.child(
|
|
||||||
Icon::new(IconType::Loading02)
|
|
||||||
.size(13.)
|
|
||||||
.color(colors::brand()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("Waiting for you to finish signing in...")
|
|
||||||
.font_size(12.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_press(move |_| {
|
.on_press(move |_| {
|
||||||
pending_login.set(None);
|
close_pending.set(None);
|
||||||
handled_code.set(None);
|
close_handled.set(None);
|
||||||
})
|
})
|
||||||
.text("Cancel"),
|
.text("Cancel"),
|
||||||
),
|
),
|
||||||
|
|
@ -621,6 +571,104 @@ fn microsoft_dialog(
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn waiting_row() -> impl IntoElement {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(6.)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::Loading02)
|
||||||
|
.size(13.)
|
||||||
|
.color(colors::brand()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Waiting for you to finish signing in...")
|
||||||
|
.font_size(12.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(18.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Your browser has opened the Microsoft sign-in page. Complete sign-in there and you'll be brought back automatically.")
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| platform::open_url(&auth_url))
|
||||||
|
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||||
|
.text("Open in browser again"),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoElement {
|
||||||
|
let copy_code = code.clone();
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(18.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Enter this code on the Microsoft sign-in page:")
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.center()
|
||||||
|
.padding(Gaps::new_symmetric(20., 16.))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::brand()))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(code)
|
||||||
|
.font_size(48.)
|
||||||
|
.font_weight(FontWeight::BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::Center)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.on_press(move |_| {
|
||||||
|
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
||||||
|
tracing::warn!("clipboard copy failed: {err:?}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconType::Copy01).size(16.))
|
||||||
|
.text("Copy code"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| platform::open_url(&verification_uri))
|
||||||
|
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||||
|
.text("Open in browser"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn kind_badge(kind: AccountKind) -> impl IntoElement {
|
fn kind_badge(kind: AccountKind) -> impl IntoElement {
|
||||||
let (icon, text) = match kind {
|
let (icon, text) = match kind {
|
||||||
AccountKind::Microsoft => (IconType::Globe01, "Microsoft"),
|
AccountKind::Microsoft => (IconType::Globe01, "Microsoft"),
|
||||||
|
|
|
||||||
|
|
@ -19,18 +19,27 @@ impl Component for SettingsLauncher {
|
||||||
let v = settings.discord_enabled;
|
let v = settings.discord_enabled;
|
||||||
move || v
|
move || v
|
||||||
});
|
});
|
||||||
|
let browser_login = use_state({
|
||||||
|
let v = settings.microsoft_login_use_browser;
|
||||||
|
move || v
|
||||||
|
});
|
||||||
|
|
||||||
let mut first = use_state(|| true);
|
let mut first = use_state(|| true);
|
||||||
use_side_effect(move || {
|
{
|
||||||
let enabled = *discord_rpc.read();
|
let settings = settings.clone();
|
||||||
if *first.peek() {
|
use_side_effect(move || {
|
||||||
first.set(false);
|
let discord = *discord_rpc.read();
|
||||||
return;
|
let browser = *browser_login.read();
|
||||||
}
|
if *first.peek() {
|
||||||
let mut next = settings.clone();
|
first.set(false);
|
||||||
next.discord_enabled = enabled;
|
return;
|
||||||
dispatch.set_settings(next);
|
}
|
||||||
});
|
let mut next = settings.clone();
|
||||||
|
next.discord_enabled = discord;
|
||||||
|
next.microsoft_login_use_browser = browser;
|
||||||
|
dispatch.set_settings(next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let folder = data_dir.clone();
|
let folder = data_dir.clone();
|
||||||
let open_folder = link_button().on_press(move |_| platform::open_url(&folder));
|
let open_folder = link_button().on_press(move |_| platform::open_url(&folder));
|
||||||
|
|
@ -43,6 +52,12 @@ impl Component for SettingsLauncher {
|
||||||
"Enable Discord Rich Presence.",
|
"Enable Discord Rich Presence.",
|
||||||
toggle(discord_rpc),
|
toggle(discord_rpc),
|
||||||
))
|
))
|
||||||
|
.child(settings_row(
|
||||||
|
IconType::Globe01,
|
||||||
|
"Browser sign-in",
|
||||||
|
"Sign in to Microsoft by opening your browser instead of entering a code.",
|
||||||
|
toggle(browser_login),
|
||||||
|
))
|
||||||
.child(section_header("FOLDERS AND FILES"))
|
.child(section_header("FOLDERS AND FILES"))
|
||||||
.child(settings_row(
|
.child(settings_row(
|
||||||
IconType::Folder,
|
IconType::Folder,
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,14 @@ impl Component for OnboardingAccount {
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let Some(login) = login else { return };
|
let Some(login) = login else { return };
|
||||||
if login_code_already_handled(&login.user_code) {
|
if login_code_already_handled(login.dedupe_key()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Browser flow: open the system browser straight away so the user
|
||||||
|
// lands on the Microsoft sign-in page without copying a code.
|
||||||
|
if let Some(url) = login.browser_url() {
|
||||||
|
platform::open_url(url);
|
||||||
|
}
|
||||||
finish.mutate(login.clone());
|
finish.mutate(login.clone());
|
||||||
pending_login.set(Some(login));
|
pending_login.set(Some(login));
|
||||||
});
|
});
|
||||||
|
|
@ -205,9 +210,15 @@ fn microsoft_dialog(
|
||||||
login: MinecraftLogin,
|
login: MinecraftLogin,
|
||||||
mut pending_login: State<Option<MinecraftLogin>>,
|
mut pending_login: State<Option<MinecraftLogin>>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let code = login.user_code.clone();
|
let body = match &login {
|
||||||
let copy_code = code.clone();
|
MinecraftLogin::DeviceCode(flow) => {
|
||||||
let verification_uri = login.verification_uri.clone();
|
device_code_dialog_body(flow.user_code.clone(), flow.verification_uri.clone())
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
MinecraftLogin::Browser(flow) => {
|
||||||
|
browser_dialog_body(flow.auth_url.clone()).into_element()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
OverlayPopup::new()
|
OverlayPopup::new()
|
||||||
.on_close(move |()| pending_login.set(None))
|
.on_close(move |()| pending_login.set(None))
|
||||||
|
|
@ -234,70 +245,8 @@ fn microsoft_dialog(
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(body)
|
||||||
label()
|
.child(waiting_row())
|
||||||
.text("Enter this code on the Microsoft sign-in page:")
|
|
||||||
.font_size(13.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::fill())
|
|
||||||
.center()
|
|
||||||
.padding(Gaps::new_symmetric(20., 16.))
|
|
||||||
.corner_radius(CornerRadius::new_all(14.))
|
|
||||||
.background(colors::component_bg())
|
|
||||||
.border(border_all_color(1., colors::brand()))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(code)
|
|
||||||
.font_size(48.)
|
|
||||||
.font_weight(FontWeight::BOLD)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.main_align(Alignment::Center)
|
|
||||||
.spacing(8.)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
|
||||||
tracing::warn!("clipboard copy failed: {err:?}");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Copy01).size(16.))
|
|
||||||
.text("Copy code"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new()
|
|
||||||
.primary()
|
|
||||||
.on_press(move |_| platform::open_url(&verification_uri))
|
|
||||||
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
|
||||||
.text("Open in browser"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.child(
|
|
||||||
Icon::new(IconType::Loading02)
|
|
||||||
.size(13.)
|
|
||||||
.color(colors::brand()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("Waiting for you to finish signing in...")
|
|
||||||
.font_size(12.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.ghost()
|
.ghost()
|
||||||
|
|
@ -308,3 +257,101 @@ fn microsoft_dialog(
|
||||||
)
|
)
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn waiting_row() -> impl IntoElement {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(6.)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::Loading02)
|
||||||
|
.size(13.)
|
||||||
|
.color(colors::brand()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Waiting for you to finish signing in...")
|
||||||
|
.font_size(12.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn browser_dialog_body(auth_url: String) -> impl IntoElement {
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(18.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Your browser has opened the Microsoft sign-in page. Complete sign-in there and you'll be brought back automatically.")
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| platform::open_url(&auth_url))
|
||||||
|
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||||
|
.text("Open in browser again"),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_code_dialog_body(code: String, verification_uri: String) -> impl IntoElement {
|
||||||
|
let copy_code = code.clone();
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(18.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Enter this code on the Microsoft sign-in page:")
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.center()
|
||||||
|
.padding(Gaps::new_symmetric(20., 16.))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::brand()))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(code)
|
||||||
|
.font_size(48.)
|
||||||
|
.font_weight(FontWeight::BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::Center)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.on_press(move |_| {
|
||||||
|
if let Err(err) = Clipboard::set(copy_code.clone()) {
|
||||||
|
tracing::warn!("clipboard copy failed: {err:?}");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconType::Copy01).size(16.))
|
||||||
|
.text("Copy code"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| platform::open_url(&verification_uri))
|
||||||
|
.child(Icon::new(IconType::LinkExternal01).size(16.))
|
||||||
|
.text("Open in browser"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use oneclient_core::auth;
|
use oneclient_core::auth::{self, MinecraftLogin};
|
||||||
use oneclient_core::{dev, logger, LauncherResult};
|
use oneclient_core::{dev, logger, LauncherResult};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -11,11 +11,18 @@ async fn main() -> LauncherResult<()> {
|
||||||
|
|
||||||
let flow = auth::begin_microsoft_login().await?;
|
let flow = auth::begin_microsoft_login().await?;
|
||||||
|
|
||||||
println!("{}\n", flow.message);
|
match &flow {
|
||||||
println!("Verification URL: {}", flow.verification_uri);
|
MinecraftLogin::DeviceCode(device) => {
|
||||||
println!("Enter this code: {}\n", flow.user_code);
|
println!("{}\n", device.message);
|
||||||
|
println!("Verification URL: {}", device.verification_uri);
|
||||||
try_open_browser(&flow.verification_uri);
|
println!("Enter this code: {}\n", device.user_code);
|
||||||
|
try_open_browser(&device.verification_uri);
|
||||||
|
}
|
||||||
|
MinecraftLogin::Browser(browser) => {
|
||||||
|
println!("Opening browser to sign in...\n");
|
||||||
|
try_open_browser(&browser.auth_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!("Waiting for you to sign in...");
|
println!("Waiting for you to sign in...");
|
||||||
let account = auth::finish_microsoft_login(flow).await?;
|
let account = auth::finish_microsoft_login(flow).await?;
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ impl MinecraftAccount {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
pub struct MinecraftLogin {
|
pub struct DeviceCodeLogin {
|
||||||
pub user_code: String,
|
pub user_code: String,
|
||||||
pub device_code: String,
|
pub device_code: String,
|
||||||
pub verification_uri: String,
|
pub verification_uri: String,
|
||||||
|
|
@ -43,3 +43,32 @@ pub struct MinecraftLogin {
|
||||||
pub interval: u64,
|
pub interval: u64,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct BrowserLogin {
|
||||||
|
pub auth_url: String,
|
||||||
|
pub state: String,
|
||||||
|
pub redirect_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum MinecraftLogin {
|
||||||
|
DeviceCode(DeviceCodeLogin),
|
||||||
|
Browser(BrowserLogin),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinecraftLogin {
|
||||||
|
pub fn dedupe_key(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
MinecraftLogin::DeviceCode(flow) => &flow.user_code,
|
||||||
|
MinecraftLogin::Browser(flow) => &flow.state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn browser_url(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
MinecraftLogin::Browser(flow) => Some(&flow.auth_url),
|
||||||
|
MinecraftLogin::DeviceCode(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use crate::http::RequestError;
|
||||||
pub enum MinecraftAuthStep {
|
pub enum MinecraftAuthStep {
|
||||||
DeviceCodeRequest,
|
DeviceCodeRequest,
|
||||||
DeviceCodePoll,
|
DeviceCodePoll,
|
||||||
|
AuthCodeExchange,
|
||||||
RefreshToken,
|
RefreshToken,
|
||||||
XblAuthenticate,
|
XblAuthenticate,
|
||||||
XstsAuthorize,
|
XstsAuthorize,
|
||||||
|
|
@ -46,6 +47,14 @@ pub enum MinecraftAuthError {
|
||||||
DeviceAuthorizationExpired,
|
DeviceAuthorizationExpired,
|
||||||
#[error("device authorization failed: {error}")]
|
#[error("device authorization failed: {error}")]
|
||||||
DeviceAuthorizationFailed { error: String },
|
DeviceAuthorizationFailed { error: String },
|
||||||
|
#[error("failed to start the loopback redirect server: {0}")]
|
||||||
|
LoopbackBind(String),
|
||||||
|
#[error("browser sign-in was cancelled or timed out before completing")]
|
||||||
|
BrowserAuthorizationExpired,
|
||||||
|
#[error("this browser sign-in is no longer active")]
|
||||||
|
BrowserLoginNotFound,
|
||||||
|
#[error("browser authorization failed: {error}")]
|
||||||
|
BrowserAuthorizationFailed { error: String },
|
||||||
#[error("failed to read user hash from Xbox token")]
|
#[error("failed to read user hash from Xbox token")]
|
||||||
HashError,
|
HashError,
|
||||||
#[error("Minecraft authentication error {error_code} during MSA step {step:?}: {message}")]
|
#[error("Minecraft authentication error {error_code} during MSA step {step:?}: {message}")]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ mod data;
|
||||||
pub use error::{AuthError, MinecraftAuthError, MinecraftAuthStep};
|
pub use error::{AuthError, MinecraftAuthError, MinecraftAuthStep};
|
||||||
pub use offline::{offline_account, offline_uuid, validate_offline_username};
|
pub use offline::{offline_account, offline_uuid, validate_offline_username};
|
||||||
pub use store::CredentialsStore;
|
pub use store::CredentialsStore;
|
||||||
pub use data::{AccountKind, MinecraftAccount, MinecraftLogin};
|
pub use data::{AccountKind, BrowserLogin, DeviceCodeLogin, MinecraftAccount, MinecraftLogin};
|
||||||
|
pub use msa::PendingBrowserLogin;
|
||||||
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -16,14 +17,60 @@ use crate::state::LauncherState;
|
||||||
|
|
||||||
pub async fn begin_microsoft_login() -> LauncherResult<MinecraftLogin> {
|
pub async fn begin_microsoft_login() -> LauncherResult<MinecraftLogin> {
|
||||||
let state = LauncherState::get()?;
|
let state = LauncherState::get()?;
|
||||||
let mut auth = state.auth.lock().await;
|
let use_browser = state.settings.read().microsoft_login_use_browser;
|
||||||
auth.begin_microsoft_login(&state.services).await
|
|
||||||
|
if use_browser {
|
||||||
|
let (login, pending) = msa::begin_browser_login().await.map_err(AuthError::from)?;
|
||||||
|
state
|
||||||
|
.microsoft_logins
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(login.state.clone(), pending);
|
||||||
|
Ok(MinecraftLogin::Browser(login))
|
||||||
|
} else {
|
||||||
|
let client = state.services.requester.http();
|
||||||
|
let flow = msa::begin_device_login(client).await.map_err(AuthError::from)?;
|
||||||
|
Ok(MinecraftLogin::DeviceCode(flow))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_microsoft_login(flow: MinecraftLogin) -> LauncherResult<MinecraftAccount> {
|
pub async fn finish_microsoft_login(flow: MinecraftLogin) -> LauncherResult<MinecraftAccount> {
|
||||||
let state = LauncherState::get()?;
|
let state = LauncherState::get()?;
|
||||||
let mut auth = state.auth.lock().await;
|
|
||||||
auth.finish_microsoft_login(&flow, &state.services).await
|
match flow {
|
||||||
|
MinecraftLogin::DeviceCode(flow) => {
|
||||||
|
let mut auth = state.auth.lock().await;
|
||||||
|
auth.finish_device_login(&flow, &state.services).await
|
||||||
|
}
|
||||||
|
MinecraftLogin::Browser(login) => {
|
||||||
|
let pending = state
|
||||||
|
.microsoft_logins
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.remove(&login.state)
|
||||||
|
.ok_or(AuthError::Minecraft(
|
||||||
|
MinecraftAuthError::BrowserLoginNotFound,
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let client = state.services.requester.http();
|
||||||
|
let progress_id = Uuid::new_v4();
|
||||||
|
let notifier = state.services.notifier.clone();
|
||||||
|
let account = msa::finish_browser_login(client, pending, |label, current, total| {
|
||||||
|
notifier.send_progress(&progress_id, label, current, total);
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::from)?;
|
||||||
|
|
||||||
|
let mut auth = state.auth.lock().await;
|
||||||
|
auth.commit_account(account, &state.services).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_microsoft_login(state_token: &str) {
|
||||||
|
if let Ok(state) = LauncherState::get() {
|
||||||
|
state.microsoft_logins.lock().await.remove(state_token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_offline_account(username: String) -> LauncherResult<MinecraftAccount> {
|
pub async fn add_offline_account(username: String) -> LauncherResult<MinecraftAccount> {
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,43 @@
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use base64::prelude::BASE64_URL_SAFE_NO_PAD;
|
||||||
|
use base64::Engine;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::de::Error as DeError;
|
use serde::de::Error as DeError;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::data::{AccountKind, MinecraftAccount, MinecraftLogin};
|
use super::data::{AccountKind, BrowserLogin, DeviceCodeLogin, MinecraftAccount};
|
||||||
use super::error::{MinecraftAuthError, MinecraftAuthStep};
|
use super::error::{MinecraftAuthError, MinecraftAuthStep};
|
||||||
use crate::constants::{MICROSOFT_CLIENT_ID, MINECRAFT_SCOPES};
|
use crate::constants::{MICROSOFT_CLIENT_ID, MINECRAFT_SCOPES};
|
||||||
|
|
||||||
const DEVICE_CODE_URL: &str =
|
const DEVICE_CODE_URL: &str =
|
||||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
|
||||||
|
|
||||||
|
const AUTHORIZE_URL: &str =
|
||||||
|
"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
|
||||||
|
|
||||||
const TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
const TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||||
|
|
||||||
const AUTH_STEPS: u64 = 5;
|
const AUTH_STEPS: u64 = 5;
|
||||||
|
|
||||||
pub async fn begin_login(client: &Client) -> Result<MinecraftLogin, MinecraftAuthError> {
|
const BROWSER_LOGIN_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn begin_device_login(client: &Client) -> Result<DeviceCodeLogin, MinecraftAuthError> {
|
||||||
request_device_code(client).await
|
request_device_code(client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_login(
|
pub async fn finish_device_login(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
flow: &MinecraftLogin,
|
flow: &DeviceCodeLogin,
|
||||||
on_progress: impl Fn(&str, u64, u64),
|
on_progress: impl Fn(&str, u64, u64),
|
||||||
) -> Result<MinecraftAccount, MinecraftAuthError> {
|
) -> Result<MinecraftAccount, MinecraftAuthError> {
|
||||||
on_progress("Waiting for sign-in", 0, AUTH_STEPS);
|
on_progress("Waiting for sign-in", 0, AUTH_STEPS);
|
||||||
|
|
@ -34,6 +46,258 @@ pub async fn finish_login(
|
||||||
account_from_msa_token(client, msa, on_progress).await
|
account_from_msa_token(client, msa, on_progress).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct PendingBrowserLogin {
|
||||||
|
listener: TcpListener,
|
||||||
|
pkce_verifier: String,
|
||||||
|
redirect_uri: String,
|
||||||
|
csrf_state: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn begin_browser_login() -> Result<(BrowserLogin, PendingBrowserLogin), MinecraftAuthError>
|
||||||
|
{
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0")
|
||||||
|
.await
|
||||||
|
.map_err(|err| MinecraftAuthError::LoopbackBind(err.to_string()))?;
|
||||||
|
let port = listener
|
||||||
|
.local_addr()
|
||||||
|
.map_err(|err| MinecraftAuthError::LoopbackBind(err.to_string()))?
|
||||||
|
.port();
|
||||||
|
let redirect_uri = format!("http://127.0.0.1:{port}");
|
||||||
|
|
||||||
|
let pkce_verifier = random_token();
|
||||||
|
let code_challenge = pkce_challenge(&pkce_verifier);
|
||||||
|
let csrf_state = random_token();
|
||||||
|
|
||||||
|
let mut url = Url::parse(AUTHORIZE_URL).expect("authorize url is valid");
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("client_id", MICROSOFT_CLIENT_ID)
|
||||||
|
.append_pair("response_type", "code")
|
||||||
|
.append_pair("redirect_uri", &redirect_uri)
|
||||||
|
.append_pair("response_mode", "query")
|
||||||
|
.append_pair("scope", MINECRAFT_SCOPES)
|
||||||
|
.append_pair("state", &csrf_state)
|
||||||
|
.append_pair("code_challenge", &code_challenge)
|
||||||
|
.append_pair("code_challenge_method", "S256")
|
||||||
|
.append_pair("prompt", "select_account");
|
||||||
|
|
||||||
|
let login = BrowserLogin {
|
||||||
|
auth_url: url.to_string(),
|
||||||
|
state: csrf_state.clone(),
|
||||||
|
redirect_uri: redirect_uri.clone(),
|
||||||
|
};
|
||||||
|
let pending = PendingBrowserLogin {
|
||||||
|
listener,
|
||||||
|
pkce_verifier,
|
||||||
|
redirect_uri,
|
||||||
|
csrf_state,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((login, pending))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn finish_browser_login(
|
||||||
|
client: &Client,
|
||||||
|
pending: PendingBrowserLogin,
|
||||||
|
on_progress: impl Fn(&str, u64, u64),
|
||||||
|
) -> Result<MinecraftAccount, MinecraftAuthError> {
|
||||||
|
on_progress("Waiting for sign-in", 0, AUTH_STEPS);
|
||||||
|
let code = tokio::time::timeout(BROWSER_LOGIN_TIMEOUT, wait_for_redirect(&pending))
|
||||||
|
.await
|
||||||
|
.map_err(|_| MinecraftAuthError::BrowserAuthorizationExpired)??;
|
||||||
|
|
||||||
|
let msa = exchange_auth_code(client, &code, &pending).await?;
|
||||||
|
account_from_msa_token(client, msa, on_progress).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_redirect(pending: &PendingBrowserLogin) -> Result<String, MinecraftAuthError> {
|
||||||
|
loop {
|
||||||
|
let (mut stream, _) = pending
|
||||||
|
.listener
|
||||||
|
.accept()
|
||||||
|
.await
|
||||||
|
.map_err(|err| MinecraftAuthError::LoopbackBind(err.to_string()))?;
|
||||||
|
|
||||||
|
let mut buf = [0u8; 2048];
|
||||||
|
let read = stream.read(&mut buf).await.unwrap_or(0);
|
||||||
|
let request = String::from_utf8_lossy(&buf[..read]);
|
||||||
|
let Some(target) = request.split_whitespace().nth(1) else {
|
||||||
|
respond(&mut stream, RedirectPage::Waiting).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !target.contains('?') {
|
||||||
|
respond(&mut stream, RedirectPage::Waiting).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let full = format!("http://127.0.0.1{target}");
|
||||||
|
let parsed = Url::parse(&full).ok();
|
||||||
|
let mut code = None;
|
||||||
|
let mut state = None;
|
||||||
|
let mut error = None;
|
||||||
|
if let Some(parsed) = &parsed {
|
||||||
|
for (key, value) in parsed.query_pairs() {
|
||||||
|
match key.as_ref() {
|
||||||
|
"code" => code = Some(value.into_owned()),
|
||||||
|
"state" => state = Some(value.into_owned()),
|
||||||
|
"error" => error = Some(value.into_owned()),
|
||||||
|
"error_description" => error = Some(value.into_owned()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(error) = error {
|
||||||
|
respond(&mut stream, RedirectPage::Failed).await;
|
||||||
|
return Err(MinecraftAuthError::BrowserAuthorizationFailed { error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.as_deref() != Some(pending.csrf_state.as_str()) {
|
||||||
|
respond(&mut stream, RedirectPage::Failed).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(code) = code {
|
||||||
|
respond(&mut stream, RedirectPage::Success).await;
|
||||||
|
return Ok(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(&mut stream, RedirectPage::Waiting).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum RedirectPage {
|
||||||
|
Waiting,
|
||||||
|
Success,
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn respond(stream: &mut tokio::net::TcpStream, page: RedirectPage) {
|
||||||
|
let body = redirect_page_html(page);
|
||||||
|
let response = format!(
|
||||||
|
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||||
|
body.len(),
|
||||||
|
body
|
||||||
|
);
|
||||||
|
let _ = stream.write_all(response.as_bytes()).await;
|
||||||
|
let _ = stream.flush().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redirect_page_html(page: RedirectPage) -> String {
|
||||||
|
let (accent, glyph, heading, detail) = match page {
|
||||||
|
RedirectPage::Waiting => (
|
||||||
|
"#2567d8",
|
||||||
|
// Spinning ring.
|
||||||
|
r#"<div class="ring"></div>"#,
|
||||||
|
"Signing you in",
|
||||||
|
"Finish signing in with Microsoft in this window.",
|
||||||
|
),
|
||||||
|
RedirectPage::Success => (
|
||||||
|
"#45de2b",
|
||||||
|
r#"<svg viewBox="0 0 24 24" class="glyph"><path d="M20 6 9 17l-5-5"/></svg>"#,
|
||||||
|
"You're signed in!",
|
||||||
|
"You can close this tab and return to OneClient.",
|
||||||
|
),
|
||||||
|
RedirectPage::Failed => (
|
||||||
|
"#ff0000",
|
||||||
|
r#"<svg viewBox="0 0 24 24" class="glyph"><path d="M18 6 6 18M6 6l12 12"/></svg>"#,
|
||||||
|
"Sign-in failed",
|
||||||
|
"Something went wrong. Close this tab and try again from OneClient.",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r##"<!doctype html><html lang="en"><head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>OneClient</title>
|
||||||
|
<style>
|
||||||
|
*{{box-sizing:border-box;margin:0;padding:0}}
|
||||||
|
:root{{--accent:{accent}}}
|
||||||
|
html,body{{height:100%}}
|
||||||
|
body{{
|
||||||
|
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||||
|
color:#111827;
|
||||||
|
background:#ffffff;
|
||||||
|
display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px;
|
||||||
|
}}
|
||||||
|
.card{{
|
||||||
|
position:relative;width:100%;max-width:420px;text-align:center;
|
||||||
|
padding:44px 36px 40px;border-radius:20px;
|
||||||
|
background:#ffffff;
|
||||||
|
border:1px solid #e5e7eb;
|
||||||
|
box-shadow:0 10px 30px rgba(17,24,39,.10);
|
||||||
|
animation:rise .5s cubic-bezier(.2,.8,.2,1) both;
|
||||||
|
}}
|
||||||
|
.badge{{width:76px;height:76px;margin:0 auto 26px;border-radius:50%;
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
background:color-mix(in srgb, var(--accent) 12%, transparent);
|
||||||
|
border:1px solid color-mix(in srgb, var(--accent) 40%, transparent)}}
|
||||||
|
.glyph{{width:34px;height:34px;fill:none;stroke:var(--accent);stroke-width:2.4;
|
||||||
|
stroke-linecap:round;stroke-linejoin:round;animation:pop .4s .15s cubic-bezier(.2,1.4,.3,1) both}}
|
||||||
|
.ring{{width:34px;height:34px;border-radius:50%;
|
||||||
|
border:3px solid rgba(37,103,216,.22);border-top-color:#2567d8;animation:spin .8s linear infinite}}
|
||||||
|
h1{{font-size:21px;font-weight:600;color:#111827;margin-bottom:10px}}
|
||||||
|
p{{font-size:14px;line-height:1.55;color:#6b7280}}
|
||||||
|
@keyframes spin{{to{{transform:rotate(360deg)}}}}
|
||||||
|
@keyframes rise{{from{{opacity:0;transform:translateY(10px)}}to{{opacity:1;transform:none}}}}
|
||||||
|
@keyframes pop{{from{{opacity:0;transform:scale(.6)}}to{{opacity:1;transform:none}}}}
|
||||||
|
</style></head>
|
||||||
|
<body>
|
||||||
|
<main class="card">
|
||||||
|
<div class="badge">{glyph}</div>
|
||||||
|
<h1>{heading}</h1>
|
||||||
|
<p>{detail}</p>
|
||||||
|
</main>
|
||||||
|
</body></html>"##
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn exchange_auth_code(
|
||||||
|
client: &Client,
|
||||||
|
code: &str,
|
||||||
|
pending: &PendingBrowserLogin,
|
||||||
|
) -> Result<MsaToken, MinecraftAuthError> {
|
||||||
|
let body = [
|
||||||
|
("client_id", MICROSOFT_CLIENT_ID),
|
||||||
|
("grant_type", "authorization_code"),
|
||||||
|
("code", code),
|
||||||
|
("redirect_uri", pending.redirect_uri.as_str()),
|
||||||
|
("code_verifier", pending.pkce_verifier.as_str()),
|
||||||
|
("scope", MINECRAFT_SCOPES),
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = auth_retry(|| {
|
||||||
|
client
|
||||||
|
.post(TOKEN_URL)
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.form(&body)
|
||||||
|
.send()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|source| MinecraftAuthError::RequestError {
|
||||||
|
step: MinecraftAuthStep::AuthCodeExchange,
|
||||||
|
source,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
parse_json_response(res, MinecraftAuthStep::AuthCodeExchange)
|
||||||
|
.await
|
||||||
|
.map(MsaToken::from_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn random_token() -> String {
|
||||||
|
let mut bytes = [0u8; 32];
|
||||||
|
bytes[..16].copy_from_slice(Uuid::new_v4().as_bytes());
|
||||||
|
bytes[16..].copy_from_slice(Uuid::new_v4().as_bytes());
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pkce_challenge(verifier: &str) -> String {
|
||||||
|
let digest = Sha256::digest(verifier.as_bytes());
|
||||||
|
BASE64_URL_SAFE_NO_PAD.encode(digest)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn refresh_microsoft_account(
|
pub async fn refresh_microsoft_account(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
creds: &MinecraftAccount,
|
creds: &MinecraftAccount,
|
||||||
|
|
@ -45,7 +309,7 @@ pub async fn refresh_microsoft_account(
|
||||||
Ok(account)
|
Ok(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn request_device_code(client: &Client) -> Result<MinecraftLogin, MinecraftAuthError> {
|
async fn request_device_code(client: &Client) -> Result<DeviceCodeLogin, MinecraftAuthError> {
|
||||||
let body = [
|
let body = [
|
||||||
("client_id", MICROSOFT_CLIENT_ID),
|
("client_id", MICROSOFT_CLIENT_ID),
|
||||||
("scope", MINECRAFT_SCOPES),
|
("scope", MINECRAFT_SCOPES),
|
||||||
|
|
@ -69,7 +333,7 @@ async fn request_device_code(client: &Client) -> Result<MinecraftLogin, Minecraf
|
||||||
|
|
||||||
async fn poll_device_token(
|
async fn poll_device_token(
|
||||||
client: &Client,
|
client: &Client,
|
||||||
flow: &MinecraftLogin,
|
flow: &DeviceCodeLogin,
|
||||||
) -> Result<MsaToken, MinecraftAuthError> {
|
) -> Result<MsaToken, MinecraftAuthError> {
|
||||||
let deadline = Utc::now() + chrono::TimeDelta::seconds(flow.expires_in as i64);
|
let deadline = Utc::now() + chrono::TimeDelta::seconds(flow.expires_in as i64);
|
||||||
let mut interval_secs = flow.interval.max(5);
|
let mut interval_secs = flow.interval.max(5);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use super::error::AuthError;
|
use super::error::AuthError;
|
||||||
use super::offline::{offline_account, validate_offline_username};
|
use super::offline::{offline_account, validate_offline_username};
|
||||||
use super::data::{AccountKind, MinecraftAccount, MinecraftLogin};
|
use super::data::{AccountKind, DeviceCodeLogin, MinecraftAccount};
|
||||||
use crate::paths;
|
use crate::paths;
|
||||||
use crate::state::LauncherServices;
|
use crate::state::LauncherServices;
|
||||||
use crate::LauncherResult;
|
use crate::LauncherResult;
|
||||||
|
|
@ -57,30 +57,28 @@ impl CredentialsStore {
|
||||||
self.users.get(&id)
|
self.users.get(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn begin_microsoft_login(
|
pub async fn finish_device_login(
|
||||||
&mut self,
|
&mut self,
|
||||||
services: &LauncherServices,
|
flow: &DeviceCodeLogin,
|
||||||
) -> LauncherResult<MinecraftLogin> {
|
|
||||||
let client = services.requester.http();
|
|
||||||
Ok(super::msa::begin_login(client)
|
|
||||||
.await
|
|
||||||
.map_err(AuthError::from)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn finish_microsoft_login(
|
|
||||||
&mut self,
|
|
||||||
flow: &MinecraftLogin,
|
|
||||||
services: &LauncherServices,
|
services: &LauncherServices,
|
||||||
) -> LauncherResult<MinecraftAccount> {
|
) -> LauncherResult<MinecraftAccount> {
|
||||||
let client = services.requester.http();
|
let client = services.requester.http();
|
||||||
let progress_id = Uuid::new_v4();
|
let progress_id = Uuid::new_v4();
|
||||||
let notifier = services.notifier.clone();
|
let notifier = services.notifier.clone();
|
||||||
let account = super::msa::finish_login(client, flow, |label, current, total| {
|
let account = super::msa::finish_device_login(client, flow, |label, current, total| {
|
||||||
notifier.send_progress(&progress_id, label, current, total);
|
notifier.send_progress(&progress_id, label, current, total);
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(AuthError::from)?;
|
.map_err(AuthError::from)?;
|
||||||
|
|
||||||
|
self.commit_account(account, services).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn commit_account(
|
||||||
|
&mut self,
|
||||||
|
account: MinecraftAccount,
|
||||||
|
services: &LauncherServices,
|
||||||
|
) -> LauncherResult<MinecraftAccount> {
|
||||||
self.users.insert(account.id, account.clone());
|
self.users.insert(account.id, account.clone());
|
||||||
|
|
||||||
if self.default_user.is_none() {
|
if self.default_user.is_none() {
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ pub async fn ephemeral_state() -> LauncherResult<Arc<LauncherState>> {
|
||||||
},
|
},
|
||||||
settings: parking_lot::RwLock::new(settings),
|
settings: parking_lot::RwLock::new(settings),
|
||||||
auth: tokio::sync::Mutex::new(CredentialsStore::default()),
|
auth: tokio::sync::Mutex::new(CredentialsStore::default()),
|
||||||
|
microsoft_logins: tokio::sync::Mutex::new(std::collections::HashMap::new()),
|
||||||
java: crate::java::JavaManager,
|
java: crate::java::JavaManager,
|
||||||
metadata: tokio::sync::Mutex::new(crate::metadata::MetadataStore::new()),
|
metadata: tokio::sync::Mutex::new(crate::metadata::MetadataStore::new()),
|
||||||
bundles: Arc::new(crate::bundles::BundlesManager::new()),
|
bundles: Arc::new(crate::bundles::BundlesManager::new()),
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ pub struct LauncherSettings {
|
||||||
pub auto_update: bool,
|
pub auto_update: bool,
|
||||||
pub enable_gamemode: bool,
|
pub enable_gamemode: bool,
|
||||||
pub discord_enabled: bool,
|
pub discord_enabled: bool,
|
||||||
|
pub microsoft_login_use_browser: bool,
|
||||||
pub max_concurrent_requests: usize,
|
pub max_concurrent_requests: usize,
|
||||||
pub global_game_settings: GameSettingsProfile,
|
pub global_game_settings: GameSettingsProfile,
|
||||||
pub allow_parallel_running_clusters: bool,
|
pub allow_parallel_running_clusters: bool,
|
||||||
|
|
@ -58,6 +59,7 @@ impl Default for LauncherSettings {
|
||||||
log_debug: false,
|
log_debug: false,
|
||||||
auto_update: true,
|
auto_update: true,
|
||||||
discord_enabled: true,
|
discord_enabled: true,
|
||||||
|
microsoft_login_use_browser: true,
|
||||||
enable_gamemode: false,
|
enable_gamemode: false,
|
||||||
max_concurrent_requests: 25,
|
max_concurrent_requests: 25,
|
||||||
global_game_settings: GameSettingsProfile::default_global_profile(),
|
global_game_settings: GameSettingsProfile::default_global_profile(),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use oneclient_db::DbPool;
|
use oneclient_db::DbPool;
|
||||||
use tokio::sync::{Mutex, OnceCell};
|
use tokio::sync::{Mutex, OnceCell};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::auth::CredentialsStore;
|
use crate::auth::{CredentialsStore, PendingBrowserLogin};
|
||||||
use crate::bundles::BundlesManager;
|
use crate::bundles::BundlesManager;
|
||||||
use crate::http::RequestClient;
|
use crate::http::RequestClient;
|
||||||
use crate::images::ImageCacheStore;
|
use crate::images::ImageCacheStore;
|
||||||
|
|
@ -31,6 +32,7 @@ pub struct LauncherState {
|
||||||
pub services: LauncherServices,
|
pub services: LauncherServices,
|
||||||
pub settings: parking_lot::RwLock<LauncherSettings>,
|
pub settings: parking_lot::RwLock<LauncherSettings>,
|
||||||
pub auth: Mutex<CredentialsStore>,
|
pub auth: Mutex<CredentialsStore>,
|
||||||
|
pub microsoft_logins: Mutex<HashMap<String, PendingBrowserLogin>>,
|
||||||
pub java: JavaManager,
|
pub java: JavaManager,
|
||||||
pub metadata: Mutex<MetadataStore>,
|
pub metadata: Mutex<MetadataStore>,
|
||||||
pub bundles: Arc<BundlesManager>,
|
pub bundles: Arc<BundlesManager>,
|
||||||
|
|
@ -63,6 +65,7 @@ impl LauncherState {
|
||||||
services,
|
services,
|
||||||
settings: parking_lot::RwLock::new(settings),
|
settings: parking_lot::RwLock::new(settings),
|
||||||
auth: Mutex::new(auth),
|
auth: Mutex::new(auth),
|
||||||
|
microsoft_logins: Mutex::new(HashMap::new()),
|
||||||
java,
|
java,
|
||||||
metadata: Mutex::new(MetadataStore::new()),
|
metadata: Mutex::new(MetadataStore::new()),
|
||||||
bundles: Arc::new(BundlesManager::new()),
|
bundles: Arc::new(BundlesManager::new()),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue