mogh_auth_server/api/
mod.rs1use 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
16pub 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 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 (true, _, _) | (false, None, None) => {
102 session.insert_authenticated_user_id(user.id()).await?;
103 UserIdOrTwoFactor::UserId(user.id().to_string())
104 }
105 (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 (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}