Skip to main content

cortex/models/
webauthn.rs

1// Copyright 2015-2025 Deyan Ginev. See the LICENSE
2// file at the top-level directory of this distribution.
3//
4// Licensed under the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>.
5// This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! Persistence for passkey (**WebAuthn**) sign-in (`docs/archive/WEBAUTHN_DESIGN.md`): the
9//! per-owner WebAuthn user handle and the enrolled public-key credentials. **Only public keys are
10//! stored** — the `credential` column holds a serialized `webauthn_rs::prelude::Passkey` (public
11//! key + counter), kept as opaque JSON here so the persistence layer does not depend on the
12//! WebAuthn crate.
13
14use chrono::NaiveDateTime;
15use diesel::prelude::*;
16use diesel::result::Error;
17use serde_json::Value;
18use uuid::Uuid;
19
20use crate::schema::{webauthn_credentials, webauthn_users};
21
22/// A WebAuthn user: the stable handle credentials are bound to, one per admin `owner`.
23#[derive(Queryable, Identifiable, Debug, Clone)]
24#[diesel(table_name = webauthn_users, primary_key(owner))]
25pub struct WebauthnUser {
26  /// The human identity (the audit-log actor / token owner).
27  pub owner: String,
28  /// The stable per-user handle WebAuthn binds credentials to.
29  pub handle: Uuid,
30  /// When the user was first enrolled.
31  pub created_at: NaiveDateTime,
32}
33
34#[derive(Insertable)]
35#[diesel(table_name = webauthn_users)]
36struct NewWebauthnUser<'a> {
37  owner: &'a str,
38  handle: Uuid,
39}
40
41impl WebauthnUser {
42  /// Returns the owner's stable WebAuthn handle, creating the user with a fresh random handle on
43  /// first enrollment. Idempotent and concurrency-safe (`ON CONFLICT DO NOTHING`, then re-read).
44  pub fn ensure(connection: &mut PgConnection, owner: &str) -> Result<Uuid, Error> {
45    use crate::schema::webauthn_users::dsl;
46    if let Some(handle) = dsl::webauthn_users
47      .filter(dsl::owner.eq(owner))
48      .select(dsl::handle)
49      .first::<Uuid>(connection)
50      .optional()?
51    {
52      return Ok(handle);
53    }
54    diesel::insert_into(webauthn_users::table)
55      .values(NewWebauthnUser {
56        owner,
57        handle: Uuid::new_v4(),
58      })
59      .on_conflict(dsl::owner)
60      .do_nothing()
61      .execute(connection)?;
62    // Re-read so a concurrent insert's handle (whichever won) is the one returned.
63    dsl::webauthn_users
64      .filter(dsl::owner.eq(owner))
65      .select(dsl::handle)
66      .first::<Uuid>(connection)
67  }
68}
69
70/// An enrolled passkey row. `credential` is the serialized `Passkey` (public key + signature
71/// counter + metadata) — public data only.
72#[derive(Queryable, Identifiable, Debug, Clone)]
73#[diesel(table_name = webauthn_credentials)]
74pub struct WebauthnCredential {
75  /// Auto-incremented id.
76  pub id: i64,
77  /// The owner this passkey authenticates as.
78  pub owner: String,
79  /// A human label for the authenticator (e.g. "MacBook Touch ID").
80  pub label: String,
81  /// The serialized `webauthn_rs::prelude::Passkey` (opaque public JSON here).
82  pub credential: Value,
83  /// When the passkey was enrolled.
84  pub created_at: NaiveDateTime,
85  /// Last successful authentication with this passkey (`None` until first used).
86  pub last_used: Option<NaiveDateTime>,
87}
88
89#[derive(Insertable)]
90#[diesel(table_name = webauthn_credentials)]
91struct NewWebauthnCredential<'a> {
92  owner: &'a str,
93  label: &'a str,
94  credential: &'a Value,
95}
96
97impl WebauthnCredential {
98  /// Stores a newly-enrolled passkey for `owner`.
99  pub fn store(
100    connection: &mut PgConnection,
101    owner: &str,
102    label: &str,
103    credential: &Value,
104  ) -> Result<(), Error> {
105    diesel::insert_into(webauthn_credentials::table)
106      .values(NewWebauthnCredential {
107        owner,
108        label,
109        credential,
110      })
111      .execute(connection)
112      .map(|_| ())
113  }
114
115  /// All enrolled passkeys for `owner` (oldest first) — the set an authentication ceremony allows.
116  pub fn for_owner(connection: &mut PgConnection, owner: &str) -> Result<Vec<Self>, Error> {
117    use crate::schema::webauthn_credentials::dsl;
118    dsl::webauthn_credentials
119      .filter(dsl::owner.eq(owner))
120      .order(dsl::id)
121      .get_results(connection)
122  }
123
124  /// Removes one of `owner`'s passkeys by id (the `owner` filter prevents removing another's key).
125  /// Returns the number deleted (`0` if it wasn't theirs / didn't exist).
126  pub fn delete(connection: &mut PgConnection, id: i64, owner: &str) -> Result<usize, Error> {
127    use crate::schema::webauthn_credentials::dsl;
128    diesel::delete(
129      dsl::webauthn_credentials
130        .filter(dsl::id.eq(id))
131        .filter(dsl::owner.eq(owner)),
132    )
133    .execute(connection)
134  }
135
136  /// Replaces a credential's serialized state (e.g. the signature counter after a login) and stamps
137  /// `last_used`. Called when WebAuthn reports the authenticator's counter advanced.
138  pub fn update_after_use(
139    connection: &mut PgConnection,
140    id: i64,
141    credential: &Value,
142  ) -> Result<(), Error> {
143    use crate::schema::webauthn_credentials::dsl;
144    diesel::update(dsl::webauthn_credentials.filter(dsl::id.eq(id)))
145      .set((
146        dsl::credential.eq(credential),
147        dsl::last_used.eq(chrono::Utc::now().naive_utc()),
148      ))
149      .execute(connection)
150      .map(|_| ())
151  }
152
153  /// Stamps `last_used = now()` without changing the stored credential (login where the counter did
154  /// not need an update).
155  pub fn touch(connection: &mut PgConnection, id: i64) -> Result<(), Error> {
156    use crate::schema::webauthn_credentials::dsl;
157    diesel::update(dsl::webauthn_credentials.filter(dsl::id.eq(id)))
158      .set(dsl::last_used.eq(chrono::Utc::now().naive_utc()))
159      .execute(connection)
160      .map(|_| ())
161  }
162}