1use crate::env::ENV_AUTH_JWT_SECRET_KEY;
2use crate::{
3 env::{ENV_FRONTEND_URL, ENV_GITHUB_CLIENT_ID, ENV_GITHUB_SECRET},
4 http_server::DataStorage,
5 time::get_timestamp,
6};
7use actix_web::{HttpRequest, HttpResponse};
8use argon2::{self, Config};
9pub use github::{GithubAuth, GithubCallbackQuery};
10use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
11use mahjong_core::PlayerId;
12use serde::{Deserialize, Serialize};
13use service_contracts::{AuthInfoSummary, AuthProvider, ServicePlayer, UserPostSetAuthResponse};
14use tracing::{debug, error};
15use ts_rs::TS;
16use uuid::Uuid;
17
18pub use self::errors::{AuthInfoSummaryError, UnauthorizedError};
19
20mod errors;
21mod github;
22
23pub type Username = String;
24
25#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
26pub enum GetAuthInfo {
27 EmailUsername(Username),
28 AnonymousToken(String),
29 GithubUsername(Username),
30 PlayerId(PlayerId),
31}
32
33#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, TS)]
34#[ts(export)]
35pub enum UserRole {
36 Admin,
37 Player,
38}
39
40#[derive(Serialize, Deserialize, Clone, Debug)]
41pub struct AuthInfoGithub {
42 pub id: PlayerId,
43 pub token: Option<String>,
44 pub username: String,
45}
46
47#[derive(Serialize, Deserialize, Clone, Debug)]
48pub struct AuthInfoEmail {
49 pub hashed_pass: String,
50 pub id: PlayerId,
51 pub username: String,
52}
53
54#[derive(Serialize, Deserialize, Clone, Debug)]
55pub struct AuthInfoAnonymous {
56 pub hashed_token: String,
57 pub id: PlayerId,
58}
59
60#[derive(Serialize, Deserialize, Clone, Debug)]
61pub enum AuthInfoData {
62 Anonymous(AuthInfoAnonymous),
63 Email(AuthInfoEmail),
64 Github(AuthInfoGithub),
65}
66
67#[derive(Serialize, Deserialize, Clone, Debug)]
68pub struct AuthInfo {
69 pub data: AuthInfoData,
70 pub role: UserRole,
71 pub user_id: PlayerId,
72}
73
74#[derive(Debug, Serialize, Deserialize, TS)]
75#[ts(export)]
76struct TokenClaims {
77 exp: usize,
78 role: UserRole,
79 sub: String,
80}
81
82pub struct AuthHandler<'a> {
83 auth_info: Option<AuthInfo>,
84 req: &'a HttpRequest,
85 storage: &'a DataStorage,
86}
87
88impl<'a> AuthHandler<'a> {
89 pub fn verify_setup() -> bool {
90 std::env::var(ENV_AUTH_JWT_SECRET_KEY).is_ok()
91 && std::env::var(ENV_GITHUB_CLIENT_ID).is_ok()
92 && std::env::var(ENV_GITHUB_SECRET).is_ok()
93 && std::env::var(ENV_FRONTEND_URL).is_ok()
94 }
95
96 pub fn new(storage: &'a DataStorage, req: &'a HttpRequest) -> Self {
97 Self {
98 auth_info: None,
99 req,
100 storage,
101 }
102 }
103
104 pub async fn validate_email_user(
105 &mut self,
106 username: &String,
107 password: &String,
108 ) -> Result<Option<bool>, String> {
109 let auth_info_opts = GetAuthInfo::EmailUsername(username.clone());
110 let auth_info = self.storage.get_auth_info(auth_info_opts).await?;
111
112 if auth_info.is_none() {
113 debug!("Not found auth_info for username: {username}");
114 return Ok(None);
115 }
116
117 let auth_info_content = auth_info.unwrap();
118 let auth_info_email = auth_info_content.data.clone();
119
120 let auth_info_email = match auth_info_email {
121 AuthInfoData::Email(email) => email,
122 _ => {
123 debug!("Unexpected auth_info for username: {username}");
124 return Ok(None);
125 }
126 };
127
128 let hash = auth_info_email.hashed_pass.clone();
129 let matches = argon2::verify_encoded(&hash, password.as_bytes());
130
131 if matches.is_err() {
132 let err_str = matches.err().unwrap().to_string();
133 debug!("Matches produced an error for username: {username}, error: {err_str}");
134 return Err(err_str);
135 }
136
137 let matches = matches.unwrap();
138
139 self.auth_info = Some(auth_info_content);
140
141 Ok(Some(matches))
142 }
143
144 pub async fn validate_anon_user(
145 &mut self,
146 id_token: &String,
147 ) -> Result<Option<bool>, UnauthorizedError> {
148 let salt = Uuid::new_v4().to_string();
149 let config = Config::default();
150 let hashed_token = argon2::hash_encoded(id_token.as_bytes(), salt.as_bytes(), &config)
151 .map_err(|_| {
152 error!("Error hashing token");
153 UnauthorizedError
154 })?;
155
156 let auth_info_opts = GetAuthInfo::AnonymousToken(hashed_token.clone());
157 let auth_info = self
158 .storage
159 .get_auth_info(auth_info_opts)
160 .await
161 .map_err(|_| UnauthorizedError)?;
162
163 if auth_info.is_none() {
164 debug!("Not found auth_info for id_token: {id_token}");
165 return Ok(None);
166 }
167
168 let auth_info_content = auth_info.unwrap();
169 let auth_info_anonymous = auth_info_content.data.clone();
170
171 if let AuthInfoData::Anonymous(_) = auth_info_anonymous {
172 Ok(Some(true))
173 } else {
174 Ok(None)
175 }
176 }
177
178 pub async fn create_email_user(
179 &mut self,
180 username: &Username,
181 password: &String,
182 role: UserRole,
183 ) -> Result<(), String> {
184 let salt = Uuid::new_v4().to_string();
185 let config = Config::default();
186 let hash = argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &config).unwrap();
187 let user_id = Uuid::new_v4().to_string();
188
189 let auth_info_email = AuthInfoEmail {
190 hashed_pass: hash.clone(),
191 id: user_id.clone(),
192 username: username.clone(),
193 };
194
195 let player = ServicePlayer {
196 id: user_id.clone(),
197 name: username.clone(),
198 created_at: get_timestamp().to_string(),
199
200 ..ServicePlayer::default()
201 };
202
203 self.storage.save_player(&player).await?;
204
205 let auth_info = AuthInfo {
206 data: AuthInfoData::Email(auth_info_email),
207 role,
208 user_id,
209 };
210
211 self.storage.save_auth_info(&auth_info).await?;
212
213 self.auth_info = Some(auth_info);
214
215 Ok(())
216 }
217
218 pub async fn create_anonymous_user(
219 &mut self,
220 token: &String,
221 role: UserRole,
222 ) -> Result<(), String> {
223 let salt = Uuid::new_v4().to_string();
224 let config = Config::default();
225 let hash = argon2::hash_encoded(token.as_bytes(), salt.as_bytes(), &config).unwrap();
226 let user_id = Uuid::new_v4().to_string();
227
228 let auth_info_anonymous = AuthInfoAnonymous {
229 hashed_token: hash.clone(),
230 id: user_id.clone(),
231 };
232
233 let random_suffix = Uuid::new_v4().to_string().replace('-', "")[0..6].to_string();
234 let name = format!("Anonymous User {}", random_suffix);
235
236 let player = ServicePlayer {
237 id: user_id.clone(),
238 name,
239 created_at: get_timestamp().to_string(),
240
241 ..ServicePlayer::default()
242 };
243
244 self.storage.save_player(&player).await?;
245
246 let auth_info = AuthInfo {
247 data: AuthInfoData::Anonymous(auth_info_anonymous),
248 role,
249 user_id,
250 };
251
252 self.storage.save_auth_info(&auth_info).await?;
253
254 self.auth_info = Some(auth_info);
255
256 Ok(())
257 }
258
259 pub async fn create_github_user(
260 &mut self,
261 username: &Username,
262 token: &str,
263 role: UserRole,
264 ) -> Result<(), String> {
265 let user_id = Uuid::new_v4().to_string();
266
267 let auth_info_github = AuthInfoGithub {
268 id: user_id.clone(),
269 token: Some(token.to_string()),
270 username: username.clone(),
271 };
272
273 let player = ServicePlayer {
274 id: user_id.clone(),
275 name: "Github user".to_string(),
276 created_at: get_timestamp().to_string(),
277
278 ..ServicePlayer::default()
279 };
280
281 self.storage.save_player(&player).await?;
282
283 let auth_info = AuthInfo {
284 data: AuthInfoData::Github(auth_info_github),
285 role,
286 user_id,
287 };
288
289 self.storage.save_auth_info(&auth_info).await?;
290
291 self.auth_info = Some(auth_info);
292
293 Ok(())
294 }
295
296 pub fn generate_token(&self) -> Result<UserPostSetAuthResponse, String> {
297 if self.auth_info.is_none() {
298 debug!("Tried to generate token but no user is logged in");
299 return Err("No user logged in".to_string());
300 }
301
302 let auth_info = self.auth_info.as_ref().unwrap();
303 let my_claims = TokenClaims {
304 exp: 9999999999,
305 role: auth_info.role.clone(),
306 sub: auth_info.user_id.clone(),
307 };
308
309 let encoding_secret = std::env::var(ENV_AUTH_JWT_SECRET_KEY);
310
311 if encoding_secret.is_err() {
312 return Err("Error decoding".to_string());
313 }
314
315 let encoding_secret = encoding_secret.unwrap();
316
317 let token = encode(
318 &Header::default(),
319 &my_claims,
320 &EncodingKey::from_secret(encoding_secret.as_ref()),
321 )
322 .unwrap();
323
324 let response = UserPostSetAuthResponse { token };
325
326 Ok(response)
327 }
328
329 fn get_token_claims(&self, outer_token: Option<&String>) -> Option<TokenClaims> {
330 let encoding_secret = std::env::var(ENV_AUTH_JWT_SECRET_KEY);
331
332 if encoding_secret.is_err() {
333 error!("Missing encoding_secret environment variable");
334 return None;
335 }
336
337 let encoding_secret = encoding_secret.unwrap();
338
339 let token = if let Some(outer_token) = outer_token {
340 outer_token.clone()
341 } else {
342 let authorization = self.req.headers().get("authorization");
343
344 authorization?;
345
346 let authorization = authorization.unwrap().to_str();
347
348 if authorization.is_err() {
349 return None;
350 }
351
352 let authorization = authorization.unwrap();
353
354 authorization.replace("Bearer ", "")
355 };
356
357 let token_message = decode::<TokenClaims>(
358 &token,
359 &DecodingKey::from_secret(&encoding_secret.into_bytes()),
360 &Validation::new(Algorithm::HS256),
361 );
362
363 if token_message.is_err() {
364 return None;
365 }
366
367 let token_message = token_message.unwrap();
368
369 Some(token_message.claims)
370 }
371
372 fn get_verify_user_claims(claims: Option<TokenClaims>, player_id: &PlayerId) -> bool {
373 if claims.is_none() {
374 debug!("No claims for player_id: {player_id}");
375 return false;
376 }
377
378 let claims = claims.unwrap();
379 claims.sub == *player_id
380 }
381
382 fn get_verify_admin_claims(claims: Option<TokenClaims>) -> bool {
383 if claims.is_none() {
384 return false;
385 }
386
387 let claims = claims.unwrap();
388
389 claims.role == UserRole::Admin
390 }
391
392 pub fn verify_user(&self, player_id: &PlayerId) -> Result<(), UnauthorizedError> {
393 let claims = self.get_token_claims(None);
394
395 let is_user = AuthHandler::get_verify_user_claims(claims, player_id);
396
397 if !is_user {
398 return Err(UnauthorizedError);
399 }
400
401 Ok(())
402 }
403
404 pub fn get_user_from_token(&self) -> Result<String, UnauthorizedError> {
405 let claims = self.get_token_claims(None);
406
407 if claims.is_none() {
408 return Err(UnauthorizedError);
409 }
410
411 Ok(claims.unwrap().sub)
412 }
413
414 pub fn verify_user_token(&self, player_id: &PlayerId, token: &String) -> bool {
415 let claims = self.get_token_claims(Some(token));
416
417 AuthHandler::get_verify_user_claims(claims, player_id)
418 }
419
420 pub fn verify_admin(&self) -> Result<(), UnauthorizedError> {
421 let claims = self.get_token_claims(None);
422
423 let is_admin = AuthHandler::get_verify_admin_claims(claims);
424
425 if !is_admin {
426 return Err(UnauthorizedError);
427 }
428
429 Ok(())
430 }
431
432 pub fn verify_admin_token(&self, token: &String) -> bool {
433 let claims = self.get_token_claims(Some(token));
434
435 AuthHandler::get_verify_admin_claims(claims)
436 }
437
438 pub fn get_unauthorized() -> HttpResponse {
439 HttpResponse::Unauthorized().body("Unauthorized")
440 }
441
442 pub async fn get_auth_info_summary(&self) -> Result<AuthInfoSummary, AuthInfoSummaryError> {
443 let user_id = self
444 .get_user_from_token()
445 .map_err(|_| AuthInfoSummaryError::Unauthorized)?;
446
447 let user = self
448 .storage
449 .get_auth_info(GetAuthInfo::PlayerId(user_id.clone()))
450 .await;
451
452 if user.is_err() {
453 return Err(AuthInfoSummaryError::DatabaseError);
454 }
455
456 let user = user
457 .unwrap()
458 .map_or_else(|| Err(AuthInfoSummaryError::DatabaseError), Ok)?;
459
460 let summary = match user.data {
461 AuthInfoData::Anonymous(_) => AuthInfoSummary {
462 provider: AuthProvider::Anonymous,
463 username: None,
464 },
465 AuthInfoData::Email(email) => AuthInfoSummary {
466 provider: AuthProvider::Email,
467 username: Some(email.username),
468 },
469 AuthInfoData::Github(github) => AuthInfoSummary {
470 provider: AuthProvider::Github,
471 username: Some(github.username),
472 },
473 };
474
475 Ok(summary)
476 }
477}