Skip to content

Commit

Permalink
Require naming Passkeys, track last used time.
Browse files Browse the repository at this point in the history
  • Loading branch information
Syfaro committed Feb 29, 2024
1 parent 4a80c70 commit 7152e4a
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 51 deletions.
11 changes: 9 additions & 2 deletions frontend/src/ts/pages/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ async function performPasswordlessLogin() {
}

async function performRegistration() {
const name = prompt("Please enter a name for this Passkey.");
if (!name) return;

const opts = await generateRegistrationOptions();
let attestation = await startRegistration(opts);

try {
await verifyRegistration(attestation);
await verifyRegistration(name, attestation);
alert("Passkey registered!");
} catch {
alert("Could not register Passkey, please try again later.");
Expand All @@ -63,11 +66,15 @@ async function generateRegistrationOptions(): Promise<
return await resp.json();
}

async function verifyRegistration(response: RegistrationResponseJSON) {
async function verifyRegistration(
name: string,
response: RegistrationResponseJSON,
) {
const resp = await fetch("/auth/webauthn/verify-registration", {
method: "POST",
headers: {
"content-type": "application/json",
"x-passkey-name": name,
},
body: JSON.stringify(response),
});
Expand Down
4 changes: 3 additions & 1 deletion migrations/20240229035127_webauthn.up.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ CREATE TABLE webauthn_credential (
id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES user_account (id) ON DELETE CASCADE,
created_at timestamp with time zone NOT NULL DEFAULT current_timestamp,
last_used timestamp with time zone,
credential_id bytea NOT NULL UNIQUE,
name text NOT NULL,
credential jsonb NOT NULL
);

CREATE INDEX webauthn_credential_owner_idx ON webauthn_credential (owner_id, created_at DESC);
CREATE INDEX webauthn_credential_owner_idx ON webauthn_credential (owner_id, last_used DESC);
4 changes: 2 additions & 2 deletions queries/webauthn/insert_credential.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
INSERT INTO
webauthn_credential (owner_id, credential_id, credential)
webauthn_credential (owner_id, credential_id, name, credential)
VALUES
($1, $2, $3) RETURNING id;
($1, $2, $3, $4) RETURNING id;
6 changes: 6 additions & 0 deletions queries/webauthn/mark_used.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
UPDATE
webauthn_credential
SET
last_used = current_timestamp
WHERE
credential_id = $1;
5 changes: 4 additions & 1 deletion queries/webauthn/user_credentials.sql
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
SELECT
created_at,
name,
credential_id
FROM
webauthn_credential
WHERE
owner_id = $1;
owner_id = $1
ORDER BY
last_used DESC;
93 changes: 56 additions & 37 deletions sqlx-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1799,6 +1799,29 @@
},
"query": "SELECT\n id \"id!\",\n owner_id \"owner_id!\",\n perceptual_hash,\n sha256_hash \"sha256_hash!: Sha256Hash\",\n last_modified \"last_modified!\",\n content_url,\n content_size,\n thumb_url,\n event_count \"event_count!\",\n last_event,\n accounts \"accounts: sqlx::types::Json<Vec<OwnedMediaItemAccount>>\"\nFROM\n owned_media_item_accounts\nWHERE\n owner_id = $1\n AND (\n $4::uuid IS NULL\n OR exists(\n SELECT\n 1\n FROM\n owned_media_item_account\n WHERE\n owned_media_item_account.owned_media_item_id = owned_media_item_accounts.id\n AND account_id = $4\n )\n )\nORDER BY\n last_modified DESC\nLIMIT\n $2 OFFSET ($3::integer * $2::integer);\n"
},
"6ab4dbcded5cd1b92a51a6b55e3e81487aab2f893de339818e9a7a681ef721da": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Bytea",
"Text",
"Jsonb"
]
}
},
"query": "INSERT INTO\n webauthn_credential (owner_id, credential_id, name, credential)\nVALUES\n ($1, $2, $3, $4) RETURNING id;\n"
},
"6c0689be25c41e688a85dbc8e1365e83eec74ce2842665faf67c2ffcb970644d": {
"describe": {
"columns": [
Expand Down Expand Up @@ -2930,19 +2953,7 @@
},
"query": "SELECT\n *\nFROM\n user_account\nWHERE\n telegram_id = $1;\n"
},
"b8946dfef01780773713136e31594b2347be98536da6e066ed49378f88b11e13": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "DELETE FROM\n flist_import_run\nWHERE\n id = $1;\n"
},
"bdbabe5b3f6202b23485b703f29064a38e1efef95c102345329a5b39a57dc9c2": {
"b5dd8cc3db095aab02212c88575cf47b22aab72f1afb6ea48a9c08fba8097259": {
"describe": {
"columns": [
{
Expand All @@ -2951,12 +2962,18 @@
"type_info": "Timestamptz"
},
{
"name": "credential_id",
"name": "name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "credential_id",
"ordinal": 2,
"type_info": "Bytea"
}
],
"nullable": [
false,
false,
false
],
Expand All @@ -2966,7 +2983,19 @@
]
}
},
"query": "SELECT\n created_at,\n credential_id\nFROM\n webauthn_credential\nWHERE\n owner_id = $1;\n"
"query": "SELECT\n created_at,\n name,\n credential_id\nFROM\n webauthn_credential\nWHERE\n owner_id = $1\nORDER BY\n last_used DESC;\n"
},
"b8946dfef01780773713136e31594b2347be98536da6e066ed49378f88b11e13": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Uuid"
]
}
},
"query": "DELETE FROM\n flist_import_run\nWHERE\n id = $1;\n"
},
"c151a7a2cf44ff302d8c3eae7ed695ce52e07c597059030c282b131eee95b64b": {
"describe": {
Expand Down Expand Up @@ -3174,28 +3203,6 @@
},
"query": "INSERT INTO\n reddit_subreddit (name)\nVALUES\n (lower($1)) ON CONFLICT DO NOTHING;\n"
},
"d0bc5e25ad86c1d91c391a237d296863f2a8f6b20b40314adf449edec52ba6d4": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Uuid"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Uuid",
"Bytea",
"Jsonb"
]
}
},
"query": "INSERT INTO\n webauthn_credential (owner_id, credential_id, credential)\nVALUES\n ($1, $2, $3) RETURNING id;\n"
},
"d323b8bbcee14a2712722f63fe7d5fbdcd9ee5589640eb1679779e8eeecd70ea": {
"describe": {
"columns": [
Expand Down Expand Up @@ -3786,6 +3793,18 @@
},
"query": "SELECT\n *\nFROM\n flist_file\nWHERE\n perceptual_hash <@ ($1, $2);\n"
},
"f7ddc89d2ca938be3e0a96c89edc5c5f0887a47c8e73cf7f8973b6c37ace1383": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Bytea"
]
}
},
"query": "UPDATE\n webauthn_credential\nSET\n last_used = current_timestamp\nWHERE\n credential_id = $1;\n"
},
"f9ed81bc48afca376d8bab5df361fd73b27228ac81a79fffd82de26a47c1afc8": {
"describe": {
"columns": [
Expand Down
27 changes: 20 additions & 7 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,8 @@ async fn verify_authentication(
.ok_or(Error::Missing)?
.map_err(Error::from_displayable)?;

let (user_id, credential) = models::WebauthnCredential::lookup_by_credential_id(
&conn,
reg.get_credential_id().to_vec(),
)
.await?;
let (user_id, credential) =
models::WebauthnCredential::lookup_by_credential_id(&conn, reg.get_credential_id()).await?;

webauthn
.finish_discoverable_authentication(&reg, discoverable_auth, &[(&credential).into()])
Expand All @@ -640,12 +637,14 @@ async fn verify_authentication(
let session_id = models::UserSession::create(
&conn,
user.id,
models::UserSessionSource::Webauthn(reg.raw_id),
models::UserSessionSource::Webauthn(reg.raw_id.clone()),
client_ip.ip_addr.as_deref(),
)
.await?;
session.set_session_token(user.id, session_id)?;

let _ = models::WebauthnCredential::mark_used(&conn, reg.get_credential_id()).await;

Ok(HttpResponse::Ok().json(serde_json::json!({
"redirect_url": request.url_for_static("user_home")?.as_str(),
})))
Expand Down Expand Up @@ -701,6 +700,7 @@ async fn verify_registration(
unleash: web::Data<crate::Unleash>,
webauthn: web::Data<Arc<webauthn_rs::Webauthn>>,
conn: web::Data<sqlx::PgPool>,
request: actix_web::HttpRequest,
session: Session,
user: models::User,
Json(reg): Json<webauthn_rs::prelude::RegisterPublicKeyCredential>,
Expand All @@ -709,6 +709,12 @@ async fn verify_registration(
return Err(Error::Unauthorized);
}

let passkey_name = request
.headers()
.get("x-passkey-name")
.and_then(|name| name.to_str().ok())
.ok_or(Error::Missing)?;

let reg_state = session
.remove_as("reg_state")
.ok_or(Error::Missing)?
Expand All @@ -719,7 +725,14 @@ async fn verify_registration(

let credential_id = passkey.cred_id().0.to_owned();

models::WebauthnCredential::insert_credential(&conn, user.id, credential_id, passkey).await?;
models::WebauthnCredential::insert_credential(
&conn,
user.id,
credential_id,
passkey_name,
passkey,
)
.await?;

Ok(HttpResponse::NoContent().finish())
}
Expand Down
13 changes: 12 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ impl User {

pub struct WebauthnCredential {
pub credential_id: Vec<u8>,
pub name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}

Expand All @@ -392,12 +393,14 @@ impl WebauthnCredential {
conn: &sqlx::PgPool,
user_id: Uuid,
credential_id: Vec<u8>,
name: &str,
passkey: webauthn_rs::prelude::Passkey,
) -> Result<Uuid, Error> {
sqlx::query_file_scalar!(
"queries/webauthn/insert_credential.sql",
user_id,
credential_id,
name,
serde_json::to_value(passkey)?,
)
.fetch_one(conn)
Expand All @@ -407,7 +410,7 @@ impl WebauthnCredential {

pub async fn lookup_by_credential_id(
conn: &sqlx::PgPool,
credential_id: Vec<u8>,
credential_id: &[u8],
) -> Result<(Uuid, webauthn_rs::prelude::Passkey), Error> {
let user = sqlx::query_file!("queries/webauthn/lookup_by_credential.sql", credential_id)
.map(|row| {
Expand All @@ -418,6 +421,14 @@ impl WebauthnCredential {

Ok(user)
}

pub async fn mark_used(conn: &sqlx::PgPool, credential_id: &[u8]) -> Result<(), Error> {
sqlx::query_file!("queries/webauthn/mark_used.sql", credential_id)
.execute(conn)
.await?;

Ok(())
}
}

#[derive(Deserialize, Serialize)]
Expand Down

0 comments on commit 7152e4a

Please sign in to comment.