Skip to main content

mogh_auth_server/
lib.rs

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  /// The user ID comes from JWT, which is already validated by the JwtProvider.
43  UserId(String),
44  /// X-API-KEY and X-API-SECRET.
45  /// DANGER ⚠️ the key and secret must still be validated.
46  KeyAndSecret { key: String, secret: String },
47  /// X-API-SIGNATURE and X-API-TIMESTAMP. The handshake produces the public key.
48  /// DANGER ⚠️ the public key must still be validated as belonging to a particular client.
49  PublicKey(String),
50}
51
52/// This trait is implemented at the app level
53/// to support custom schemas, storage providers, and business logic.
54pub trait AuthImpl: Send + Sync + 'static {
55  /// Construct the auth implementation for extraction.
56  /// Only use this at the top level of a client request.
57  fn new() -> Self
58  where
59    Self: Sized;
60
61  /// Provide a static app name for passkeys.
62  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  /// Provide the app 'host' config.
69  /// Example: https://example.com
70  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  /// This should be the path to where the auth server is nested on 'host'.
77  /// Default is "/auth".
78  fn path(&self) -> &str {
79    "/auth"
80  }
81
82  /// Disable new user registration.
83  fn registration_disabled(&self) -> bool {
84    false
85  }
86
87  /// Provide usernames to lock credential updates for,
88  /// such as demo users.
89  fn locked_usernames(&self) -> &'static [String] {
90    &[]
91  }
92
93  /// If the locked usernames includes '__ALL__',
94  /// this will always error.
95  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  /// Allow user to register even when registration is disabled
114  /// when no users exist. If not implemented, this always evaluates
115  /// to false and does not change any behavior.
116  fn no_users_exist(&self) -> DynFuture<mogh_error::Result<bool>> {
117    Box::pin(async { Ok(false) })
118  }
119
120  /// Get's the user using the user id, returning UNAUTHORIZED if none exists.
121  fn get_user(
122    &self,
123    user_id: String,
124  ) -> DynFuture<mogh_error::Result<BoxAuthUser>>;
125
126  /// Handle incoming request authentication in middleware.
127  /// Can attach a client struct as request extension here.
128  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  /// Get user id from request authentication
136  /// for use in auth management API middleware.
137  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  // =========
155  // = STATE =
156  // =========
157
158  /// Get the jwt provider.
159  fn jwt_provider(&self) -> &JwtProvider;
160
161  /// Get the webauthn passkey provider
162  fn passkey_provider(&self) -> Option<&PasskeyProvider> {
163    None
164  }
165
166  /// Provide a rate limiter for
167  /// general authenticated requests.
168  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  /// Where to default redirect after linking an external login method.
175  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  // ==============
182  // = LOCAL AUTH =
183  // ==============
184
185  /// Whether local auth is enabled.
186  fn local_auth_enabled(&self) -> bool {
187    true
188  }
189
190  /// Set the password hash bcrypt cost.
191  fn local_auth_bcrypt_cost(&self) -> u32 {
192    10
193  }
194
195  /// Local login method can have it's own rate limiter
196  /// for 1 to 1 user feedback on remaining attempts.
197  /// By default uses the general rate limiter.
198  fn local_login_rate_limiter(&self) -> &RateLimiter {
199    self.general_rate_limiter()
200  }
201
202  /// Validate usernames.
203  fn validate_username(
204    &self,
205    username: &str,
206  ) -> mogh_error::Result<()> {
207    validate_username(username).status_code(StatusCode::BAD_REQUEST)
208  }
209
210  /// Validate passwords.
211  fn validate_password(
212    &self,
213    password: &str,
214  ) -> mogh_error::Result<()> {
215    validate_password(password).status_code(StatusCode::BAD_REQUEST)
216  }
217
218  /// Returns created user id, or error.
219  /// The username and password have already been validated.
220  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  /// Finds user using the username, returning UNAUTHORIZED if none exists.
237  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  // =============
278  // = OIDC AUTH =
279  // =============
280
281  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  /// Returns created user id, or error.
300  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  // ==============
327  // = NAMED AUTH =
328  // ==============
329
330  // = GITHUB =
331
332  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  /// Returns created user id, or error.
351  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  // = GOOGLE =
381
382  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  /// Returns created user id, or error.
401  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  // ==========
431  // = UNLINK =
432  // ==========
433
434  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  // ===============
445  // = PASSKEY 2FA =
446  // ===============
447
448  /// If Some(Passkey) is passed, it should be stored,
449  /// overriding any passkey which was on the User.
450  ///
451  /// If None is passed, the user passkey should be removed,
452  /// unenrolling the user from passkey 2fa.
453  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  // ============
469  // = TOTP 2FA =
470  // ============
471
472  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  // ============
520  // = SKIP 2FA =
521  // ============
522  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  // ============
538  // = API KEYS =
539  // ============
540  /// Validate api key name.
541  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  /// Set custom API key length. Default is 40.
550  fn api_key_secret_length(&self) -> usize {
551    40
552  }
553
554  /// Set the api secret hash bcrypt cost.
555  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  /// Get the user id for a given API key
574  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  /// Pass the server private key to use with api key v2 handshakes.
598  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  /// Get the user id for a given public key
617  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}