use async_trait::async_trait;
use log::{debug, info, trace};
use oauth2::{
EndUserVerificationUrl, StandardDeviceAuthorizationResponse, TokenResponse, UserCode,
VerificationUriComplete,
};
use url::Url;
use crate::{
response::{SisuAuthenticationResponse, WindowsLiveTokens},
tokenstore::TokenStore,
AccessTokenPrefix, Error, XalAuthenticator,
};
#[derive(Debug)]
pub enum AuthPromptData {
RedirectUrl {
prompt: String,
url: EndUserVerificationUrl,
expect_url: bool,
},
DeviceCode {
prompt: String,
url: EndUserVerificationUrl,
code: UserCode,
full_verificiation_url: VerificationUriComplete,
expect_url: bool,
},
}
impl From<SisuAuthenticationResponse> for AuthPromptData {
fn from(value: SisuAuthenticationResponse) -> Self {
Self::RedirectUrl {
prompt: format!(
"!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0} (Query params: {1:?})\n
\nThen enter the resulting redirected URL (might need to open DevTools in your browser before opening the link)",
value.msa_oauth_redirect,
value.msa_request_parameters,
),
url: EndUserVerificationUrl::from_url(value.msa_oauth_redirect.clone()),
expect_url: true,
}
}
}
impl From<StandardDeviceAuthorizationResponse> for AuthPromptData {
fn from(value: StandardDeviceAuthorizationResponse) -> Self {
let user_code = value.user_code().to_owned();
let verification_uri = value.verification_uri().to_owned();
let full_url = XalAuthenticator::get_device_code_verification_uri(value.user_code());
Self::DeviceCode {
prompt: format!(
"!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0}\nUse code: {1}\n\nAlternatively, use this link: {2}",
verification_uri.as_str(),
user_code.secret(),
full_url.secret(),
),
url: verification_uri,
code: user_code,
full_verificiation_url: full_url,
expect_url: false,
}
}
}
impl From<EndUserVerificationUrl> for AuthPromptData {
fn from(value: EndUserVerificationUrl) -> Self {
Self::RedirectUrl {
prompt: format!(
"!!! ACTION REQUIRED !!!\nNavigate to this URL and authenticate: {0}\nNOTE: You might have to open DevTools when navigating the flow to catch redirect",
value.as_str()
),
url: value.to_owned(),
expect_url: true,
}
}
}
impl AuthPromptData {
pub fn prompt(&self) -> String {
match self {
AuthPromptData::RedirectUrl { prompt, .. } => prompt.to_owned(),
AuthPromptData::DeviceCode { prompt, .. } => prompt.to_owned(),
}
}
pub fn expect_url(&self) -> bool {
match self {
AuthPromptData::RedirectUrl { expect_url, .. } => *expect_url,
AuthPromptData::DeviceCode { expect_url, .. } => *expect_url,
}
}
pub fn authentication_url(&self) -> Url {
match self {
AuthPromptData::RedirectUrl { url, .. } => Url::parse(url.as_str()).unwrap(),
AuthPromptData::DeviceCode {
full_verificiation_url,
..
} => Url::parse(full_verificiation_url.secret()).unwrap(),
}
}
}
#[async_trait]
pub trait AuthPromptCallback {
async fn call(
&self,
cb_data: AuthPromptData,
) -> Result<Option<Url>, Box<dyn std::error::Error>>;
}
pub struct CliCallbackHandler;
#[async_trait]
impl AuthPromptCallback for CliCallbackHandler {
async fn call(
&self,
cb_data: AuthPromptData,
) -> Result<Option<Url>, Box<dyn std::error::Error>> {
let prompt = cb_data.prompt();
let do_expect_url = cb_data.expect_url();
println!("{prompt}\n");
let res = if do_expect_url {
print!("Redirect URL> ");
let mut redirect_url = String::new();
let _ = std::io::stdin().read_line(&mut redirect_url)?;
Some(Url::parse(&redirect_url)?)
} else {
None
};
Ok(res)
}
}
pub struct Flows;
impl Flows {
pub async fn try_refresh_live_tokens_from_file(
filepath: &str,
) -> Result<(XalAuthenticator, TokenStore), Error> {
let mut ts = TokenStore::load_from_file(filepath)?;
let authenticator = Self::try_refresh_live_tokens_from_tokenstore(&mut ts).await?;
Ok((authenticator, ts))
}
pub async fn try_refresh_live_tokens_from_tokenstore(
ts: &mut TokenStore,
) -> Result<XalAuthenticator, Error> {
let mut authenticator = Into::<XalAuthenticator>::into(ts.clone());
info!("Refreshing windows live tokens");
let refreshed_wl_tokens = authenticator
.refresh_token(ts.live_token.refresh_token().unwrap())
.await
.expect("Failed to exchange refresh token for fresh WL tokens");
debug!("Windows Live tokens: {:?}", refreshed_wl_tokens);
ts.live_token = refreshed_wl_tokens.clone();
Ok(authenticator)
}
pub async fn ms_device_code_flow<S, SF>(
authenticator: &mut XalAuthenticator,
cb: impl AuthPromptCallback,
sleep_fn: S,
) -> Result<TokenStore, Error>
where
S: Fn(std::time::Duration) -> SF,
SF: std::future::Future<Output = ()>,
{
trace!("Initiating device code flow");
let device_code_flow = authenticator.initiate_device_code_auth().await?;
debug!("Device code={:?}", device_code_flow);
trace!("Reaching into callback to notify caller about device code url");
cb.call(device_code_flow.clone().into())
.await
.map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?;
trace!("Polling for device code");
let live_tokens = authenticator
.poll_device_code_auth(&device_code_flow, sleep_fn)
.await?;
let ts = TokenStore {
app_params: authenticator.app_params(),
client_params: authenticator.client_params(),
sandbox_id: authenticator.sandbox_id(),
live_token: live_tokens,
user_token: None,
title_token: None,
device_token: None,
authorization_token: None,
updated: None,
};
Ok(ts)
}
pub async fn ms_authorization_flow(
authenticator: &mut XalAuthenticator,
cb: impl AuthPromptCallback,
implicit: bool,
) -> Result<TokenStore, Error> {
trace!("Starting implicit authorization flow");
let (url, state) = authenticator.get_authorization_url(implicit)?;
trace!("Reaching into callback to receive authentication redirect URL");
let redirect_url = cb
.call(url.into())
.await
.map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?
.ok_or(Error::GeneralError(
"Failed receiving redirect URL".to_string(),
))?;
debug!("From callback: Redirect URL={:?}", redirect_url);
let live_tokens = if implicit {
trace!("Parsing (implicit grant) redirect URI");
XalAuthenticator::parse_implicit_grant_url(&redirect_url, Some(&state))?
} else {
trace!("Parsing (authorization code) redirect URI");
let authorization_code =
XalAuthenticator::parse_authorization_code_response(&redirect_url, Some(&state))?;
debug!("Authorization Code: {:?}", &authorization_code);
trace!("Getting Windows Live tokens (exchange code)");
authenticator
.exchange_code_for_token(authorization_code, None)
.await?
};
let ts = TokenStore {
app_params: authenticator.app_params(),
client_params: authenticator.client_params(),
sandbox_id: authenticator.sandbox_id(),
live_token: live_tokens,
user_token: None,
title_token: None,
device_token: None,
authorization_token: None,
updated: None,
};
Ok(ts)
}
pub async fn xbox_live_sisu_full_flow(
authenticator: &mut XalAuthenticator,
callback: impl AuthPromptCallback,
) -> Result<TokenStore, Error> {
trace!("Getting device token");
let device_token = authenticator.get_device_token().await?;
debug!("Device token={:?}", device_token);
let (code_challenge, code_verifier) = XalAuthenticator::generate_code_verifier();
trace!("Generated Code verifier={:?}", code_verifier);
trace!("Generated Code challenge={:?}", code_challenge);
let state = XalAuthenticator::generate_random_state();
trace!("Generated random state={:?}", state);
trace!("Fetching SISU authentication URL and Session Id");
let (auth_resp, session_id) = authenticator
.sisu_authenticate(&device_token, &code_challenge, &state)
.await?;
debug!(
"SISU Authenticate response={:?} Session Id={:?}",
auth_resp, session_id
);
trace!("Reaching into callback to receive authentication redirect URL");
let redirect_url = callback
.call(auth_resp.into())
.await
.map_err(|e| Error::GeneralError(format!("Failed getting redirect URL err={e}")))?
.ok_or(Error::GeneralError(
"Did not receive any Redirect URL from RedirectUrl callback".to_string(),
))?;
debug!("From callback: Redirect URL={:?}", redirect_url);
trace!("Parsing redirect URI");
let authorization_code =
XalAuthenticator::parse_authorization_code_response(&redirect_url, Some(&state))?;
debug!("Authorization Code: {:?}", &authorization_code);
trace!("Getting Windows Live tokens (exchange code)");
let live_tokens = authenticator
.exchange_code_for_token(authorization_code, Some(code_verifier))
.await?;
debug!("Windows live tokens={:?}", &live_tokens);
trace!("Getting Sisu authorization response");
let sisu_resp = authenticator
.sisu_authorize(&live_tokens, &device_token, Some(session_id))
.await?;
debug!("Sisu authorizatione response={:?}", sisu_resp);
let ts = TokenStore {
app_params: authenticator.app_params(),
client_params: authenticator.client_params(),
sandbox_id: authenticator.sandbox_id(),
live_token: live_tokens,
device_token: Some(device_token),
user_token: Some(sisu_resp.user_token),
title_token: Some(sisu_resp.title_token),
authorization_token: Some(sisu_resp.authorization_token),
updated: None,
};
Ok(ts)
}
pub async fn xbox_live_authorization_traditional_flow(
authenticator: &mut XalAuthenticator,
live_tokens: WindowsLiveTokens,
xsts_relying_party: String,
access_token_prefix: AccessTokenPrefix,
request_title_token: bool,
) -> Result<TokenStore, Error> {
debug!("Windows live tokens={:?}", &live_tokens);
trace!("Getting device token");
let device_token = authenticator.get_device_token().await?;
debug!("Device token={:?}", device_token);
trace!("Getting user token");
let user_token = authenticator
.get_user_token(&live_tokens, access_token_prefix)
.await?;
debug!("User token={:?}", user_token);
let maybe_title_token = if request_title_token {
trace!("Getting title token");
let title_token = authenticator
.get_title_token(&live_tokens, &device_token)
.await?;
debug!("Title token={:?}", title_token);
Some(title_token)
} else {
debug!("Skipping title token request..");
None
};
trace!("Getting XSTS token");
let authorization_token = authenticator
.get_xsts_token(
Some(&device_token),
maybe_title_token.as_ref(),
Some(&user_token),
&xsts_relying_party,
)
.await?;
debug!("XSTS token={:?}", authorization_token);
let ts = TokenStore {
app_params: authenticator.app_params(),
client_params: authenticator.client_params(),
sandbox_id: authenticator.sandbox_id(),
live_token: live_tokens,
device_token: Some(device_token),
user_token: Some(user_token),
title_token: maybe_title_token,
authorization_token: Some(authorization_token),
updated: None,
};
Ok(ts)
}
pub async fn xbox_live_sisu_authorization_flow(
authenticator: &mut XalAuthenticator,
live_tokens: WindowsLiveTokens,
) -> Result<TokenStore, Error> {
debug!("Windows live tokens={:?}", &live_tokens);
trace!("Getting device token");
let device_token = authenticator.get_device_token().await?;
debug!("Device token={:?}", device_token);
trace!("Getting user token");
let resp = authenticator
.sisu_authorize(&live_tokens, &device_token, None)
.await?;
debug!("Sisu authorization response");
let ts = TokenStore {
app_params: authenticator.app_params(),
client_params: authenticator.client_params(),
sandbox_id: authenticator.sandbox_id(),
live_token: live_tokens,
device_token: Some(device_token),
user_token: Some(resp.user_token),
title_token: Some(resp.title_token),
authorization_token: Some(resp.authorization_token),
updated: None,
};
Ok(ts)
}
}