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