Skip to main content

openauth_passkey/
webauthn.rs

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}