Skip to main content

systemprompt_api/routes/oauth/webauthn/
authenticate.rs

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