1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
mod entitlements;
mod microsoft;
mod minecraft;
mod minecraft_profile;
mod xbl;
mod xsts;

use crate::error::{AuthError, AuthResult};
pub use microsoft::DeviceCodeInfo;
use time::OffsetDateTime;
use uuid::Uuid;

/// A profile for Minecraft online mode.
/// 
/// ```rust
///# use cobble_core::profile::CobbleProfile;
///# use cobble_core::error::AuthResult;
///# async fn authenticate() -> AuthResult<()> {
/// // See the oauth2 crate for information to create a Microsoft Application for `MS_GRAPH_ID`.
/// let code_info = CobbleProfile::setup_authentication("<MS_GRAPH_ID>".to_string()).await?;
/// 
/// // Print and URL
/// println!("URL: {}", &code_info.verification_url());
/// println!("CODE: {}", &code_info.user_code());
/// 
/// // Waits until authentication is done
/// let profile: CobbleProfile = CobbleProfile::authenticate(code_info).await?;
///#
///# Ok(())
///# }
/// ```
#[cfg_attr(doc_cfg, doc(cfg(feature = "auth")))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[derive(Clone, Debug)]
pub struct CobbleProfile {
    /// Profile [`UUID`](uuid::Uuid).
    pub uuid: Uuid,
    /// Minecraft profile ID
    pub profile_id: String,
    /// Minecraft player name
    pub player_name: String,
    /// Microsoft refresh token
    pub microsoft_refresh_token: String,
    /// Minecraft token
    pub minecraft_token: String,
    /// Minecraft token expiration
    #[cfg_attr(feature = "serde", serde(with = "time::serde::rfc3339"))]
    pub minecraft_token_exp: OffsetDateTime,
}

impl CobbleProfile {
    /// Gets the Minecraft access token
    pub fn access_token(&self) -> String {
        self.minecraft_token.clone()
    }

    /// Setup the authentication process.
    /// Returns a struct used to retrieve the device code and verification URL.
    /// The result of this call is used to finish the authentication.
    #[instrument(name = "setup_profile_authentication", level = "debug", skip_all)]
    pub async fn setup_authentication(client_id: String) -> AuthResult<DeviceCodeInfo> {
        microsoft::setup_authentication(client_id).await
    }

    /// Finish authentication by polling microsoft for the token.
    /// Subsequently authenticates with XBoxLive, XBoxLiveSecurity and Minecraft.
    #[instrument(name = "authenticate_profile", level = "debug", skip_all)]
    pub async fn authenticate(info: DeviceCodeInfo) -> AuthResult<Self> {
        let microsoft = info.finish_authentication().await?;
        let xbl = xbl::authenticate(&microsoft.access_token).await?;
        let xsts = xsts::authenticate(&xbl.access_token).await?;
        let minecraft = minecraft::authenticate(&xsts.access_token, &xbl.user_hash).await?;

        let entitlements = entitlements::get_entitlements(&minecraft.access_token).await?;

        let minecraft_entitlement = entitlements
            .entitlements
            .iter()
            .find(|e| e.name == "product_minecraft");

        if minecraft_entitlement.is_none() {
            trace!("User does not have 'product_minecraft' entitlements");
            return Err(AuthError::Unauthorized);
        }

        let profile = minecraft_profile::get_profile(&minecraft.access_token).await?;

        Ok(CobbleProfile {
            uuid: Uuid::new_v4(),
            profile_id: profile.id,
            player_name: profile.name,
            microsoft_refresh_token: microsoft.refresh_token,
            minecraft_token: minecraft.access_token,
            minecraft_token_exp: minecraft.expiration,
        })
    }

    /// Refreshes the Minecraft access token.
    #[instrument(name = "refresh_profile", level = "debug", skip_all)]
    pub async fn refresh(&mut self, client_id: String) -> AuthResult<()> {
        let microsoft =
            microsoft::refresh_token(client_id, self.microsoft_refresh_token.clone()).await?;
        let xbl = xbl::authenticate(&microsoft.access_token).await?;
        let xsts = xsts::authenticate(&xbl.access_token).await?;
        let minecraft = minecraft::authenticate(&xsts.access_token, &xbl.user_hash).await?;

        self.microsoft_refresh_token = microsoft.refresh_token;
        self.minecraft_token = minecraft.access_token;
        self.minecraft_token_exp = minecraft.expiration;

        Ok(())
    }
}