summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/auth.rs99
-rw-r--r--src/auth/msa.rs33
-rw-r--r--src/auth/types.rs2
3 files changed, 104 insertions, 30 deletions
diff --git a/src/auth.rs b/src/auth.rs
index 86776d4..0bae4e1 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -4,18 +4,21 @@ mod msa;
use std::error::Error;
use std::fmt::{Display, Formatter};
+use std::future::Future;
use std::ops::Add;
use std::time::{Duration, Instant, SystemTime};
use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
-use oauth2::{AccessToken, AuthType, AuthUrl, ClientId, DeviceAuthorizationUrl, HttpClientError, RefreshToken, RequestTokenError, Scope, StandardTokenResponse, TokenResponse, TokenUrl};
-use oauth2::basic::{BasicErrorResponse, BasicTokenResponse};
+use oauth2::{AccessToken, AuthType, AuthUrl, ClientId, DeviceAuthorizationResponse, DeviceAuthorizationUrl, DeviceCodeErrorResponse, EmptyExtraDeviceAuthorizationFields, EndpointNotSet, EndpointSet, HttpClientError, RefreshToken, RequestTokenError, Scope, StandardDeviceAuthorizationResponse, StandardRevocableToken, StandardTokenResponse, TokenResponse, TokenUrl};
+use oauth2::basic::{BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse, BasicTokenResponse};
pub use types::*;
#[derive(Debug)]
pub enum AuthError {
Request { what: &'static str, error: reqwest::Error },
OAuthRequestToken { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, BasicErrorResponse> },
+ OAuthRequestDeviceCode { what: &'static str, error: RequestTokenError<HttpClientError<reqwest::Error>, DeviceCodeErrorResponse> },
Internal(String),
+ Cancel(Option<Box<dyn Error>>),
Timeout
}
@@ -24,7 +27,10 @@ impl Display for AuthError {
match self {
AuthError::Request { what, error } => write!(f, "auth request error ({}): {}", what, error),
AuthError::OAuthRequestToken { what, error } => write!(f, "oauth error requesting token ({what}): {error}"),
+ AuthError::OAuthRequestDeviceCode { what, error } => write!(f, "oauth error with device code ({what}): {error}"),
AuthError::Internal(msg) => write!(f, "internal auth error: {}", msg),
+ AuthError::Cancel(Some(error)) => write!(f, "operation cancelled: {error}"),
+ AuthError::Cancel(None) => f.write_str("operation cancelled"),
AuthError::Timeout => f.write_str("interactive authentication timed out")
}
}
@@ -35,6 +41,8 @@ impl Error for AuthError {
match self {
AuthError::Request { error, .. } => Some(error),
AuthError::OAuthRequestToken { error, .. } => Some(error),
+ AuthError::OAuthRequestDeviceCode { error, .. } => Some(error),
+ AuthError::Cancel(Some(error)) => Some(error.as_ref()),
_ => None
}
}
@@ -47,26 +55,25 @@ impl Token {
}
macro_rules! create_oauth_client {
- ($is_azure_client_id:expr) => {
- if is_azure_client_id {
- oauth2::Client::new(self.client_id.clone())
- .set_token_uri(TokenUrl::new("https://login.microsoftonline.com/consumers/oauth2/v2.0/token".into()).expect("hardcoded url"))
- .set_device_authorization_url(DeviceAuthorizationUrl::new("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode".into()).expect("hardcoded url"))
- as oauth2::Client<BasicErrorResponse, BasicTokenResponse, _, _, _, _, _, _, _, _>
- } else {
- oauth2::Client::new(self.client_id.clone())
- .set_token_uri(TokenUrl::new("https://login.live.com/oauth20_token.srf".into()).expect("hardcoded url"))
- .set_device_authorization_url(DeviceAuthorizationUrl::new("https://login.live.com/oauth20_connect.srf".into()).expect("hardcoded url"))
- as oauth2::Client<BasicErrorResponse, BasicTokenResponse, _, _, _, _, _, _, _, _>
- }
+ ($is_azure_client_id:expr, $client_id:expr) => {
+ oauth2::Client::new($client_id)
+ .set_token_uri(TokenUrl::new(if $is_azure_client_id { AZURE_TOKEN_URL.into() } else { NON_AZURE_TOKEN_URL.into() }).expect("hardcoded url"))
+ .set_device_authorization_url(DeviceAuthorizationUrl::new(if $is_azure_client_id { AZURE_TOKEN_URL.into() } else { NON_AZURE_TOKEN_URL.into() }).expect("hardcoded url"))
+ as oauth2::Client<BasicErrorResponse, BasicTokenResponse, BasicTokenIntrospectionResponse, StandardRevocableToken, BasicRevocationErrorResponse,
+ EndpointNotSet, EndpointSet, EndpointNotSet, EndpointNotSet, EndpointSet>
}
}
+const AZURE_TOKEN_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
+const AZURE_DEVICE_CODE_URL: &str = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
+const NON_AZURE_TOKEN_URL: &str = "https://login.live.com/oauth20_token.srf";
+const NON_AZURE_DEVICE_CODE_URL: &str = "https://login.live.com/oauth20_connect.srf";
+
const AZURE_LOGIN_SCOPES: &[&str] = ["XboxLive.signin", "offline_access"].as_slice();
const NON_AZURE_LOGIN_SCOPES: &[&str] = ["service::user.auth.xboxlive.com::MBI_SSL"].as_slice();
impl MsaUser {
- pub fn create_client(&self) -> reqwest::Client {
+ pub fn create_client() -> reqwest::Client {
reqwest::ClientBuilder::new()
.redirect(reqwest::redirect::Policy::none())
.build().expect("building client should succeed")
@@ -83,23 +90,47 @@ impl MsaUser {
}
// uses an access token from, for example, a device code grant logs into xbox live
- async fn xbl_log_in(&mut self, token: &AccessToken) {
+ async fn xbl_login(&mut self, token: &AccessToken) -> Result<(), AuthError> {
}
- async fn refresh_access_token(&mut self, client: reqwest::Client) -> Result<BasicTokenResponse, AuthError> {
- let oauth_client = create_oauth_client!(self.is_azure_client_id);
+ // logs into xbox live using a refresh token
+ // (panics if no refresh token present)
+ async fn xbl_login_refresh(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
+ let oauth_client = create_oauth_client!(self.is_azure_client_id, self.client_id.clone());
let refresh_token = self.refresh_token.as_ref().expect("refresh_access_token called with no refresh token");
let tokenres: BasicTokenResponse = oauth_client
.exchange_refresh_token(refresh_token)
.add_scopes(self.scopes_iter())
- .request_async(&client)
+ .request_async(client)
.await.map_err(|e| AuthError::OAuthRequestToken { what: "refresh", error: e })?;
self.refresh_token = tokenres.refresh_token().cloned();
- todo!()
+ self.xbl_login(tokenres.access_token()).await
+ }
+
+ async fn xbl_login_device<D, DF>(&mut self, client: &reqwest::Client, handle_device: D) -> Result<(), AuthError>
+ where
+ D: FnOnce(&StandardDeviceAuthorizationResponse) -> DF,
+ DF: Future<Output = ()>
+ {
+ let oauth_client = create_oauth_client!(self.is_azure_client_id, self.client_id.clone());
+ let device_auth: StandardDeviceAuthorizationResponse = oauth_client.exchange_device_code().add_scopes(self.scopes_iter())
+ .request_async(client)
+ .await.map_err(|e| AuthError::OAuthRequestToken { what: "device code", error: e })?;
+
+ handle_device(&device_auth).await;
+
+ let tokenres = oauth_client.exchange_device_access_token(&device_auth)
+ .set_max_backoff_interval(Duration::from_secs(20u64))
+ .request_async(client, tokio::time::sleep, None)
+ .await.map_err(|e| AuthError::OAuthRequestDeviceCode { what: "device access code", error: e })?;
+
+ self.refresh_token = tokenres.refresh_token().cloned();
+
+ self.xbl_login(tokenres.access_token()).await
}
// ensure we have an xbox live token for this person
@@ -107,11 +138,11 @@ impl MsaUser {
// - check if the XBL token is valid/not expired
// - if it is expired, try to use refresh token to get a new one
// - get rid of auth token if yeah
- async fn ensure_xbl(&mut self, client: reqwest::Client) -> Result<(), AuthError> {
+ async fn ensure_xbl(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
todo!()
}
- async fn log_in_silent(&mut self, client: reqwest::Client) -> Result<(), AuthError> {
+ async fn log_in_silent(&mut self, client: &reqwest::Client) -> Result<(), AuthError> {
let now: DateTime<Utc> = SystemTime::now().into();
if self.auth_token.as_ref().is_some_and(|tok| !tok.is_expired(now.add(TimeDelta::hours(12)))) {
@@ -131,12 +162,22 @@ mod test {
#[tokio::test]
async fn abc() {
simple_logger::SimpleLogger::new().with_colors(true).with_level(log::LevelFilter::Trace).init().unwrap();
-
- _=device_code::DeviceCodeAuthBuilder::new()
- .client_id("00000000402b5328")
- .add_scope("service::user.auth.xboxlive.com::MBI_SSL", true)
- .code_request_url("https://login.live.com/oauth20_connect.srf")
- .check_url("https://login.live.com/oauth20_token.srf")
- .begin(Client::new()).await.unwrap().drive().await;
+
+ let mut user: MsaUser = MsaUser {
+ profile: None,
+ xuid: None,
+ client_id: ClientId::new("00000000402b5328".into()),
+ is_azure_client_id: false,
+ auth_token: None,
+ xbl_token: None,
+ refresh_token: None
+ };
+
+ let client = MsaUser::create_client();
+
+ user.xbl_login_device(&client, |d| {
+ dbg!(d);
+ futures::future::ready(())
+ }).await.unwrap();
}
}
diff --git a/src/auth/msa.rs b/src/auth/msa.rs
index e69de29..da9a376 100644
--- a/src/auth/msa.rs
+++ b/src/auth/msa.rs
@@ -0,0 +1,33 @@
+use chrono::{DateTime, Utc};
+use oauth2::AccessToken;
+use serde::{Deserialize, Serialize};
+use crate::auth::AuthError;
+
+const XBOX_LIVE_AUTH: &str = "https://user.auth.xboxlive.com/user/authenticate";
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "PascalCase")]
+struct XboxLiveAuthRequestProperties<'a> {
+ auth_method: &'a str,
+ site_name: &'a str,
+ rps_ticket: &'a str
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "PascalCase")]
+struct XboxLiveAuthRequest<'a> {
+ properties: XboxLiveAuthRequestProperties<'a>,
+ relying_party: &'a str,
+ token_type: &'a str
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "PascalCase")]
+struct XboxLiveAuthResponse {
+ token: String,
+ not_after: DateTime<Utc>
+}
+
+pub fn xbox_live_login(client: &reqwest::Client, access_token: &AccessToken) -> Result<(), AuthError> {
+
+} \ No newline at end of file
diff --git a/src/auth/types.rs b/src/auth/types.rs
index 79cf84f..f455657 100644
--- a/src/auth/types.rs
+++ b/src/auth/types.rs
@@ -31,7 +31,7 @@ pub(super) struct Token {
pub struct MsaUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub profile: Option<UserProfile>,
- pub xuid: Uuid,
+ pub xuid: Option<Uuid>,
pub(super) client_id: oauth2::ClientId,