systemprompt_api/routes/oauth/webauthn/
authenticate.rs1use 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(¶ms.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}