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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use crate::consts;
use crate::error::CobbleResult;
use crate::profile::error::ProfileError;
use crate::profile::microsoft::MicrosoftToken;
use crossbeam_channel::Receiver;
use oauth2::basic::BasicClient;
use oauth2::reqwest::http_client;
use oauth2::{AuthType, AuthUrl, ClientId, ClientSecret, RefreshToken, TokenUrl};
use serde::{Deserialize, Serialize};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::thread;
use time::OffsetDateTime;
use uuid::Uuid;

mod entitlements;
pub(crate) mod error;
mod microsoft;
mod minecraft;
mod minecraft_profile;
mod xbox_live;
mod xbox_live_security;

/// A profile used to play Minecraft in online mode.
/// The profile is created by authenticating with Microsoft, XBoxLive, XBoxLiveSecurity and finally Minecraft.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Profile {
    /// UUID of the profile.
    pub uuid: Uuid,
    /// XBoxLive username.
    pub username: String,
    /// Minecraft profile ID.
    pub profile_id: String,
    /// Minecraft player name.
    pub player_name: String,
    /// Microsoft refresh token.
    pub microsoft_refresh_token: String,
    /// Minecraft access token.
    pub minecraft_token: String,
    /// Minecraft refresh token.
    #[serde(with = "time::serde::rfc3339")]
    pub minecraft_token_exp: OffsetDateTime,
}

impl Profile {
    /// Performs the whole authentication process.
    /// This function almost immediately returns the OAuth2 URL and a receiver that receives the result of the authentication process.
    /// The authentication process is started in the background in a separate thread.
    pub fn authenticate(
        client_id: String,
        client_secret: String,
        cancel: Arc<AtomicBool>,
    ) -> CobbleResult<(String, Receiver<CobbleResult<Self>>)> {
        let (url, microsoft_token) = microsoft::authenticate(client_id, client_secret, cancel)?;
        let (sender, receiver) = crossbeam_channel::unbounded();

        thread::spawn(move || {
            let result = perform_auth(microsoft_token);
            sender.send(result).expect("Channel already closed");
        });

        Ok((url, receiver))
    }

    /// Refreshes the Minecraft access token by refreshing the Microsoft token.
    /// It then performs the whole authentication chain:
    ///
    /// - Microsoft
    /// - XBoxLive
    /// - XBoxLiveSecurity
    /// - Minecraft
    pub fn refresh(&mut self, client_id: String, client_secret: String) -> CobbleResult<()> {
        let client_id = ClientId::new(client_id);
        let client_secret = ClientSecret::new(client_secret);
        let auth_url = AuthUrl::new(consts::MS_AUTH_CODE_URL.to_string()).unwrap();
        let token_url = TokenUrl::new(consts::MS_AUTH_TOKEN_URL.to_string()).unwrap();
        let refresh_token = RefreshToken::new(self.microsoft_refresh_token.clone());

        let oauth_client =
            BasicClient::new(client_id, Some(client_secret), auth_url, Some(token_url))
                .set_auth_type(AuthType::RequestBody);

        let microsoft_token = oauth_client
            .exchange_refresh_token(&refresh_token)
            .request(http_client)
            .map(MicrosoftToken::from_token_response)
            .map_err(|err| ProfileError::MicrosoftAuthenticate(err.to_string()))?;

        let xbox_live_token = xbox_live::authenticate(&microsoft_token.access_token)?;
        let xbox_live_security_token =
            xbox_live_security::authenticate(&xbox_live_token.access_token)?;
        let user_hash = xbox_live_security_token
            .user_hash
            .ok_or(ProfileError::MissingUserHash)?;
        let minecraft_token =
            minecraft::authenticate(&xbox_live_security_token.access_token, &user_hash)?;

        self.microsoft_refresh_token = microsoft_token.refresh_token;
        self.minecraft_token = minecraft_token.access_token;
        self.minecraft_token_exp = minecraft_token.expiration;

        Ok(())
    }
}

fn perform_auth(token_receiver: Receiver<CobbleResult<MicrosoftToken>>) -> CobbleResult<Profile> {
    let microsoft_token = token_receiver
        .recv()
        .map_err(|err| ProfileError::MicrosoftAuthenticate(err.to_string()))??;
    let xbox_live_token = xbox_live::authenticate(&microsoft_token.access_token)?;
    let xbox_live_security_token = xbox_live_security::authenticate(&xbox_live_token.access_token)?;
    let user_hash = xbox_live_security_token
        .user_hash
        .ok_or(ProfileError::MissingUserHash)?;
    let minecraft_token =
        minecraft::authenticate(&xbox_live_security_token.access_token, &user_hash)?;
    let entitlements = entitlements::get_entitlements(&minecraft_token.access_token)?;
    if entitlements.entitlements.is_empty() {
        return Err(ProfileError::Unauthorized.into());
    }
    let minecraft_profile = minecraft_profile::get_profile(&minecraft_token.access_token)?;

    let profile = Profile {
        uuid: Uuid::new_v4(),
        username: minecraft_token.username,
        profile_id: minecraft_profile.id,
        player_name: minecraft_profile.name,
        microsoft_refresh_token: microsoft_token.refresh_token,
        minecraft_token: minecraft_token.access_token,
        minecraft_token_exp: minecraft_token.expiration,
    };

    Ok(profile)
}