azalea_auth/
sessionserver.rs1use std::sync::LazyLock;
3
4use reqwest::StatusCode;
5use serde::Deserialize;
6use serde_json::json;
7use thiserror::Error;
8use tracing::debug;
9use uuid::Uuid;
10
11use crate::game_profile::{GameProfile, SerializableGameProfile};
12
13#[derive(Debug, Error)]
14pub enum ClientSessionServerError {
15    #[error("Error sending HTTP request to sessionserver: {0}")]
16    HttpError(#[from] reqwest::Error),
17    #[error("Multiplayer is not enabled for this account")]
18    MultiplayerDisabled,
19    #[error("This account has been banned from multiplayer")]
20    Banned,
21    #[error("The authentication servers are currently not reachable")]
22    AuthServersUnreachable,
23    #[error("Invalid or expired session")]
24    InvalidSession,
25    #[error("Unknown sessionserver error: {0}")]
26    Unknown(String),
27    #[error("Forbidden operation (expired session?)")]
28    ForbiddenOperation,
29    #[error("RateLimiter disallowed request")]
30    RateLimited,
31    #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
32    UnexpectedResponse { status_code: u16, body: String },
33}
34
35#[derive(Debug, Error)]
36pub enum ServerSessionServerError {
37    #[error("Error sending HTTP request to sessionserver: {0}")]
38    HttpError(#[from] reqwest::Error),
39    #[error("Invalid or expired session")]
40    InvalidSession,
41    #[error("Unexpected response from sessionserver (status code {status_code}): {body}")]
42    UnexpectedResponse { status_code: u16, body: String },
43    #[error("Unknown sessionserver error: {0}")]
44    Unknown(String),
45}
46
47#[derive(Deserialize)]
48pub struct ForbiddenError {
49    pub error: String,
50    pub path: String,
51}
52
53static REQWEST_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(reqwest::Client::new);
54
55pub async fn join(
60    access_token: &str,
61    public_key: &[u8],
62    private_key: &[u8],
63    uuid: &Uuid,
64    server_id: &str,
65) -> Result<(), ClientSessionServerError> {
66    let client = REQWEST_CLIENT.clone();
67
68    let server_hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
69        server_id.as_bytes(),
70        public_key,
71        private_key,
72    ));
73
74    join_with_server_id_hash(&client, access_token, uuid, &server_hash).await
75}
76
77pub async fn join_with_server_id_hash(
78    client: &reqwest::Client,
79    access_token: &str,
80    uuid: &Uuid,
81    server_hash: &str,
82) -> Result<(), ClientSessionServerError> {
83    let mut encode_buffer = Uuid::encode_buffer();
84    let undashed_uuid = uuid.simple().encode_lower(&mut encode_buffer);
85
86    let data = json!({
87        "accessToken": access_token,
88        "selectedProfile": undashed_uuid,
89        "serverId": server_hash
90    });
91    let res = client
92        .post("https://sessionserver.mojang.com/session/minecraft/join")
93        .json(&data)
94        .send()
95        .await?;
96
97    match res.status() {
98        StatusCode::NO_CONTENT => Ok(()),
99        StatusCode::FORBIDDEN => {
100            let forbidden = res.json::<ForbiddenError>().await?;
101            match forbidden.error.as_str() {
102                "InsufficientPrivilegesException" => {
103                    Err(ClientSessionServerError::MultiplayerDisabled)
104                }
105                "UserBannedException" => Err(ClientSessionServerError::Banned),
106                "AuthenticationUnavailableException" => {
107                    Err(ClientSessionServerError::AuthServersUnreachable)
108                }
109                "InvalidCredentialsException" => Err(ClientSessionServerError::InvalidSession),
110                "ForbiddenOperationException" => Err(ClientSessionServerError::ForbiddenOperation),
111                _ => Err(ClientSessionServerError::Unknown(forbidden.error)),
112            }
113        }
114        StatusCode::TOO_MANY_REQUESTS => Err(ClientSessionServerError::RateLimited),
115        status_code => {
116            debug!("Error headers: {:#?}", res.headers());
118            let body = res.text().await?;
119            Err(ClientSessionServerError::UnexpectedResponse {
120                status_code: status_code.as_u16(),
121                body,
122            })
123        }
124    }
125}
126
127pub async fn serverside_auth(
132    username: &str,
133    public_key: &[u8],
134    private_key: &[u8; 16],
135    ip: Option<&str>,
136) -> Result<GameProfile, ServerSessionServerError> {
137    let hash = azalea_crypto::hex_digest(&azalea_crypto::digest_data(
138        "".as_bytes(),
139        public_key,
140        private_key,
141    ));
142
143    let url = reqwest::Url::parse_with_params(
144        "https://sessionserver.mojang.com/session/minecraft/hasJoined",
145        if let Some(ip) = ip {
146            vec![("username", username), ("serverId", &hash), ("ip", ip)]
147        } else {
148            vec![("username", username), ("serverId", &hash)]
149        },
150    )
151    .expect("URL should always be valid");
152
153    let res = reqwest::get(url).await?;
154
155    match res.status() {
156        StatusCode::OK => {}
157        StatusCode::NO_CONTENT => {
158            return Err(ServerSessionServerError::InvalidSession);
159        }
160        StatusCode::FORBIDDEN => {
161            return Err(ServerSessionServerError::Unknown(
162                res.json::<ForbiddenError>().await?.error,
163            ));
164        }
165        status_code => {
166            debug!("Error headers: {:#?}", res.headers());
168            let body = res.text().await?;
169            return Err(ServerSessionServerError::UnexpectedResponse {
170                status_code: status_code.as_u16(),
171                body,
172            });
173        }
174    };
175
176    Ok(res.json::<SerializableGameProfile>().await?.into())
177}