systemprompt_api/routes/oauth/endpoints/
webauthn_complete.rs1use axum::Json;
12use axum::extract::{Query, State};
13use axum::http::HeaderMap;
14use axum::response::{IntoResponse, Redirect, Response};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17
18use crate::routes::oauth::OAuthHttpError;
19use crate::routes::oauth::extractors::OAuthRepo;
20use crate::services::request_base_url::RequestBaseUrl;
21use systemprompt_identifiers::{ClientId, UserId};
22use systemprompt_models::oauth::OAuthServerConfig;
23use systemprompt_oauth::OAuthState;
24use systemprompt_oauth::repository::{MintAuthCodeParams, OAuthRepository};
25use systemprompt_oauth::services::is_browser_request;
26use systemprompt_oauth::services::webauthn::WebAuthnRegistry;
27
28#[derive(Debug, Deserialize)]
29pub struct WebAuthnCompleteQuery {
30 pub user_id: UserId,
31 pub auth_token: Option<String>,
32 pub response_type: Option<String>,
33 pub client_id: Option<ClientId>,
34 pub redirect_uri: Option<String>,
35 pub scope: Option<String>,
36 pub state: Option<String>,
37 pub code_challenge: Option<String>,
38 pub code_challenge_method: Option<String>,
39 pub response_mode: Option<String>,
40 pub resource: Option<String>,
41}
42
43async fn verify_completion(
44 params: &WebAuthnCompleteQuery,
45 state: &OAuthState,
46 repo: &OAuthRepository,
47) -> Result<(UserId, String), OAuthHttpError> {
48 let auth_token = params
49 .auth_token
50 .as_deref()
51 .ok_or_else(|| OAuthHttpError::invalid_request("Missing auth_token parameter"))?;
52
53 let webauthn_service =
54 WebAuthnRegistry::get_or_create_service(repo.clone(), Arc::clone(state.user_provider()))
55 .await
56 .map_err(|e| {
57 OAuthHttpError::server_error(format!("WebAuthn service initialization failed: {e}"))
58 })?;
59
60 let verified_user_id = webauthn_service
61 .consume_verified_authentication(auth_token)
62 .await
63 .map_err(|_e| OAuthHttpError::access_denied("Invalid or expired authentication token"))?;
64
65 if params.user_id != verified_user_id {
66 return Err(OAuthHttpError::access_denied(
67 "User identity verification failed",
68 ));
69 }
70
71 if params.client_id.is_none() {
72 return Err(OAuthHttpError::invalid_request(
73 "Missing client_id parameter",
74 ));
75 }
76
77 let redirect_uri = params
78 .redirect_uri
79 .clone()
80 .ok_or_else(|| OAuthHttpError::invalid_request("Missing redirect_uri parameter"))?;
81
82 Ok((verified_user_id, redirect_uri))
83}
84
85pub async fn handle_webauthn_complete(
86 headers: HeaderMap,
87 base: RequestBaseUrl,
88 Query(params): Query<WebAuthnCompleteQuery>,
89 State(state): State<OAuthState>,
90 OAuthRepo(repo): OAuthRepo,
91) -> Result<Response, OAuthHttpError> {
92 let (verified_user_id, redirect_uri) = verify_completion(¶ms, &state, &repo).await?;
93
94 let user = state.user_provider().find_by_id(&verified_user_id).await?;
95 if user.is_none() {
96 return Err(OAuthHttpError::access_denied("User not found"));
97 }
98
99 let client_id = params
100 .client_id
101 .as_ref()
102 .ok_or_else(|| OAuthHttpError::invalid_request("client_id is required"))?;
103 let authorization_code = repo
104 .mint_authorization_code(MintAuthCodeParams {
105 client_id,
106 user_id: ¶ms.user_id,
107 redirect_uri: &redirect_uri,
108 scope: params.scope.as_deref(),
109 code_challenge: params.code_challenge.as_deref(),
110 code_challenge_method: params.code_challenge_method.as_deref(),
111 resource: params.resource.as_deref(),
112 })
113 .await?;
114
115 let issuer = OAuthServerConfig::from_api_server_url(base.as_str()).issuer;
119
120 Ok(create_successful_response(
121 &headers,
122 &redirect_uri,
123 authorization_code.as_str(),
124 ¶ms,
125 &issuer,
126 ))
127}
128
129#[derive(Debug, Serialize)]
130pub struct WebAuthnCompleteResponse {
131 pub authorization_code: String,
132 pub state: String,
133 pub redirect_uri: String,
134 pub client_id: ClientId,
135}
136
137fn create_successful_response(
138 headers: &HeaderMap,
139 redirect_uri: &str,
140 authorization_code: &str,
141 params: &WebAuthnCompleteQuery,
142 issuer: &str,
143) -> Response {
144 let state = params.state.as_deref().filter(|s| !s.is_empty());
145
146 if is_browser_request(headers) {
147 let mut target = format!("{redirect_uri}?code={authorization_code}");
148
149 if let Some(client_id_val) = params.client_id.as_ref() {
150 target.push_str(&format!(
151 "&client_id={}",
152 urlencoding::encode(client_id_val.as_str())
153 ));
154 }
155
156 if let Some(state_val) = state {
157 target.push_str(&format!("&state={}", urlencoding::encode(state_val)));
158 }
159 target.push_str(&format!("&iss={}", urlencoding::encode(issuer)));
160 Redirect::to(&target).into_response()
161 } else {
162 let response_data = WebAuthnCompleteResponse {
163 authorization_code: authorization_code.to_owned(),
164 state: state.unwrap_or("").to_owned(),
165 redirect_uri: redirect_uri.to_owned(),
166 client_id: params
167 .client_id
168 .clone()
169 .unwrap_or_else(|| ClientId::new("")),
170 };
171
172 Json(response_data).into_response()
173 }
174}