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 =
38        WebAuthnRegistry::get_or_create_service(oauth_repo, user_provider).await?;
39
40    let (challenge, challenge_id) = webauthn_service
41        .start_authentication(&params.email, params.oauth_state)
42        .await
43        .map_err(|e| {
44            let http: OAuthHttpError = e.into();
45            if matches!(http.code(), crate::routes::oauth::OAuthErrorCode::NotFound) {
46                http
47            } else {
48                OAuthHttpError::authentication_failed(http.description().to_owned())
49            }
50        })?;
51
52    let challenge_json = serde_json::to_value(&challenge)
53        .map_err(|e| OAuthHttpError::server_error(format!("Failed to serialize challenge: {e}")))?;
54
55    let mut public_key = challenge_json
56        .get("publicKey")
57        .cloned()
58        .ok_or_else(|| OAuthHttpError::server_error("Missing publicKey in challenge"))?;
59
60    if let Some(obj) = public_key.as_object_mut() {
61        obj.remove("authenticatorAttachment");
62    }
63
64    Ok((
65        StatusCode::OK,
66        Json(StartAuthResponse {
67            public_key,
68            challenge_id: ChallengeId::new(challenge_id),
69        }),
70    )
71        .into_response())
72}
73
74#[derive(Debug, Deserialize)]
75pub struct FinishAuthRequest {
76    pub challenge_id: ChallengeId,
77    pub credential: PublicKeyCredential,
78}
79
80#[derive(Debug, Serialize)]
81pub struct FinishAuthResponse {
82    pub user_id: UserId,
83    pub oauth_state: Option<String>,
84    pub success: bool,
85    pub auth_token: Option<String>,
86}
87
88#[instrument(skip(state, oauth_repo, request), fields(challenge_id = %request.challenge_id))]
89pub async fn finish_auth(
90    State(state): State<OAuthState>,
91    OAuthRepo(oauth_repo): OAuthRepo,
92    Json(request): Json<FinishAuthRequest>,
93) -> Result<Response, OAuthHttpError> {
94    let user_provider = Arc::clone(state.user_provider());
95
96    let webauthn_service =
97        WebAuthnRegistry::get_or_create_service(oauth_repo, user_provider).await?;
98
99    let (user_id, oauth_state) = webauthn_service
100        .finish_authentication(request.challenge_id.as_str(), &request.credential)
101        .await
102        .map_err(|e| OAuthHttpError::authentication_failed(e.to_string()))?;
103
104    let auth_token = systemprompt_oauth::services::generate_secure_token("webauthn_verified");
105    webauthn_service
106        .store_verified_authentication(auth_token.clone(), user_id.clone())
107        .await;
108
109    Ok((
110        StatusCode::OK,
111        Json(FinishAuthResponse {
112            user_id,
113            oauth_state,
114            success: true,
115            auth_token: Some(auth_token),
116        }),
117    )
118        .into_response())
119}