1use openauth_core::error::OpenAuthError;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use url::Url;
5use uuid::Uuid;
6use webauthn_rs::prelude::{
7 CreationChallengeResponse, Credential, DiscoverableAuthentication, DiscoverableKey,
8 PasskeyAuthentication, PasskeyRegistration, PublicKeyCredential, RegisterPublicKeyCredential,
9 RequestChallengeResponse, Webauthn, WebauthnBuilder,
10};
11
12use crate::options::{PasskeyRegistrationUser, RegistrationWebAuthnOptions};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct WebAuthnConfig {
16 pub rp_id: String,
17 pub rp_name: String,
18 pub origins: Vec<String>,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct PasskeyRegistrationStart {
23 pub options: Value,
24 pub state: Value,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct PasskeyAuthenticationStart {
29 pub options: Value,
30 pub state: Value,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct VerifiedPasskeyCredential {
35 pub credential_id: String,
36 pub public_key: String,
37 pub counter: u32,
38 pub device_type: String,
39 pub backed_up: bool,
40 pub transports: Option<String>,
41 pub aaguid: Option<String>,
42 pub credential: Value,
43}
44
45#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46pub struct VerifiedAuthentication {
47 pub credential: Option<Value>,
48 pub new_counter: u32,
49}
50
51pub trait PasskeyWebAuthnBackend: Send + Sync {
52 fn start_registration(
53 &self,
54 config: WebAuthnConfig,
55 user: &PasskeyRegistrationUser,
56 exclude_credentials: Vec<Value>,
57 options: RegistrationWebAuthnOptions,
58 ) -> Result<PasskeyRegistrationStart, OpenAuthError>;
59
60 fn finish_registration(
61 &self,
62 config: WebAuthnConfig,
63 response: Value,
64 state: Value,
65 ) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
66 let _ = (config, response, state);
67 Err(OpenAuthError::Api(
68 "passkey registration verification is not implemented".to_owned(),
69 ))
70 }
71
72 fn start_authentication(
73 &self,
74 config: WebAuthnConfig,
75 credentials: Vec<Value>,
76 extensions: Option<Value>,
77 ) -> Result<PasskeyAuthenticationStart, OpenAuthError>;
78
79 fn finish_authentication(
80 &self,
81 config: WebAuthnConfig,
82 response: Value,
83 state: Value,
84 credential: Option<Value>,
85 ) -> Result<VerifiedAuthentication, OpenAuthError> {
86 let _ = (config, response, state, credential);
87 Err(OpenAuthError::Api(
88 "passkey authentication verification is not implemented".to_owned(),
89 ))
90 }
91}
92
93#[derive(Debug, Clone, Copy)]
94pub struct RealPasskeyWebAuthnBackend;
95
96impl PasskeyWebAuthnBackend for RealPasskeyWebAuthnBackend {
97 fn start_registration(
98 &self,
99 config: WebAuthnConfig,
100 user: &PasskeyRegistrationUser,
101 exclude_credentials: Vec<Value>,
102 request_options: RegistrationWebAuthnOptions,
103 ) -> Result<PasskeyRegistrationStart, OpenAuthError> {
104 let webauthn = webauthn(&config)?;
105 let exclude = exclude_credentials
106 .into_iter()
107 .map(|value| {
108 serde_json::from_value::<Credential>(value).map(|credential| credential.cred_id)
109 })
110 .collect::<Result<Vec<_>, _>>()
111 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
112 let user_id = stable_user_uuid(&user.id);
113 let display_name = user.display_name.as_deref().unwrap_or(&user.name);
114 let (options, state) = webauthn
115 .start_passkey_registration(user_id, &user.name, display_name, Some(exclude))
116 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
117 let mut options = option_value(options)?;
118 apply_registration_request_options(&mut options, &request_options);
119 Ok(PasskeyRegistrationStart {
120 options,
121 state: serde_json::to_value(state).map_err(json_error)?,
122 })
123 }
124
125 fn finish_registration(
126 &self,
127 config: WebAuthnConfig,
128 response: Value,
129 state: Value,
130 ) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
131 let webauthn = webauthn(&config)?;
132 let response = serde_json::from_value::<RegisterPublicKeyCredential>(response)
133 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
134 let state = serde_json::from_value::<PasskeyRegistration>(state).map_err(json_error)?;
135 let passkey = webauthn
136 .finish_passkey_registration(&response, &state)
137 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
138 credential_output(passkey)
139 }
140
141 fn start_authentication(
142 &self,
143 config: WebAuthnConfig,
144 credentials: Vec<Value>,
145 extensions: Option<Value>,
146 ) -> Result<PasskeyAuthenticationStart, OpenAuthError> {
147 let webauthn = webauthn(&config)?;
148 if credentials.is_empty() {
149 let (options, state) = webauthn
150 .start_discoverable_authentication()
151 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
152 let mut options = auth_option_value(options)?;
153 apply_authentication_request_options(&mut options, extensions);
154 return Ok(PasskeyAuthenticationStart {
155 options,
156 state: serde_json::to_value(StoredAuthenticationState::Discoverable(state))
157 .map_err(json_error)?,
158 });
159 }
160 let passkeys = credentials
161 .into_iter()
162 .map(credential_value_to_passkey)
163 .collect::<Result<Vec<_>, _>>()?;
164 let (options, state) = webauthn
165 .start_passkey_authentication(&passkeys)
166 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
167 let mut options = auth_option_value(options)?;
168 apply_authentication_request_options(&mut options, extensions);
169 Ok(PasskeyAuthenticationStart {
170 options,
171 state: serde_json::to_value(StoredAuthenticationState::Passkey(state))
172 .map_err(json_error)?,
173 })
174 }
175
176 fn finish_authentication(
177 &self,
178 config: WebAuthnConfig,
179 response: Value,
180 state: Value,
181 credential: Option<Value>,
182 ) -> Result<VerifiedAuthentication, OpenAuthError> {
183 let webauthn = webauthn(&config)?;
184 let response = serde_json::from_value::<PublicKeyCredential>(response)
185 .map_err(|error| OpenAuthError::Api(error.to_string()))?;
186 let state =
187 serde_json::from_value::<StoredAuthenticationState>(state).map_err(json_error)?;
188 let credential = credential.map(credential_value_to_passkey).transpose()?;
189 let result = match state {
190 StoredAuthenticationState::Passkey(state) => webauthn
191 .finish_passkey_authentication(&response, &state)
192 .map_err(|error| OpenAuthError::Api(error.to_string()))?,
193 StoredAuthenticationState::Discoverable(state) => {
194 let Some(credential) = credential.as_ref() else {
195 return Err(OpenAuthError::Api(
196 "passkey credential is required".to_owned(),
197 ));
198 };
199 let discoverable = DiscoverableKey::from(credential);
200 webauthn
201 .finish_discoverable_authentication(&response, state, &[discoverable])
202 .map_err(|error| OpenAuthError::Api(error.to_string()))?
203 }
204 };
205 let updated_credential = credential.and_then(|mut passkey| {
206 passkey
207 .update_credential(&result)
208 .and_then(|changed| changed.then_some(passkey))
209 });
210 Ok(VerifiedAuthentication {
211 credential: updated_credential
212 .map(|passkey| serde_json::to_value(passkey).map_err(json_error))
213 .transpose()?,
214 new_counter: result.counter(),
215 })
216 }
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
220enum StoredAuthenticationState {
221 Passkey(PasskeyAuthentication),
222 Discoverable(DiscoverableAuthentication),
223}
224
225fn webauthn(config: &WebAuthnConfig) -> Result<Webauthn, OpenAuthError> {
226 let primary_origin = config
227 .origins
228 .first()
229 .ok_or_else(|| OpenAuthError::InvalidConfig("passkey origin is required".to_owned()))?;
230 let primary =
231 Url::parse(primary_origin).map_err(|error| OpenAuthError::Api(error.to_string()))?;
232 let mut builder = WebauthnBuilder::new(&config.rp_id, &primary)
233 .map_err(|error| OpenAuthError::Api(error.to_string()))?
234 .rp_name(&config.rp_name)
235 .allow_any_port(true);
236 for origin in config.origins.iter().skip(1) {
237 let origin = Url::parse(origin).map_err(|error| OpenAuthError::Api(error.to_string()))?;
238 builder = builder.append_allowed_origin(&origin);
239 }
240 builder
241 .build()
242 .map_err(|error| OpenAuthError::Api(error.to_string()))
243}
244
245fn option_value(options: CreationChallengeResponse) -> Result<Value, OpenAuthError> {
246 serde_json::to_value(options)
247 .map(|mut value| value.pointer_mut("/publicKey").cloned().unwrap_or(value))
248 .map_err(json_error)
249}
250
251fn auth_option_value(options: RequestChallengeResponse) -> Result<Value, OpenAuthError> {
252 serde_json::to_value(options)
253 .map(|mut value| value.pointer_mut("/publicKey").cloned().unwrap_or(value))
254 .map_err(json_error)
255}
256
257fn apply_registration_request_options(
258 options: &mut Value,
259 request_options: &RegistrationWebAuthnOptions,
260) {
261 options["authenticatorSelection"] = request_options.authenticator_selection.to_json();
262 if let Some(extensions) = &request_options.extensions {
263 options["extensions"] = extensions.clone();
264 }
265}
266
267fn apply_authentication_request_options(options: &mut Value, extensions: Option<Value>) {
268 options["userVerification"] = Value::String("preferred".to_owned());
269 if let Some(extensions) = extensions {
270 options["extensions"] = extensions;
271 }
272}
273
274fn credential_value_to_passkey(
275 value: Value,
276) -> Result<webauthn_rs::prelude::Passkey, OpenAuthError> {
277 serde_json::from_value::<webauthn_rs::prelude::Passkey>(value).map_err(json_error)
278}
279
280fn credential_output(
281 passkey: webauthn_rs::prelude::Passkey,
282) -> Result<VerifiedPasskeyCredential, OpenAuthError> {
283 let credential = Credential::from(passkey.clone());
284 let credential_id = serde_json::to_value(&credential.cred_id)
285 .and_then(serde_json::from_value::<String>)
286 .unwrap_or_else(|_| format!("{:?}", credential.cred_id));
287 let public_key = serde_json::to_string(&credential.cred).map_err(json_error)?;
288 let transports = credential.transports.as_ref().map(|values| {
289 values
290 .iter()
291 .map(|value| format!("{value:?}"))
292 .collect::<Vec<_>>()
293 .join(",")
294 });
295 Ok(VerifiedPasskeyCredential {
296 credential_id,
297 public_key,
298 counter: credential.counter,
299 device_type: if credential.backup_eligible {
300 "multiDevice".to_owned()
301 } else {
302 "singleDevice".to_owned()
303 },
304 backed_up: credential.backup_state,
305 transports,
306 aaguid: None,
307 credential: serde_json::to_value(passkey).map_err(json_error)?,
308 })
309}
310
311fn stable_user_uuid(user_id: &str) -> Uuid {
312 Uuid::new_v5(&Uuid::NAMESPACE_URL, user_id.as_bytes())
313}
314
315fn json_error(error: serde_json::Error) -> OpenAuthError {
316 OpenAuthError::Api(error.to_string())
317}