use super::mc_msa::{
MinecraftAccessToken, MinecraftAuthenticationResponse, MinecraftAuthorizationFlow,
};
use anyhow::{anyhow, Context};
use nitro_shared::output::{MessageContents, MessageLevel, NitroOutput};
use nitro_shared::translate;
pub use oauth2::basic::{BasicClient, BasicTokenType};
pub use oauth2::reqwest::async_http_client;
pub use oauth2::{
AuthUrl, ClientId, DeviceAuthorizationUrl, EmptyExtraTokenFields, ErrorResponse, RefreshToken,
RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardTokenResponse,
TokenResponse, TokenUrl,
};
use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
const DEVICE_CODE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode";
const MSA_AUTHORIZE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize";
const MSA_TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
pub async fn authenticate_microsoft_account(
client_id: ClientId,
client: &reqwest::Client,
o: &mut impl NitroOutput,
) -> anyhow::Result<MicrosoftAuthResult> {
let oauth_client = create_client(client_id).context("Failed to create OAuth client")?;
let response = generate_login_page(&oauth_client)
.await
.context("Failed to execute authorization and generate login page")?;
o.display_special_ms_auth(response.verification_uri(), response.user_code().secret());
let token = get_microsoft_token(&oauth_client, response)
.await
.context("Failed to get Microsoft token")?;
let result = authenticate_microsoft_account_from_token(token, client, o).await?;
Ok(result)
}
pub async fn authenticate_microsoft_account_from_token(
token: MicrosoftToken,
client: &reqwest::Client,
o: &mut impl NitroOutput,
) -> anyhow::Result<MicrosoftAuthResult> {
let refresh_token = token.refresh_token().cloned();
let mc_token = auth_minecraft(token, client)
.await
.context("Failed to get Minecraft token")?;
let access_token = mc_access_token_to_string(&mc_token.access_token);
o.display(
MessageContents::Success(translate!(o, AuthenticationSuccessful)),
MessageLevel::Important,
);
let out = MicrosoftAuthResult {
access_token: AccessToken(access_token),
xbox_uid: mc_token.username.clone(),
refresh_token,
};
Ok(out)
}
fn get_auth_url() -> anyhow::Result<AuthUrl> {
Ok(AuthUrl::new(MSA_AUTHORIZE_URL.to_string())?)
}
fn get_token_url() -> anyhow::Result<TokenUrl> {
Ok(TokenUrl::new(MSA_TOKEN_URL.to_string())?)
}
fn get_device_code_url() -> anyhow::Result<DeviceAuthorizationUrl> {
Ok(DeviceAuthorizationUrl::new(DEVICE_CODE_URL.to_string())?)
}
pub fn create_client(client_id: ClientId) -> anyhow::Result<BasicClient> {
let client = BasicClient::new(
client_id,
None,
get_auth_url().context("Failed to get authorization URL")?,
Some(get_token_url().context("Failed to get token URL")?),
)
.set_device_authorization_url(get_device_code_url().context("Failed to get device code URL")?);
Ok(client)
}
pub async fn generate_login_page(
client: &BasicClient,
) -> anyhow::Result<StandardDeviceAuthorizationResponse> {
let out = client
.exchange_device_code()
.context("Failed to exchange device code")?
.add_scope(Scope::new("XboxLive.signin offline_access".into()))
.request_async(async_http_client)
.await;
out.map_err(decorate_request_token_error)
}
pub type MicrosoftToken = StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>;
pub async fn get_microsoft_token(
client: &BasicClient,
auth_response: StandardDeviceAuthorizationResponse,
) -> anyhow::Result<MicrosoftToken> {
let out = client
.exchange_device_access_token(&auth_response)
.request_async(
async_http_client,
|x| async move { std::thread::sleep(x) },
None,
)
.await;
out.map_err(decorate_request_token_error)
}
pub async fn refresh_microsoft_token(
client: &BasicClient,
refresh_token: &RefreshToken,
) -> anyhow::Result<MicrosoftToken> {
let out = client
.exchange_refresh_token(refresh_token)
.request_async(async_http_client)
.await;
out.map_err(decorate_request_token_error)
}
pub async fn auth_minecraft(
token: MicrosoftToken,
client: &reqwest::Client,
) -> anyhow::Result<MinecraftAuthenticationResponse> {
let mc_flow = MinecraftAuthorizationFlow::new(client.clone());
let mc_token = mc_flow
.exchange_microsoft_token(token.access_token().secret())
.await?;
Ok(mc_token)
}
pub fn mc_access_token_to_string(token: &MinecraftAccessToken) -> String {
token.clone().into_inner()
}
fn decorate_request_token_error<RE: std::error::Error, T: ErrorResponse>(
e: RequestTokenError<RE, T>,
) -> anyhow::Error {
match e {
RequestTokenError::ServerResponse(response) => {
anyhow!("{response:?}").context("Server returned an error response")
}
e => anyhow!("{e}"),
}
}
pub async fn account_owns_game(
access_token: &str,
client: &reqwest::Client,
) -> anyhow::Result<bool> {
let response = call_mc_api_impl(
"https://api.minecraftservices.com/entitlements/mcstore",
access_token,
client,
)
.await
.context("Failed to call API to check game ownership")?;
let text = response.text().await?;
let out = text.contains("product_minecraft") | text.contains("game_minecraft");
Ok(out)
}
pub struct MicrosoftAuthResult {
pub access_token: AccessToken,
pub xbox_uid: String,
pub refresh_token: Option<RefreshToken>,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AccessToken(pub String);
impl Debug for AccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "AccessToken(***)")
}
}
pub async fn call_mc_api<T: DeserializeOwned>(
url: &str,
access_token: &str,
client: &reqwest::Client,
) -> anyhow::Result<T> {
let response = call_mc_api_impl(url, access_token, client).await?;
let response = response.json().await?;
Ok(response)
}
async fn call_mc_api_impl(
url: &str,
access_token: &str,
client: &reqwest::Client,
) -> anyhow::Result<Response> {
let response = client
.get(url)
.header("Authorization", format!("Bearer {access_token}"))
.send()
.await?
.error_for_status()?;
Ok(response)
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
pub struct Keypair {
#[serde(alias = "privateKey")]
pub private_key: String,
#[serde(alias = "publicKey")]
pub public_key: String,
}