1use std::sync::{Arc, LazyLock};
2
3use anyhow::{Context as _, anyhow};
4use axum::{extract::Request, http::StatusCode};
5use mogh_auth_client::{
6 api::{login::LoginProvider, manage::CreateApiKey},
7 config::{NamedOauthConfig, OidcConfig},
8 passkey::Passkey,
9};
10use mogh_error::{AddStatusCode, AddStatusCodeError};
11use mogh_pki::RotatableKeyPair;
12use mogh_rate_limit::RateLimiter;
13use openidconnect::SubjectIdentifier;
14
15pub mod api;
16pub mod middleware;
17pub mod provider;
18pub mod rand;
19pub mod user;
20pub mod validations;
21
22mod session;
23
24use crate::{
25 provider::{jwt::JwtProvider, passkey::PasskeyProvider},
26 user::BoxAuthUser,
27 validations::{
28 validate_api_key_name, validate_password, validate_username,
29 },
30};
31
32pub mod request_ip {
33 pub use mogh_request_ip::*;
34}
35
36pub type BoxAuthImpl = Box<dyn AuthImpl>;
37pub type DynFuture<O> =
38 std::pin::Pin<Box<dyn Future<Output = O> + Send>>;
39
40#[derive(Clone)]
41pub enum RequestAuthentication {
42 UserId(String),
44 KeyAndSecret { key: String, secret: String },
47 PublicKey(String),
50}
51
52pub trait AuthImpl: Send + Sync + 'static {
55 fn new() -> Self
58 where
59 Self: Sized;
60
61 fn app_name(&self) -> &'static str {
63 panic!(
64 "Must implement 'AuthImpl::app_name' in order for passkey 2fa to work."
65 )
66 }
67
68 fn host(&self) -> &str {
71 panic!(
72 "Must implement 'AuthImpl::host' in order for external logins and other features to work."
73 )
74 }
75
76 fn path(&self) -> &str {
79 "/auth"
80 }
81
82 fn registration_disabled(&self) -> bool {
84 false
85 }
86
87 fn locked_usernames(&self) -> &'static [String] {
90 &[]
91 }
92
93 fn check_username_locked(
96 &self,
97 username: &str,
98 ) -> mogh_error::Result<()> {
99 if self
100 .locked_usernames()
101 .iter()
102 .any(|locked| locked == username || locked == "__ALL__")
103 {
104 Err(
105 anyhow!("Login credentials are locked for this user")
106 .status_code(StatusCode::UNAUTHORIZED),
107 )
108 } else {
109 Ok(())
110 }
111 }
112
113 fn no_users_exist(&self) -> DynFuture<mogh_error::Result<bool>> {
117 Box::pin(async { Ok(false) })
118 }
119
120 fn get_user(
122 &self,
123 user_id: String,
124 ) -> DynFuture<mogh_error::Result<BoxAuthUser>>;
125
126 fn handle_request_authentication(
129 &self,
130 auth: RequestAuthentication,
131 require_user_enabled: bool,
132 req: Request,
133 ) -> DynFuture<mogh_error::Result<Request>>;
134
135 fn get_user_id_from_request_authentication(
138 &self,
139 auth: RequestAuthentication,
140 ) -> DynFuture<mogh_error::Result<String>> {
141 match auth {
142 RequestAuthentication::UserId(user_id) => {
143 Box::pin(async { Ok(user_id) })
144 }
145 RequestAuthentication::KeyAndSecret { key, .. } => {
146 self.get_api_key_user_id(key)
147 }
148 RequestAuthentication::PublicKey(public_key) => {
149 self.get_api_key_v2_user_id(public_key)
150 }
151 }
152 }
153
154 fn jwt_provider(&self) -> &JwtProvider;
160
161 fn passkey_provider(&self) -> Option<&PasskeyProvider> {
163 None
164 }
165
166 fn general_rate_limiter(&self) -> &RateLimiter {
169 static DISABLED_RATE_LIMITER: LazyLock<Arc<RateLimiter>> =
170 LazyLock::new(|| RateLimiter::new(true, 0, 0));
171 &DISABLED_RATE_LIMITER
172 }
173
174 fn post_link_redirect(&self) -> &str {
176 panic!(
177 "Must implement 'AuthImpl::post_link_redirect' in order for linking to work. This is usually the application profile or settings page."
178 )
179 }
180
181 fn local_auth_enabled(&self) -> bool {
187 true
188 }
189
190 fn local_auth_bcrypt_cost(&self) -> u32 {
192 10
193 }
194
195 fn local_login_rate_limiter(&self) -> &RateLimiter {
199 self.general_rate_limiter()
200 }
201
202 fn validate_username(
204 &self,
205 username: &str,
206 ) -> mogh_error::Result<()> {
207 validate_username(username).status_code(StatusCode::BAD_REQUEST)
208 }
209
210 fn validate_password(
212 &self,
213 password: &str,
214 ) -> mogh_error::Result<()> {
215 validate_password(password).status_code(StatusCode::BAD_REQUEST)
216 }
217
218 fn sign_up_local_user(
221 &self,
222 _username: String,
223 _hashed_password: String,
224 _no_users_exist: bool,
225 ) -> DynFuture<mogh_error::Result<String>> {
226 Box::pin(async {
227 Err(
228 anyhow!(
229 "Must implement 'AuthImpl::sign_up_local_user' in order for local login to work."
230 )
231 .into(),
232 )
233 })
234 }
235
236 fn find_user_with_username(
238 &self,
239 _username: String,
240 ) -> DynFuture<mogh_error::Result<Option<BoxAuthUser>>> {
241 Box::pin(async {
242 Err(
243 anyhow!(
244 "Must implement 'AuthImpl::find_user_with_username' in order for local login to work."
245 )
246 .into(),
247 )
248 })
249 }
250
251 fn update_user_username(
252 &self,
253 _user_id: String,
254 _username: String,
255 ) -> DynFuture<mogh_error::Result<()>> {
256 Box::pin(async {
257 Err(
258 anyhow!("Must implement 'AuthImpl::update_user_username'.")
259 .into(),
260 )
261 })
262 }
263
264 fn update_user_password(
265 &self,
266 _user_id: String,
267 _hashed_password: String,
268 ) -> DynFuture<mogh_error::Result<()>> {
269 Box::pin(async {
270 Err(
271 anyhow!("Must implement 'AuthImpl::update_user_password'.")
272 .into(),
273 )
274 })
275 }
276
277 fn oidc_config(&self) -> Option<&OidcConfig> {
282 None
283 }
284
285 fn find_user_with_oidc_subject(
286 &self,
287 _subject: SubjectIdentifier,
288 ) -> DynFuture<mogh_error::Result<Option<BoxAuthUser>>> {
289 Box::pin(async {
290 Err(
291 anyhow!(
292 "Must implement 'AuthImpl::find_user_with_oidc_subject'."
293 )
294 .into(),
295 )
296 })
297 }
298
299 fn sign_up_oidc_user(
301 &self,
302 _username: String,
303 _subject: SubjectIdentifier,
304 _no_users_exist: bool,
305 ) -> DynFuture<mogh_error::Result<String>> {
306 Box::pin(async {
307 Err(
308 anyhow!("Must implement 'AuthImpl::sign_up_oidc_user'.")
309 .into(),
310 )
311 })
312 }
313
314 fn link_oidc_login(
315 &self,
316 _user_id: String,
317 _subject: SubjectIdentifier,
318 ) -> DynFuture<mogh_error::Result<()>> {
319 Box::pin(async {
320 Err(
321 anyhow!("Must implement 'AuthImpl::link_oidc_login'.").into(),
322 )
323 })
324 }
325
326 fn github_config(&self) -> Option<&NamedOauthConfig> {
333 None
334 }
335
336 fn find_user_with_github_id(
337 &self,
338 _github_id: String,
339 ) -> DynFuture<mogh_error::Result<Option<BoxAuthUser>>> {
340 Box::pin(async {
341 Err(
342 anyhow!(
343 "Must implement 'AuthImpl::find_user_with_github_id'."
344 )
345 .into(),
346 )
347 })
348 }
349
350 fn sign_up_github_user(
352 &self,
353 _username: String,
354 _github_id: String,
355 _avatar_url: String,
356 _no_users_exist: bool,
357 ) -> DynFuture<mogh_error::Result<String>> {
358 Box::pin(async {
359 Err(
360 anyhow!("Must implement 'AuthImpl::sign_up_github_user'.")
361 .into(),
362 )
363 })
364 }
365
366 fn link_github_login(
367 &self,
368 _user_id: String,
369 _github_id: String,
370 _avatar_url: String,
371 ) -> DynFuture<mogh_error::Result<()>> {
372 Box::pin(async {
373 Err(
374 anyhow!("Must implement 'AuthImpl::link_github_login'.")
375 .into(),
376 )
377 })
378 }
379
380 fn google_config(&self) -> Option<&NamedOauthConfig> {
383 None
384 }
385
386 fn find_user_with_google_id(
387 &self,
388 _google_id: String,
389 ) -> DynFuture<mogh_error::Result<Option<BoxAuthUser>>> {
390 Box::pin(async {
391 Err(
392 anyhow!(
393 "Must implement 'AuthImpl::find_user_with_google_id'."
394 )
395 .into(),
396 )
397 })
398 }
399
400 fn sign_up_google_user(
402 &self,
403 _username: String,
404 _google_id: String,
405 _avatar_url: String,
406 _no_users_exist: bool,
407 ) -> DynFuture<mogh_error::Result<String>> {
408 Box::pin(async {
409 Err(
410 anyhow!("Must implement 'AuthImpl::sign_up_google_user'.")
411 .into(),
412 )
413 })
414 }
415
416 fn link_google_login(
417 &self,
418 _user_id: String,
419 _google_id: String,
420 _avatar_url: String,
421 ) -> DynFuture<mogh_error::Result<()>> {
422 Box::pin(async {
423 Err(
424 anyhow!("Must implement 'AuthImpl::link_google_login'.")
425 .into(),
426 )
427 })
428 }
429
430 fn unlink_login(
435 &self,
436 _user_id: String,
437 _provider: LoginProvider,
438 ) -> DynFuture<mogh_error::Result<()>> {
439 Box::pin(async {
440 Err(anyhow!("Must implement 'AuthImpl::unlink_login'.").into())
441 })
442 }
443
444 fn update_user_stored_passkey(
454 &self,
455 _user_id: String,
456 _passkey: Option<Passkey>,
457 ) -> DynFuture<mogh_error::Result<()>> {
458 Box::pin(async {
459 Err(
460 anyhow!(
461 "Must implement 'AuthImpl::update_user_stored_passkey'."
462 )
463 .into(),
464 )
465 })
466 }
467
468 fn update_user_stored_totp(
473 &self,
474 _user_id: String,
475 _encoded_secret: String,
476 _hashed_recovery_codes: Vec<String>,
477 ) -> DynFuture<mogh_error::Result<()>> {
478 Box::pin(async {
479 Err(
480 anyhow!(
481 "Must implement 'AuthImpl::update_user_stored_totp'."
482 )
483 .into(),
484 )
485 })
486 }
487
488 fn remove_user_stored_totp(
489 &self,
490 _user_id: String,
491 ) -> DynFuture<mogh_error::Result<()>> {
492 Box::pin(async {
493 Err(
494 anyhow!(
495 "Must implement 'AuthImpl::remove_user_stored_totp'."
496 )
497 .into(),
498 )
499 })
500 }
501
502 fn make_totp(
503 &self,
504 secret_bytes: Vec<u8>,
505 account_name: Option<String>,
506 ) -> anyhow::Result<totp_rs::TOTP> {
507 totp_rs::TOTP::new(
508 totp_rs::Algorithm::SHA1,
509 6,
510 1,
511 30,
512 secret_bytes,
513 Some(String::from(self.app_name())),
514 account_name.unwrap_or_default(),
515 )
516 .context("Failed to construct TOTP")
517 }
518
519 fn update_user_external_skip_2fa(
523 &self,
524 _user_id: String,
525 _external_skip_2fa: bool,
526 ) -> DynFuture<mogh_error::Result<()>> {
527 Box::pin(async {
528 Err(
529 anyhow!(
530 "Must implement 'AuthImpl::update_user_external_skip_2fa'."
531 )
532 .into(),
533 )
534 })
535 }
536
537 fn validate_api_key_name(
542 &self,
543 api_key_name: &str,
544 ) -> mogh_error::Result<()> {
545 validate_api_key_name(api_key_name)
546 .status_code(StatusCode::BAD_REQUEST)
547 }
548
549 fn api_key_secret_length(&self) -> usize {
551 40
552 }
553
554 fn api_secret_bcrypt_cost(&self) -> u32 {
556 self.local_auth_bcrypt_cost()
557 }
558
559 fn create_api_key(
560 &self,
561 _user_id: String,
562 _body: CreateApiKey,
563 _key: String,
564 _hashed_secret: String,
565 ) -> DynFuture<mogh_error::Result<()>> {
566 Box::pin(async {
567 Err(
568 anyhow!("Must implement 'AuthImpl::create_api_key'.").into(),
569 )
570 })
571 }
572
573 fn get_api_key_user_id(
575 &self,
576 _key: String,
577 ) -> DynFuture<mogh_error::Result<String>> {
578 Box::pin(async {
579 Err(
580 anyhow!("Must implement 'AuthImpl::get_api_key_user_id'.")
581 .into(),
582 )
583 })
584 }
585
586 fn delete_api_key(
587 &self,
588 _key: String,
589 ) -> DynFuture<mogh_error::Result<()>> {
590 Box::pin(async {
591 Err(
592 anyhow!("Must implement 'AuthImpl::delete_api_key'.").into(),
593 )
594 })
595 }
596
597 fn server_private_key(&self) -> Option<&RotatableKeyPair> {
599 None
600 }
601
602 fn create_api_key_v2(
603 &self,
604 _user_id: String,
605 _body: CreateApiKey,
606 _public_key: String,
607 ) -> DynFuture<mogh_error::Result<()>> {
608 Box::pin(async {
609 Err(
610 anyhow!("Must implement 'AuthImpl::create_api_key_v2'.")
611 .into(),
612 )
613 })
614 }
615
616 fn get_api_key_v2_user_id(
618 &self,
619 _public_key: String,
620 ) -> DynFuture<mogh_error::Result<String>> {
621 Box::pin(async {
622 Err(
623 anyhow!("Must implement 'AuthImpl::get_api_key_v2_user_id'.")
624 .into(),
625 )
626 })
627 }
628
629 fn delete_api_key_v2(
630 &self,
631 _public_key: String,
632 ) -> DynFuture<mogh_error::Result<()>> {
633 Box::pin(async {
634 Err(
635 anyhow!("Must implement 'AuthImpl::delete_api_key_v2'.")
636 .into(),
637 )
638 })
639 }
640}