mahjong_service/auth/
mod.rs

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}