mogh_auth_server/api/
mod.rs

1use anyhow::{Context as _, anyhow};
2use axum::{Router, response::Redirect, routing::get};
3use mogh_auth_client::api::login::UserIdOrTwoFactor;
4use mogh_error::{AddStatusCode as _, AddStatusCodeError as _};
5use reqwest::StatusCode;
6use serde::Deserialize;
7use utoipa::ToSchema;
8
9use crate::{AuthImpl, session::Session, user::BoxAuthUser};
10
11pub mod login;
12pub mod manage;
13pub mod named;
14pub mod oidc;
15
16/// This router should be nested without any additional middleware
17pub fn router<I: AuthImpl>() -> Router {
18  Router::new()
19    .route("/version", get(|| async { env!("CARGO_PKG_VERSION") }))
20    .nest("/login", login::router::<I>())
21    .nest("/manage", manage::router::<I>())
22    .nest("/oidc", oidc::router::<I>())
23    .merge(named::router::<I>())
24}
25
26#[derive(serde::Deserialize)]
27struct Variant {
28  variant: String,
29}
30
31#[derive(serde::Deserialize)]
32pub struct RedirectQuery {
33  redirect: Option<String>,
34}
35
36#[derive(Debug, Deserialize, ToSchema)]
37pub struct StandardCallbackQuery {
38  pub state: Option<String>,
39  pub code: Option<String>,
40  pub error: Option<String>,
41}
42
43impl StandardCallbackQuery {
44  /// Returns (state, code)
45  pub fn open(self) -> mogh_error::Result<(String, String)> {
46    if let Some(e) = self.error {
47      return Err(
48        anyhow!("Provider returned error: {e}")
49          .status_code(StatusCode::UNAUTHORIZED),
50      );
51    }
52    let state = self
53      .state
54      .context("Callback query does not contain state")
55      .status_code(StatusCode::UNAUTHORIZED)?;
56    let code = self
57      .code
58      .context("Callback query does not contain code")
59      .status_code(StatusCode::UNAUTHORIZED)?;
60
61    Ok((state, code))
62  }
63}
64
65fn format_redirect(
66  host: &str,
67  redirect: Option<&str>,
68  extra: &str,
69) -> Redirect {
70  let redirect_url = if let Some(redirect) = redirect
71    && !redirect.is_empty()
72  {
73    let splitter = if extra.is_empty() {
74      ""
75    } else if redirect.contains('?') {
76      "&"
77    } else {
78      "?"
79    };
80    format!("{redirect}{splitter}{extra}")
81  } else {
82    format!(
83      "{host}{}{extra}",
84      if extra.is_empty() { "" } else { "?" }
85    )
86  };
87  Redirect::to(&redirect_url)
88}
89
90async fn get_user_id_or_two_factor<I: AuthImpl>(
91  auth: &I,
92  session: &Session,
93  user: &BoxAuthUser,
94) -> mogh_error::Result<UserIdOrTwoFactor> {
95  let res = match (
96    user.external_skip_2fa(),
97    user.passkey(),
98    user.totp_secret(),
99  ) {
100    // Skip / No 2FA
101    (true, _, _) | (false, None, None) => {
102      session.insert_authenticated_user_id(user.id()).await?;
103      UserIdOrTwoFactor::UserId(user.id().to_string())
104    }
105    // WebAuthn Passkey 2FA
106    (false, Some(passkey), _) => {
107      let provider = auth.passkey_provider().context(
108        "No passkey provider available, possibly invalid 'host' config.",
109      )?;
110      let (response, state) = provider
111        .start_passkey_authentication(passkey)
112        .context("Failed to start passkey authentication flow")?;
113      session.insert_passkey_login(user.id(), &state).await?;
114      UserIdOrTwoFactor::Passkey(response)
115    }
116    // TOTP 2FA
117    (false, None, Some(_)) => {
118      session.insert_totp_login_user_id(user.id()).await?;
119      UserIdOrTwoFactor::Totp {}
120    }
121  };
122  Ok(res)
123}
124
125fn user_id_or_two_factor_redirect<I: AuthImpl>(
126  auth: &I,
127  user_id_or_two_factor: UserIdOrTwoFactor,
128  redirect: Option<&str>,
129) -> mogh_error::Result<Redirect> {
130  match user_id_or_two_factor {
131    UserIdOrTwoFactor::UserId(_) => {
132      Ok(format_redirect(auth.host(), redirect, "redeem_ready=true"))
133    }
134    UserIdOrTwoFactor::Totp {} => {
135      Ok(format_redirect(auth.host(), redirect, "totp=true"))
136    }
137    UserIdOrTwoFactor::Passkey(passkey) => {
138      let passkey = serde_json::to_string(&passkey)
139        .context("Failed to serialize passkey response")?;
140      let passkey = urlencoding::encode(&passkey);
141      Ok(format_redirect(
142        auth.host(),
143        redirect,
144        &format!("passkey={passkey}"),
145      ))
146    }
147  }
148}