Skip to main content

systemprompt_api/routes/oauth/webauthn/
authenticate.rs

1use axum::Json;
2use axum::extract::{Query, State};
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::{Deserialize, Serialize};
6use std::sync::Arc;
7use systemprompt_identifiers::{ChallengeId, UserId};
8use systemprompt_oauth::OAuthState;
9use systemprompt_oauth::services::webauthn::WebAuthnRegistry;
10use tracing::instrument;
11use webauthn_rs::prelude::*;
12
13use crate::routes::oauth::OAuthHttpError;
14use crate::routes::oauth::extractors::OAuthRepo;
15
16#[derive(Debug, Deserialize)]
17pub struct StartAuthQuery {
18    pub email: String,
19    pub oauth_state: Option<String>,
20}
21
22#[derive(Debug, Serialize)]
23pub struct StartAuthResponse {
24    #[serde(rename = "publicKey")]
25    pub public_key: serde_json::Value,
26    pub challenge_id: ChallengeId,
27}
28
29#[instrument(skip(state, oauth_repo, params), fields(email = %params.email))]
30pub async fn start_auth(
31    Query(params): Query<StartAuthQuery>,
32    State(state): State<OAuthState>,
33    OAuthRepo(oauth_repo): OAuthRepo,
34) -> Result<Response, OAuthHttpError> {
35    let user_provider = Arc::clone(state.user_provider());
36
37    let webauthn_service = WebAuthnRegistry::get_or_create_service(oauth_repo, user_provider)
38        .await
39        .map_err(|e| OAuthHttpError::server_error(format!("Failed to initialize WebAuthn: {e}")))?;
40
41    let (challenge, challenge_id) = webauthn_service
42        .start_authentication(&params.email, params.oauth_state)
43        .await
44        .map_err(|e| {
45            let http: OAuthHttpError = e.into();
46            if matches!(http.code(), crate::routes::oauth::OAuthErrorCode::NotFound) {
47                http
48            } else {
49                OAuthHttpError::authentication_failed(http.description().to_owned())
50            }
51        })?;
52
53    let challenge_json = serde_json::to_value(&challenge)
54        .map_err(|e| OAuthHttpError::server_error(format!("Failed to serialize challenge: {e}")))?;
55
56    let mut public_key = challenge_json
57        .get("publicKey")
58        .cloned()
59        .ok_or_else(|| OAuthHttpError::server_error("Missing publicKey in challenge"))?;
60
61    if let Some(obj) = public_key.as_object_mut() {
62        obj.remove("authenticatorAttachment");
63    }
64
65    Ok((
66        StatusCode::OK,
67        Json(StartAuthResponse {
68            public_key,
69            challenge_id: ChallengeId::new(challenge_id),
70        }),
71    )
72        .into_response())
73}
74
75#[derive(Debug, Deserialize)]
76pub struct FinishAuthRequest {
77    pub challenge_id: ChallengeId,
78    pub credential: PublicKeyCredential,
79}
80
81#[derive(Debug, Serialize)]
82pub struct FinishAuthResponse {
83    pub user_id: UserId,
84    pub oauth_state: Option<String>,
85    pub success: bool,
86    pub auth_token: Option<String>,
87}
88
89#[instrument(skip(state, oauth_repo, request), fields(challenge_id = %request.challenge_id))]
90pub async fn finish_auth(
91    State(state): State<OAuthState>,
92    OAuthRepo(oauth_repo): OAuthRepo,
93    Json(request): Json<FinishAuthRequest>,
94) -> Result<Response, OAuthHttpError> {
95    let user_provider = Arc::clone(state.user_provider());
96
97    let webauthn_service = WebAuthnRegistry::get_or_create_service(oauth_repo, user_provider)
98        .await
99        .map_err(|e| OAuthHttpError::server_error(format!("Failed to initialize WebAuthn: {e}")))?;
100
101    let (user_id, oauth_state) = webauthn_service
102        .finish_authentication(request.challenge_id.as_str(), &request.credential)
103        .await
104        .map_err(|e| OAuthHttpError::authentication_failed(e.to_string()))?;
105
106    let auth_token = systemprompt_oauth::services::generate_secure_token("webauthn_verified");
107    webauthn_service
108        .store_verified_authentication(auth_token.clone(), user_id.clone())
109        .await;
110
111    Ok((
112        StatusCode::OK,
113        Json(FinishAuthResponse {
114            user_id,
115            oauth_state,
116            success: true,
117            auth_token: Some(auth_token),
118        }),
119    )
120        .into_response())
121}