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