Skip to main content

rustlavel_webauthn/
authentication.rs

1//! Authentication: the ceremony that proves the private key is still there.
2//!
3//! The server issues a challenge; the authenticator signs
4//! `authenticatorData || SHA-256(clientDataJSON)` with the key registered
5//! earlier. That message is the whole design: the client data carries the
6//! origin the browser was actually at, and the authenticator refuses to sign
7//! for the wrong one — which is why a passkey cannot be phished the way a
8//! password or a one-time code can.
9//!
10//! Everything [`RelyingParty::finish_authentication`] checks is a way that
11//! guarantee gets lost if the check is missing.
12
13use crate::ceremony::{
14    AuthenticatorData, AuthenticatorFlags, ClientData, RelyingParty, UserVerification,
15};
16use crate::challenge::{Ceremony, Challenge, ChallengeStore};
17use crate::credential::{Credential, CredentialStore};
18use crate::{Error, Json, Result};
19use rustlavel_auth::{base64, unix_now};
20
21/// What the server sends to `navigator.credentials.get()`.
22#[derive(Debug, Clone)]
23pub struct PublicKeyCredentialRequestOptions {
24    rp_id: String,
25    challenge: Vec<u8>,
26    timeout_ms: u64,
27    user_verification: UserVerification,
28    allow: Vec<Vec<u8>>,
29}
30
31impl PublicKeyCredentialRequestOptions {
32    /// Name the credentials that may answer.
33    ///
34    /// An empty list is the passkey case: the authenticator offers whichever
35    /// discoverable credential it holds for this site, and the user picks. A
36    /// non-empty one is needed for credentials that are not discoverable,
37    /// which cannot be found without being named.
38    pub fn allowing<I: IntoIterator<Item = Vec<u8>>>(
39        mut self,
40        credential_ids: I,
41    ) -> PublicKeyCredentialRequestOptions {
42        self.allow = credential_ids.into_iter().collect();
43        self
44    }
45
46    pub fn challenge(&self) -> &[u8] {
47        &self.challenge
48    }
49
50    pub fn allow_credentials(&self) -> &[Vec<u8>] {
51        &self.allow
52    }
53
54    /// The `publicKey` object, ready to be serialised into a response body.
55    pub fn json(&self) -> Json {
56        let allow: Vec<Json> = self
57            .allow
58            .iter()
59            .map(|id| {
60                Json::object([
61                    ("type", Json::String("public-key".into())),
62                    ("id", Json::String(base64::encode_url(id))),
63                ])
64            })
65            .collect();
66
67        Json::object([
68            ("rpId", Json::String(self.rp_id.clone())),
69            ("challenge", Json::String(base64::encode_url(&self.challenge))),
70            ("timeout", Json::Number(self.timeout_ms as f64)),
71            ("userVerification", Json::String(self.user_verification.as_str().into())),
72            ("allowCredentials", Json::Array(allow)),
73        ])
74    }
75}
76
77/// Compact JSON, so `format!("{options}")` is a response body.
78impl std::fmt::Display for PublicKeyCredentialRequestOptions {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}", self.json())
81    }
82}
83
84/// What the browser posts back after `navigator.credentials.get()`.
85#[derive(Debug, Clone)]
86pub struct AuthenticationResponse {
87    id: Vec<u8>,
88    client_data_json: Vec<u8>,
89    authenticator_data: Vec<u8>,
90    signature: Vec<u8>,
91    user_handle: Option<Vec<u8>>,
92}
93
94impl AuthenticationResponse {
95    pub fn new(
96        id: impl Into<Vec<u8>>,
97        client_data_json: impl Into<Vec<u8>>,
98        authenticator_data: impl Into<Vec<u8>>,
99        signature: impl Into<Vec<u8>>,
100    ) -> AuthenticationResponse {
101        AuthenticationResponse {
102            id: id.into(),
103            client_data_json: client_data_json.into(),
104            authenticator_data: authenticator_data.into(),
105            signature: signature.into(),
106            user_handle: None,
107        }
108    }
109
110    /// The user handle a discoverable credential hands back.
111    pub fn with_user_handle(mut self, user_handle: impl Into<Vec<u8>>) -> AuthenticationResponse {
112        self.user_handle = Some(user_handle.into());
113        self
114    }
115
116    /// Read the shape a browser produces from `PublicKeyCredential.toJSON()`.
117    pub fn from_json(value: &Json) -> Result<AuthenticationResponse> {
118        let response = AuthenticationResponse {
119            id: crate::ceremony::base64_field(value, "rawId")?,
120            client_data_json: crate::ceremony::base64_field(value, "response.clientDataJSON")?,
121            authenticator_data: crate::ceremony::base64_field(
122                value,
123                "response.authenticatorData",
124            )?,
125            signature: crate::ceremony::base64_field(value, "response.signature")?,
126            user_handle: None,
127        };
128
129        // Optional, and null for a credential that is not discoverable.
130        match value.get("response.userHandle") {
131            Some(handle) if !handle.is_null() => {
132                Ok(response.with_user_handle(crate::ceremony::base64_field(
133                    value,
134                    "response.userHandle",
135                )?))
136            }
137            _ => Ok(response),
138        }
139    }
140
141    /// The same, from the request body directly.
142    pub fn parse(body: &str) -> Result<AuthenticationResponse> {
143        AuthenticationResponse::from_json(&Json::parse(body)?)
144    }
145
146    pub fn id(&self) -> &[u8] {
147        &self.id
148    }
149
150    pub fn client_data_json(&self) -> &[u8] {
151        &self.client_data_json
152    }
153
154    pub fn authenticator_data(&self) -> &[u8] {
155        &self.authenticator_data
156    }
157
158    pub fn signature(&self) -> &[u8] {
159        &self.signature
160    }
161
162    pub fn user_handle(&self) -> Option<&[u8]> {
163        self.user_handle.as_deref()
164    }
165}
166
167/// A completed authentication: who signed in, and what the device reported.
168#[derive(Debug, Clone)]
169pub struct Authentication {
170    credential_id: Vec<u8>,
171    user_handle: Vec<u8>,
172    sign_count: u32,
173    flags: AuthenticatorFlags,
174}
175
176impl Authentication {
177    pub fn credential_id(&self) -> &[u8] {
178        &self.credential_id
179    }
180
181    /// The user handle from the *stored* credential, never the one the client
182    /// sent. The client's copy is checked against this, not trusted over it.
183    pub fn user_handle(&self) -> &[u8] {
184        &self.user_handle
185    }
186
187    pub fn sign_count(&self) -> u32 {
188        self.sign_count
189    }
190
191    pub fn flags(&self) -> AuthenticatorFlags {
192        self.flags
193    }
194
195    pub fn user_verified(&self) -> bool {
196        self.flags.user_verified()
197    }
198}
199
200impl RelyingParty {
201    /// Build request options around a challenge you have already issued.
202    pub fn request_options(&self, challenge: &Challenge) -> PublicKeyCredentialRequestOptions {
203        PublicKeyCredentialRequestOptions {
204            rp_id: self.id().to_string(),
205            challenge: challenge.bytes().to_vec(),
206            timeout_ms: self.timeout_ms(),
207            user_verification: self.user_verification(),
208            allow: Vec::new(),
209        }
210    }
211
212    /// Issue and store a challenge for a login that names no user.
213    ///
214    /// The passkey case: `allowCredentials` is empty, the authenticator offers
215    /// what it has, and the account is discovered from the user handle it
216    /// returns. Nothing is revealed about which accounts exist, which is the
217    /// other reason to prefer it.
218    pub async fn start_authentication(
219        &self,
220        challenges: &dyn ChallengeStore,
221    ) -> Result<PublicKeyCredentialRequestOptions> {
222        let challenge = Challenge::issue(Ceremony::Authentication);
223        let options = self.request_options(&challenge);
224        challenges.store(challenge).await?;
225
226        Ok(options)
227    }
228
229    /// Issue and store a challenge for one known user.
230    ///
231    /// Use this when the user has already typed a username. The challenge is
232    /// bound to their handle, so an assertion from somebody else's credential
233    /// cannot finish it, and `allowCredentials` lists what they registered so
234    /// a non-discoverable credential can still answer.
235    pub async fn start_authentication_for(
236        &self,
237        user_handle: &[u8],
238        challenges: &dyn ChallengeStore,
239        credentials: &dyn CredentialStore,
240    ) -> Result<PublicKeyCredentialRequestOptions> {
241        let challenge =
242            Challenge::issue(Ceremony::Authentication).bound_to(user_handle.to_vec());
243        let options = self.request_options(&challenge);
244        challenges.store(challenge).await?;
245
246        let theirs = credentials.find_for_user(user_handle).await?;
247        Ok(options.allowing(theirs.into_iter().map(|credential| credential.id().to_vec())))
248    }
249
250    /// Check an assertion, and advance the credential's sign counter.
251    ///
252    /// On success the credential's `sign_count` and `last_used_at` have been
253    /// written. On any error nothing was written, and the challenge has been
254    /// spent either way.
255    pub async fn finish_authentication(
256        &self,
257        response: &AuthenticationResponse,
258        challenges: &dyn ChallengeStore,
259        credentials: &dyn CredentialStore,
260    ) -> Result<Authentication> {
261        let credential = credentials.find(response.id()).await?.ok_or_else(|| {
262            Error::msg(
263                "no credential is registered under that id, so there is no key to check this \
264                 assertion against",
265            )
266        })?;
267
268        let client_data = ClientData::parse(response.client_data_json())?;
269
270        // A registration response is not evidence of a login.
271        client_data.expect_kind(Ceremony::Authentication.client_data_type())?;
272
273        // Taking it out is what makes it single-use. Without this every
274        // captured assertion is a reusable credential.
275        let key = base64::encode_url(client_data.challenge());
276        let challenge = challenges.take(&key).await?.ok_or_else(|| {
277            Error::msg(
278                "that challenge is not one this server issued, or it has already been answered. \
279                 A challenge that can be answered twice is a password.",
280            )
281        })?;
282        challenge.accept(Ceremony::Authentication, unix_now())?;
283
284        if let Some(bound) = challenge.user_handle()
285            && bound != credential.user_handle()
286        {
287            return Err(Error::msg(
288                "that challenge was issued for a different user. An assertion from one account \
289                 must not finish another account's login.",
290            ));
291        }
292
293        // The check phishing has to get past.
294        client_data.expect_origin(self)?;
295
296        let auth_data = AuthenticatorData::parse(response.authenticator_data())?;
297        auth_data.expect_rp_id(self)?;
298        auth_data.expect_user_present()?;
299        auth_data.expect_user_verification(self.user_verification())?;
300
301        // A discoverable credential says whose it is. The stored row already
302        // knows, so this is a check and not a lookup: believing the client
303        // here would let an assertion name any account it liked.
304        if let Some(claimed) = response.user_handle()
305            && claimed != credential.user_handle()
306        {
307            return Err(Error::msg(
308                "the assertion claims a user handle that does not belong to that credential",
309            ));
310        }
311
312        // Everything above is bookkeeping; this is the proof.
313        let mut message = response.authenticator_data().to_vec();
314        message.extend_from_slice(&crate::ceremony::sha256(response.client_data_json()));
315
316        if !credential.key().verify(&message, response.signature()) {
317            return Err(Error::msg(
318                "the signature does not verify under the registered public key. Either the \
319                 signer was not this credential, or the authenticator data or client data was \
320                 changed after it was signed.",
321            ));
322        }
323
324        check_sign_count(&credential, auth_data.sign_count())?;
325
326        let now = unix_now();
327        credentials.record_use(credential.id(), auth_data.sign_count(), now).await?;
328
329        Ok(Authentication {
330            credential_id: credential.id().to_vec(),
331            user_handle: credential.user_handle().to_vec(),
332            sign_count: auth_data.sign_count(),
333            flags: auth_data.flags(),
334        })
335    }
336}
337
338/// The one check that looks for a copy of the key rather than a forgery.
339///
340/// An authenticator that counts increments the counter on every assertion. If
341/// a value arrives that is not greater than the last one stored, two things
342/// have been signing with the same key — which is what a cloned authenticator
343/// looks like from here, and it is the only signal there is.
344///
345/// A counter of zero on **both** sides is not a failure and must not be
346/// treated as one: it means the authenticator does not keep a counter, which
347/// is normal and expected for a passkey synced across devices, since there is
348/// no single device left to count. Refusing that would refuse every modern
349/// credential.
350///
351/// A credential that counted before and reports zero now *is* refused. The
352/// clause above is about an authenticator that never counted, not about one
353/// that stopped.
354fn check_sign_count(credential: &Credential, reported: u32) -> Result<()> {
355    let stored = credential.sign_count();
356
357    if stored != 0 && reported <= stored {
358        return Err(Error::msg(format!(
359            "the sign counter went backwards: this credential last reported {stored} and now \
360             reports {reported}. Two authenticators are signing with the same private key, \
361             which means it has been copied off the device that was supposed to hold it."
362        )));
363    }
364
365    Ok(())
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::ceremony::fake::{ASSERTION_FLAGS, Authenticator, REGISTRATION_FLAGS};
372    use crate::ceremony::UserEntity;
373    use crate::challenge::MemoryChallengeStore;
374    use crate::credential::MemoryCredentialStore;
375    use crate::registration::RegistrationResponse;
376
377    const ORIGIN: &str = "https://example.com";
378
379    fn relying_party() -> RelyingParty {
380        RelyingParty::new("example.com", "Example")
381    }
382
383    fn user() -> UserEntity {
384        UserEntity::new(b"user-41".to_vec(), "ada", "Ada Lovelace")
385    }
386
387    /// A registered credential and a challenge waiting to be answered, with
388    /// the pieces left separate so a test can corrupt exactly one of them.
389    struct Session {
390        rp: RelyingParty,
391        device: Authenticator,
392        challenges: MemoryChallengeStore,
393        credentials: MemoryCredentialStore,
394        challenge: Vec<u8>,
395    }
396
397    impl Session {
398        async fn fresh() -> Session {
399            Session::with(relying_party(), Authenticator::new(1)).await
400        }
401
402        async fn with(rp: RelyingParty, device: Authenticator) -> Session {
403            let challenges = MemoryChallengeStore::new();
404            let credentials = MemoryCredentialStore::new();
405
406            // Register for real, so the key an assertion is checked against is
407            // the key the authenticator actually holds.
408            let options =
409                rp.start_registration(&user(), &challenges, &credentials).await.unwrap();
410            let auth_data = device.authenticator_data(rp.id(), REGISTRATION_FLAGS, 0);
411            let registration = RegistrationResponse::new(
412                device.credential_id.clone(),
413                device.client_data("webauthn.create", options.challenge(), ORIGIN),
414                device.attestation_object("none", 0, &auth_data),
415            );
416            rp.finish_registration(&user(), &registration, &challenges, &credentials)
417                .await
418                .unwrap();
419
420            let request = rp.start_authentication(&challenges).await.unwrap();
421            let challenge = request.challenge().to_vec();
422
423            Session { rp, device, challenges, credentials, challenge }
424        }
425
426        fn assert_with(
427            &self,
428            flags: u8,
429            rp_id: &str,
430            origin: &str,
431            sign_count: u32,
432            challenge: &[u8],
433        ) -> AuthenticationResponse {
434            let auth_data = self.device.authenticator_data(rp_id, flags, sign_count);
435            let client_data = self.device.client_data("webauthn.get", challenge, origin);
436            let signature = self.device.sign(&auth_data, &client_data);
437
438            AuthenticationResponse::new(
439                self.device.credential_id.clone(),
440                client_data,
441                auth_data,
442                signature,
443            )
444        }
445
446        fn healthy(&self) -> AuthenticationResponse {
447            self.assert_with(ASSERTION_FLAGS, self.rp.id(), ORIGIN, 0, &self.challenge)
448        }
449
450        async fn finish(&self, response: &AuthenticationResponse) -> Result<Authentication> {
451            self.rp.finish_authentication(response, &self.challenges, &self.credentials).await
452        }
453    }
454
455    #[tokio::test]
456    async fn an_assertion_round_trips_and_records_the_login() {
457        let session = Session::fresh().await;
458
459        let authentication = session.finish(&session.healthy()).await.unwrap();
460
461        assert_eq!(authentication.credential_id(), session.device.credential_id.as_slice());
462        assert_eq!(authentication.user_handle(), b"user-41");
463        assert!(authentication.user_verified());
464
465        let stored =
466            session.credentials.find(&session.device.credential_id).await.unwrap().unwrap();
467        assert!(stored.last_used_at().is_some(), "the login was not recorded");
468    }
469
470    #[tokio::test]
471    async fn a_signature_from_a_different_credential_is_refused() {
472        // A genuine assertion, correctly formed, signed by another device.
473        let session = Session::fresh().await;
474        let intruder =
475            Authenticator::new(2).with_credential_id(session.device.credential_id.clone());
476
477        let auth_data = intruder.authenticator_data("example.com", ASSERTION_FLAGS, 0);
478        let client_data = intruder.client_data("webauthn.get", &session.challenge, ORIGIN);
479        let response = AuthenticationResponse::new(
480            session.device.credential_id.clone(),
481            client_data.clone(),
482            auth_data.clone(),
483            intruder.sign(&auth_data, &client_data),
484        );
485
486        let error = session.finish(&response).await.unwrap_err().to_string();
487        assert!(error.contains("does not verify"), "got {error}");
488    }
489
490    #[tokio::test]
491    async fn a_tampered_authenticator_data_is_refused() {
492        // The counter, raised by one after signing. Nothing else changed.
493        let session = Session::fresh().await;
494        let mut response = session.healthy();
495        response.authenticator_data[36] = 1;
496
497        let error = session.finish(&response).await.unwrap_err().to_string();
498        assert!(error.contains("does not verify"), "got {error}");
499    }
500
501    #[tokio::test]
502    async fn a_tampered_client_data_json_is_refused() {
503        // The origin rewritten to the expected one after the browser signed
504        // for somewhere else: the check that catches it is the signature, not
505        // the string comparison.
506        let session = Session::fresh().await;
507        let auth_data = session.device.authenticator_data("example.com", ASSERTION_FLAGS, 0);
508        let signed =
509            session.device.client_data("webauthn.get", &session.challenge, "https://evil.test");
510        let signature = session.device.sign(&auth_data, &signed);
511        let presented =
512            session.device.client_data("webauthn.get", &session.challenge, ORIGIN);
513
514        let response = AuthenticationResponse::new(
515            session.device.credential_id.clone(),
516            presented,
517            auth_data,
518            signature,
519        );
520
521        let error = session.finish(&response).await.unwrap_err().to_string();
522        assert!(error.contains("does not verify"), "got {error}");
523    }
524
525    #[tokio::test]
526    async fn a_regressed_sign_counter_is_refused_as_a_cloned_authenticator() {
527        let session = Session::fresh().await;
528
529        // Get the stored counter up to five with a genuine assertion.
530        let response =
531            session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 5, &session.challenge);
532        session.finish(&response).await.unwrap();
533
534        let stored =
535            session.credentials.find(&session.device.credential_id).await.unwrap().unwrap();
536        assert_eq!(stored.sign_count(), 5);
537
538        // Now a second device, holding a copy of the key, at a lower count.
539        let request = session.rp.start_authentication(&session.challenges).await.unwrap();
540        let cloned =
541            session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 3, request.challenge());
542
543        let error = session.finish(&cloned).await.unwrap_err().to_string();
544        assert!(error.contains("went backwards"), "got {error}");
545        assert!(error.contains("copied off the device"), "got {error}");
546    }
547
548    #[tokio::test]
549    async fn a_sign_counter_of_zero_on_both_sides_is_normal_and_not_a_clone() {
550        // Synced passkeys keep no counter: there is no single device to count.
551        // Refusing this would refuse every modern credential.
552        let session = Session::fresh().await;
553
554        session.finish(&session.healthy()).await.unwrap();
555
556        let request = session.rp.start_authentication(&session.challenges).await.unwrap();
557        let again =
558            session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, request.challenge());
559
560        assert!(session.finish(&again).await.is_ok(), "a passkey was mistaken for a clone");
561    }
562
563    #[tokio::test]
564    async fn a_replayed_assertion_is_refused_because_answering_burned_the_challenge() {
565        let session = Session::fresh().await;
566        let response = session.healthy();
567
568        session.finish(&response).await.unwrap();
569
570        // Byte for byte the message that just worked.
571        let error = session.finish(&response).await.unwrap_err().to_string();
572        assert!(error.contains("already been answered"), "got {error}");
573    }
574
575    #[tokio::test]
576    async fn an_expired_challenge_is_refused() {
577        let session = Session::fresh().await;
578        let stale = Challenge::issue_lasting(Ceremony::Authentication, 0);
579        let bytes = stale.bytes().to_vec();
580        session.challenges.store(stale).await.unwrap();
581
582        let response = session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, &bytes);
583
584        let error = session.finish(&response).await.unwrap_err().to_string();
585        assert!(error.contains("expired"), "got {error}");
586    }
587
588    #[tokio::test]
589    async fn a_registration_challenge_cannot_be_spent_on_a_login() {
590        let session = Session::fresh().await;
591        let other = Challenge::issue(Ceremony::Registration);
592        let bytes = other.bytes().to_vec();
593        session.challenges.store(other).await.unwrap();
594
595        let response = session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, &bytes);
596
597        let error = session.finish(&response).await.unwrap_err().to_string();
598        assert!(error.contains("issued for registration"), "got {error}");
599    }
600
601    #[tokio::test]
602    async fn a_wrong_origin_is_refused() {
603        let session = Session::fresh().await;
604        let response = session.assert_with(
605            ASSERTION_FLAGS,
606            "example.com",
607            "https://evil.test",
608            0,
609            &session.challenge,
610        );
611
612        let error = session.finish(&response).await.unwrap_err().to_string();
613        assert!(error.contains("evil.test"), "got {error}");
614    }
615
616    #[tokio::test]
617    async fn an_origin_that_is_only_a_prefix_of_the_right_one_is_refused() {
618        // A genuine signature over a genuine challenge, collected at
619        // `https://example.com.evil.test`. Only the exact origin check refuses
620        // it, and this is the attack the whole protocol exists to stop.
621        let session = Session::fresh().await;
622        let response = session.assert_with(
623            ASSERTION_FLAGS,
624            "example.com",
625            "https://example.com.evil.test",
626            0,
627            &session.challenge,
628        );
629
630        let error = session.finish(&response).await.unwrap_err().to_string();
631        assert!(error.contains("exact"), "got {error}");
632    }
633
634    #[tokio::test]
635    async fn a_wrong_relying_party_id_hash_is_refused() {
636        let session = Session::fresh().await;
637        let response =
638            session.assert_with(ASSERTION_FLAGS, "evil.test", ORIGIN, 0, &session.challenge);
639
640        let error = session.finish(&response).await.unwrap_err().to_string();
641        assert!(error.contains("different relying party"), "got {error}");
642    }
643
644    #[tokio::test]
645    async fn user_present_not_set_is_refused() {
646        let session = Session::fresh().await;
647        let response = session.assert_with(0x00, "example.com", ORIGIN, 0, &session.challenge);
648
649        let error = session.finish(&response).await.unwrap_err().to_string();
650        assert!(error.contains("User Present"), "got {error}");
651    }
652
653    #[tokio::test]
654    async fn user_verification_is_refused_when_required_and_not_performed() {
655        let rp = relying_party().with_user_verification(UserVerification::Required);
656        let session = Session::with(rp, Authenticator::new(1)).await;
657        let response = session.assert_with(0x01, "example.com", ORIGIN, 0, &session.challenge);
658
659        let error = session.finish(&response).await.unwrap_err().to_string();
660        assert!(error.contains("user verification was required"), "got {error}");
661    }
662
663    #[tokio::test]
664    async fn a_user_handle_that_belongs_to_somebody_else_is_refused() {
665        let session = Session::fresh().await;
666        let response = session.healthy().with_user_handle(b"user-99".to_vec());
667
668        let error = session.finish(&response).await.unwrap_err().to_string();
669        assert!(error.contains("does not belong"), "got {error}");
670    }
671
672    #[tokio::test]
673    async fn an_unknown_credential_id_is_refused() {
674        let session = Session::fresh().await;
675        let mut response = session.healthy();
676        response.id = b"never registered".to_vec();
677
678        let error = session.finish(&response).await.unwrap_err().to_string();
679        assert!(error.contains("no credential is registered"), "got {error}");
680    }
681
682    #[tokio::test]
683    async fn a_login_bound_to_one_user_cannot_be_finished_by_another() {
684        let session = Session::fresh().await;
685        let bound = session
686            .rp
687            .start_authentication_for(b"user-99", &session.challenges, &session.credentials)
688            .await
689            .unwrap();
690
691        let response =
692            session.assert_with(ASSERTION_FLAGS, "example.com", ORIGIN, 0, bound.challenge());
693
694        let error = session.finish(&response).await.unwrap_err().to_string();
695        assert!(error.contains("issued for a different user"), "got {error}");
696    }
697
698    #[tokio::test]
699    async fn a_login_started_for_a_user_names_the_credentials_they_have() {
700        let session = Session::fresh().await;
701        let options = session
702            .rp
703            .start_authentication_for(b"user-41", &session.challenges, &session.credentials)
704            .await
705            .unwrap();
706
707        assert_eq!(
708            options.allow_credentials(),
709            std::slice::from_ref(&session.device.credential_id)
710        );
711
712        let json = options.json();
713        assert_eq!(json.get("rpId").and_then(Json::as_str), Some("example.com"));
714        assert_eq!(
715            json.get("allowCredentials.0.id").and_then(Json::as_str),
716            Some(base64::encode_url(&session.device.credential_id).as_str())
717        );
718
719        // The usernameless form names nobody, so it leaks nothing about which
720        // accounts exist.
721        let anonymous = session.rp.start_authentication(&session.challenges).await.unwrap();
722        assert!(anonymous.allow_credentials().is_empty());
723        let json = anonymous.json();
724        assert!(json.get("allowCredentials").and_then(Json::as_array).unwrap().is_empty());
725    }
726
727    #[test]
728    fn a_response_can_be_read_from_the_json_a_browser_posts() {
729        let body = format!(
730            "{{\"id\":\"{id}\",\"rawId\":\"{id}\",\"type\":\"public-key\",\"response\":\
731             {{\"clientDataJSON\":\"{client}\",\"authenticatorData\":\"{auth}\",\
732             \"signature\":\"{signature}\",\"userHandle\":\"{handle}\"}}}}",
733            id = base64::encode_url(b"credential"),
734            client = base64::encode_url(b"{}"),
735            auth = base64::encode_url(b"authdata"),
736            signature = base64::encode_url(b"sig"),
737            handle = base64::encode_url(b"user-41"),
738        );
739
740        let response = AuthenticationResponse::parse(&body).unwrap();
741
742        assert_eq!(response.id(), b"credential");
743        assert_eq!(response.authenticator_data(), b"authdata");
744        assert_eq!(response.signature(), b"sig");
745        assert_eq!(response.user_handle(), Some(b"user-41".as_slice()));
746
747        // A credential that is not discoverable sends null, which is not a
748        // handle of zero bytes.
749        let without = body.replace(
750            &format!("\"userHandle\":\"{}\"", base64::encode_url(b"user-41")),
751            "\"userHandle\":null",
752        );
753        assert_eq!(AuthenticationResponse::parse(&without).unwrap().user_handle(), None);
754
755        assert!(AuthenticationResponse::parse("{}").is_err());
756    }
757}