1use super::{
3 PasskeyHandlerOutcome, PasskeyPlugin, delete_passkey_core, list_user_passkeys_core,
4 update_passkey_core,
5};
6use alibi_core::endpoint::{
7 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
8};
9use alibi_core::{AuthContext, AuthResult, AuthSchema, HttpMethod};
10use serde_json::Value;
11
12pub(super) fn definitions() -> Vec<EndpointDefinition> {
13 vec![
14 crate::endpoint::definition(
15 "verifyPasskeyAuthentication",
16 "passkeyVerifyAuthentication",
17 Some("/passkey/verify-authentication"),
18 HttpMethod::Post,
19 ),
20 crate::endpoint::definition(
21 "verifyPasskeyRegistration",
22 "passkeyVerifyRegistration",
23 Some("/passkey/verify-registration"),
24 HttpMethod::Post,
25 ),
26 crate::endpoint::definition(
27 "generatePasskeyAuthenticationOptions",
28 "passkeyGenerateAuthenticateOptions",
29 Some("/passkey/generate-authenticate-options"),
30 HttpMethod::Get,
31 ),
32 crate::endpoint::definition(
33 "generatePasskeyRegistrationOptions",
34 "generatePasskeyRegistrationOptions",
35 Some("/passkey/generate-register-options"),
36 HttpMethod::Get,
37 ),
38 crate::endpoint::definition(
39 "updatePasskey",
40 "updatePasskey",
41 Some("/passkey/update-passkey"),
42 HttpMethod::Post,
43 ),
44 crate::endpoint::definition(
45 "listPasskeys",
46 "listPasskeys",
47 Some("/passkey/list-user-passkeys"),
48 HttpMethod::Get,
49 ),
50 crate::endpoint::definition(
51 "deletePasskey",
52 "deletePasskey",
53 Some("/passkey/delete-passkey"),
54 HttpMethod::Post,
55 ),
56 ]
57}
58
59#[derive(Debug, serde::Deserialize)]
61pub struct PasskeyDeleted {
62 pub status: bool,
63}
64
65#[derive(Debug, serde::Deserialize)]
67pub struct PasskeyUpdated {
68 pub passkey: Value,
69}
70
71#[derive(Debug, serde::Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct PasskeyRegistrationInput {
75 pub response: alibi_core::utils::json::JsValue,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub name: Option<String>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub create_session: Option<bool>,
80}
81
82fn registration_input(call: &EndpointCall) -> AuthResult<alibi_core::utils::json::JsValue> {
83 use crate::authentication_helpers::{JsonField, JsonFieldKind, field_issue, json_type};
84 use alibi_core::utils::json::JsValue;
85 let Some(object) = call.body().and_then(JsValue::as_object) else {
86 return Err(crate::endpoint::validation(format!(
87 "[body] Invalid input: expected object, received {}",
88 json_type(call.body())
89 )));
90 };
91 let mut issues = Vec::new();
92 if !object.contains_key("response") {
93 issues.push(
94 "[body.response] Invalid input: expected nonoptional, received undefined".to_owned(),
95 );
96 }
97 for field in [
98 JsonField::string("name", false),
99 JsonField {
100 name: "createSession",
101 kind: JsonFieldKind::Boolean,
102 required: false,
103 },
104 ] {
105 if let Some(value) = object.get(field.name)
106 && let Some(issue) = field_issue(field.kind, Some(value), json_type(Some(value)))
107 {
108 issues.push(format!("[body.{}] {issue}", field.name));
109 }
110 }
111 if !issues.is_empty() {
112 return Err(crate::endpoint::validation(issues.join("; ")));
113 }
114 Ok(JsValue::Object(
115 object
116 .iter()
117 .filter(|(name, _)| matches!(name.as_str(), "response" | "name" | "createSession"))
118 .map(|(name, value)| {
119 let value = if name == "name" {
120 JsValue::String(
121 super::registration::trim_name(value.as_str().unwrap_or_default())
122 .to_owned(),
123 )
124 } else {
125 value.clone()
126 };
127 (name.clone(), value)
128 })
129 .collect(),
130 ))
131}
132
133#[derive(Debug, serde::Deserialize)]
135pub struct PasskeyAuthenticated {
136 pub session: Value,
137 pub user: Value,
138}
139
140fn verification_response(output: alibi_core::AuthResponse) -> AuthResult<EndpointResponse> {
141 let body =
142 alibi_core::utils::json::from_slice::<alibi_core::utils::json::JsValue>(&output.body).ok();
143 let mut response = if output.status >= 400 {
144 let response = EndpointResponse::error(crate::endpoint::error_response(output.clone()));
145 if let Some(body) = body {
146 response.with_error_body(body)
147 } else {
148 response.without_error_body()
149 }
150 } else {
151 EndpointResponse::json(&body)?
152 };
153 response.merge_headers(output.headers);
154 Ok(response)
155}
156
157pub(super) fn validate(call: &EndpointCall) -> AuthResult<EndpointInput> {
158 Ok(EndpointInput {
159 body: if call.operation_id() == "passkeyVerifyAuthentication" {
160 Some(crate::endpoint::validate_fields(
161 call.body(),
162 "body",
163 &[crate::authentication_helpers::JsonField {
164 name: "response",
165 kind: crate::authentication_helpers::JsonFieldKind::Record,
166 required: true,
167 }],
168 )?)
169 } else if call.operation_id() == "passkeyVerifyRegistration" {
170 Some(registration_input(call)?)
171 } else if call.operation_id() == "deletePasskey" {
172 Some(crate::endpoint::validate_fields(
173 call.body(),
174 "body",
175 &[crate::authentication_helpers::JsonField::string("id", true)],
176 )?)
177 } else if call.operation_id() == "updatePasskey" {
178 let mut body = crate::endpoint::validate_fields(
179 call.body(),
180 "body",
181 &[
182 crate::authentication_helpers::JsonField::string("id", true),
183 crate::authentication_helpers::JsonField::string("name", true),
184 ],
185 )?;
186 let name = super::registration::trim_name(
187 body.get("name")
188 .and_then(alibi_core::utils::json::JsValue::as_str)
189 .unwrap_or_default(),
190 );
191 if name.is_empty() {
192 return Err(crate::endpoint::validation(
193 "[body.name] Too small: expected string to have >=1 characters",
194 ));
195 }
196 let name = name.to_owned();
197 if let alibi_core::utils::json::JsValue::Object(object) = &mut body {
198 _ = object.insert(
199 "name".into(),
200 alibi_core::utils::json::JsValue::String(name),
201 );
202 }
203 Some(body)
204 } else {
205 call.body().cloned()
206 },
207 query: if call.operation_id() == "generatePasskeyRegistrationOptions" {
208 call.query()
209 .map(|query| {
210 crate::endpoint::validate_fields(
211 Some(query),
212 "query",
213 &[
214 crate::authentication_helpers::JsonField {
215 name: "authenticatorAttachment",
216 kind: crate::authentication_helpers::JsonFieldKind::OneOf(&[
217 "platform",
218 "cross-platform",
219 ]),
220 required: false,
221 },
222 crate::authentication_helpers::JsonField::string("name", false),
223 crate::authentication_helpers::JsonField::string("context", false),
224 ],
225 )
226 })
227 .transpose()?
228 } else {
229 call.query().cloned()
230 },
231 })
232}
233
234impl PasskeyPlugin {
235 #[must_use]
237 pub fn verify_passkey_authentication_endpoint(
238 response: alibi_core::utils::json::JsValue,
239 ) -> ServerEndpoint<PasskeyAuthenticated> {
240 ServerEndpoint::new("passkey", "verifyPasskeyAuthentication").with_body_value(
241 alibi_core::utils::json::JsValue::Object(
242 [("response".into(), response)].into_iter().collect(),
243 ),
244 )
245 }
246
247 pub(super) async fn verify_authentication_endpoint<S: AuthSchema>(
248 &self,
249 call: &EndpointCall,
250 ctx: &AuthContext<S>,
251 ) -> AuthResult<EndpointResponse> {
252 verification_response(
253 self.verify_authentication(&call.body_as()?, call, ctx)
254 .await?,
255 )
256 }
257
258 #[must_use]
260 pub fn verify_passkey_registration_endpoint(
261 input: PasskeyRegistrationInput,
262 ) -> ServerEndpoint<Value> {
263 use alibi_core::utils::json::JsValue;
264 let mut body = [("response".into(), input.response)]
265 .into_iter()
266 .collect::<indexmap::IndexMap<_, _>>();
267 if let Some(name) = input.name {
268 _ = body.insert("name".into(), JsValue::String(name));
269 }
270 if let Some(create_session) = input.create_session {
271 _ = body.insert("createSession".into(), JsValue::Bool(create_session));
272 }
273 ServerEndpoint::new("passkey", "verifyPasskeyRegistration")
274 .with_body_value(JsValue::Object(body))
275 }
276
277 pub(super) async fn verify_registration_endpoint<S: AuthSchema>(
278 &self,
279 call: &EndpointCall,
280 ctx: &AuthContext<S>,
281 ) -> AuthResult<EndpointResponse> {
282 let output = self
283 .verify_registration(&call.body_as()?, call, ctx)
284 .await
285 .map_err(crate::helpers::unauthorized_if_unauthenticated)?;
286 verification_response(output)
287 }
288
289 #[must_use]
291 pub fn generate_passkey_authentication_options_endpoint() -> ServerEndpoint<Value> {
292 ServerEndpoint::new("passkey", "generatePasskeyAuthenticationOptions")
293 }
294
295 pub(super) async fn authentication_options_endpoint<S: AuthSchema>(
296 &self,
297 call: &EndpointCall,
298 ctx: &AuthContext<S>,
299 ) -> AuthResult<EndpointResponse> {
300 let output = self.generate_authentication_options(call, ctx).await?;
301 let mut response = EndpointResponse::json(&alibi_core::utils::json::from_slice::<
302 alibi_core::utils::json::JsValue,
303 >(&output.body)?)?;
304 response.merge_headers(output.headers);
305 Ok(response)
306 }
307
308 #[must_use]
310 pub fn generate_passkey_registration_options_endpoint() -> ServerEndpoint<Value> {
311 ServerEndpoint::new("passkey", "generatePasskeyRegistrationOptions")
312 }
313
314 pub(super) async fn registration_options_endpoint<S: AuthSchema>(
315 &self,
316 call: &EndpointCall,
317 ctx: &AuthContext<S>,
318 ) -> AuthResult<EndpointResponse> {
319 let query = call
320 .query()
321 .and_then(alibi_core::utils::json::JsValue::as_object)
322 .map(|object| {
323 object
324 .iter()
325 .filter_map(|(name, value)| {
326 value.as_str().map(|value| (name.clone(), value.to_owned()))
327 })
328 .collect()
329 })
330 .unwrap_or_default();
331 let output = self
332 .generate_register_options(call, &query, ctx)
333 .await
334 .map_err(crate::helpers::unauthorized_if_unauthenticated)?;
335 if output.status >= 400 {
336 let headers = output.headers.clone();
337 let mut response = EndpointResponse::error(crate::endpoint::error_response(output));
338 response.merge_headers(headers);
339 Ok(response)
340 } else {
341 let mut response = EndpointResponse::json(&alibi_core::utils::json::from_slice::<
342 alibi_core::utils::json::JsValue,
343 >(&output.body)?)?;
344 response.merge_headers(output.headers);
345 Ok(response)
346 }
347 }
348
349 #[must_use]
351 pub fn update_passkey_endpoint(
352 id: impl Into<String>,
353 name: impl Into<String>,
354 ) -> ServerEndpoint<PasskeyUpdated> {
355 ServerEndpoint::new("passkey", "updatePasskey").with_body_value(
356 alibi_core::utils::json::JsValue::Object(
357 [
358 (
359 "id".into(),
360 alibi_core::utils::json::JsValue::String(id.into()),
361 ),
362 (
363 "name".into(),
364 alibi_core::utils::json::JsValue::String(name.into()),
365 ),
366 ]
367 .into_iter()
368 .collect(),
369 ),
370 )
371 }
372
373 pub(super) async fn update_endpoint<S: AuthSchema>(
374 &self,
375 call: &EndpointCall,
376 ctx: &AuthContext<S>,
377 ) -> AuthResult<EndpointResponse> {
378 let (user, _) = crate::helpers::ordinary_session(call, ctx).await?;
379 match update_passkey_core(&call.body_as()?, &user, ctx).await? {
380 PasskeyHandlerOutcome::Success(value) => EndpointResponse::json(&value),
381 PasskeyHandlerOutcome::Response(output) => {
382 let headers = output.headers.clone();
383 let mut response = EndpointResponse::error(crate::endpoint::error_response(output));
384 response.merge_headers(headers);
385 Ok(response)
386 }
387 }
388 }
389
390 #[must_use]
392 pub fn delete_passkey_endpoint(id: impl Into<String>) -> ServerEndpoint<PasskeyDeleted> {
393 ServerEndpoint::new("passkey", "deletePasskey").with_body_value(
394 alibi_core::utils::json::JsValue::Object(
395 [(
396 "id".into(),
397 alibi_core::utils::json::JsValue::String(id.into()),
398 )]
399 .into_iter()
400 .collect(),
401 ),
402 )
403 }
404
405 pub(super) async fn delete_endpoint<S: AuthSchema>(
406 &self,
407 call: &EndpointCall,
408 ctx: &AuthContext<S>,
409 ) -> AuthResult<EndpointResponse> {
410 let (user, _) = crate::helpers::ordinary_session(call, ctx).await?;
411 match delete_passkey_core(&call.body_as()?, &user, ctx).await? {
412 PasskeyHandlerOutcome::Success(value) => EndpointResponse::json(&value),
413 PasskeyHandlerOutcome::Response(output) => {
414 let mut response = EndpointResponse::error(alibi_core::AuthError::Api {
415 status: output.status,
416 code: None,
417 message: String::new(),
418 })
419 .without_error_body();
420 response.merge_headers(output.headers);
421 Ok(response)
422 }
423 }
424 }
425
426 #[must_use]
428 pub fn list_passkeys_endpoint() -> ServerEndpoint<Vec<Value>> {
429 ServerEndpoint::new("passkey", "listPasskeys")
430 }
431
432 pub(super) async fn list_endpoint<S: AuthSchema>(
433 &self,
434 call: &EndpointCall,
435 ctx: &AuthContext<S>,
436 ) -> AuthResult<EndpointResponse> {
437 let (user, _) = crate::helpers::ordinary_session(call, ctx).await?;
438 EndpointResponse::json(&list_user_passkeys_core(&user, ctx).await?)
439 }
440}