From 75e616201b50bc174c22992d6a306d8a691e60af Mon Sep 17 00:00:00 2001 From: mortis-0 <1190200714@stu.hit.edu.cn> Date: Thu, 14 Mar 2024 00:24:04 +0800 Subject: [PATCH] fuck WIP shit --- ...4efe82935f5f681c3cf54cbeaf845b70c7ff6.json | 12 + ...440258c22cc6d14848d247a322a1ff2c73810.json | 20 + ...af623fe2887015ea22f1579f477bc1aeb7a21.json | 20 + ...e7f25415da7e916934e3cf983a9f590804a41.json | 20 + ...1ad58037c7d5b433cba9855fff21461b9e7fc.json | 12 + ...5c3c7ec2a57c9789d04485b998fd934d6b0a8.json | 26 -- ...2b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json | 12 + ...b525ae6b4de88eba24b0d4126ba54d07b0731.json | 20 + ...05886df0573e55c9ac62ac1cc8ecd15ae9801.json | 20 + ...764a0965331c825faf0a86ca18331e4d8ebdf.json | 20 + Cargo.lock | 402 +++++++++++++++-- Cargo.toml | 6 +- src/config.rs | 22 + src/entities/mod.rs | 42 +- src/main.rs | 58 +-- src/services/auth.rs | 413 ++++++++++++++++++ src/services/mod.rs | 36 +- 17 files changed, 1031 insertions(+), 130 deletions(-) create mode 100644 .sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json create mode 100644 .sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json create mode 100644 .sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json create mode 100644 .sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json create mode 100644 .sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json delete mode 100644 .sqlx/query-6b379715355f3443d66111020be5c3c7ec2a57c9789d04485b998fd934d6b0a8.json create mode 100644 .sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json create mode 100644 .sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json create mode 100644 .sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json create mode 100644 .sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json create mode 100644 src/services/auth.rs diff --git a/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json b/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json new file mode 100644 index 0000000..f503a0d --- /dev/null +++ b/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users(KEY, NAME) VALUES($1, $2);", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6" +} diff --git a/.sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json b/.sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json new file mode 100644 index 0000000..5d1cdec --- /dev/null +++ b/.sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT key FROM users WHERE name = $1;", + "describe": { + "columns": [ + { + "name": "KEY", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810" +} diff --git a/.sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json b/.sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json new file mode 100644 index 0000000..deb3f2e --- /dev/null +++ b/.sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT CREDENTIAL FROM CREDENTIALS WHERE USER_ID = $1;", + "describe": { + "columns": [ + { + "name": "CREDENTIAL", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21" +} diff --git a/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json b/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json new file mode 100644 index 0000000..7f11ffe --- /dev/null +++ b/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT credential FROM CREDENTIALS WHERE user_id = $1;", + "describe": { + "columns": [ + { + "name": "CREDENTIAL", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41" +} diff --git a/.sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json b/.sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json new file mode 100644 index 0000000..30d5f40 --- /dev/null +++ b/.sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO CREDENTIALS(user_id, credential) VALUES($1, $2);", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc" +} diff --git a/.sqlx/query-6b379715355f3443d66111020be5c3c7ec2a57c9789d04485b998fd934d6b0a8.json b/.sqlx/query-6b379715355f3443d66111020be5c3c7ec2a57c9789d04485b998fd934d6b0a8.json deleted file mode 100644 index d80585e..0000000 --- a/.sqlx/query-6b379715355f3443d66111020be5c3c7ec2a57c9789d04485b998fd934d6b0a8.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "db_name": "SQLite", - "query": "\nSELECT NAME, KEY FROM USERS WHERE NAME = ?1 AND KEY = ?2\n ", - "describe": { - "columns": [ - { - "name": "NAME", - "ordinal": 0, - "type_info": "Text" - }, - { - "name": "KEY", - "ordinal": 1, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - false, - false - ] - }, - "hash": "6b379715355f3443d66111020be5c3c7ec2a57c9789d04485b998fd934d6b0a8" -} diff --git a/.sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json b/.sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json new file mode 100644 index 0000000..de3d125 --- /dev/null +++ b/.sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE CREDENTIALS SET credential = $1 WHERE user_id = $2 AND credential = $3;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc" +} diff --git a/.sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json b/.sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json new file mode 100644 index 0000000..0ea765a --- /dev/null +++ b/.sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731" +} diff --git a/.sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json b/.sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json new file mode 100644 index 0000000..82161e4 --- /dev/null +++ b/.sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT NAME FROM users WHERE KEY = $1;", + "describe": { + "columns": [ + { + "name": "NAME", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801" +} diff --git a/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json b/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json new file mode 100644 index 0000000..e115a62 --- /dev/null +++ b/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT KEY FROM users WHERE NAME = $1;", + "describe": { + "columns": [ + { + "name": "KEY", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf" +} diff --git a/Cargo.lock b/Cargo.lock index 81d2539..97f69ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -97,6 +97,45 @@ dependencies = [ "toml", ] +[[package]] +name = "asn1-rs" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff05a702273012438132f449575dbc804e27b2f3cbe3069aa237d26c98fa33" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8b7511298d5b7784b40b092d9e9dcd3a627a5707e4b5e507931ab0d44eeebf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "async-trait" version = "0.1.74" @@ -105,7 +144,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -126,6 +165,7 @@ dependencies = [ "minijinja", "once_cell", "serde", + "serde_json", "sqlx", "tokio", "totp-rs", @@ -135,6 +175,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "webauthn-rs", ] [[package]] @@ -151,6 +192,7 @@ checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bytes", "futures-util", "http", @@ -198,6 +240,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -219,6 +273,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.5" @@ -237,6 +297,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "base64urlsafedata" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b3d30abb74120a9d5267463b9e0045fdccc4dd152e7249d966612dc1721384" +dependencies = [ + "base64 0.21.5", + "serde", + "serde_json", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -300,6 +371,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "compact_jwt" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa76ef19968577838a34d02848136bb9b6bdbfd7675fb968fe9c931bc434b33" +dependencies = [ + "base64 0.13.1", + "base64urlsafedata", + "hex", + "openssl", + "serde", + "serde_json", + "tracing", + "url", + "uuid", +] + [[package]] name = "const-oid" version = "0.9.5" @@ -376,6 +464,12 @@ dependencies = [ "typenum", ] +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + [[package]] name = "der" version = "0.7.8" @@ -387,6 +481,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe398ac75057914d7d07307bf67dc7f3f574a26783b4fc7805a20ffa9f506e82" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.9" @@ -409,6 +517,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -486,6 +605,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -567,7 +701,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -645,6 +779,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hashbrown" version = "0.14.2" @@ -871,9 +1011,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libsqlite3-sys" -version = "0.26.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -1008,6 +1148,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1075,12 +1226,59 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e20717fa0541f39bd146692035c37bedfa532b3e5071b35761082407546b2a" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1148,7 +1346,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -1204,18 +1402,18 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1335,6 +1533,15 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.21" @@ -1368,29 +1575,39 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] -name = "serde_derive" -version = "1.0.190" +name = "serde_cbor_2" +version = "0.12.0-dev" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -1532,9 +1749,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1545,9 +1762,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ "ahash", "atoi", @@ -1555,7 +1772,6 @@ dependencies = [ "bytes", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -1585,9 +1801,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -1598,9 +1814,9 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", @@ -1623,9 +1839,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.5", @@ -1665,9 +1881,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.5", @@ -1692,7 +1908,6 @@ dependencies = [ "rand", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -1704,9 +1919,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "flume", @@ -1722,6 +1937,7 @@ dependencies = [ "sqlx-core", "tracing", "url", + "urlencoding", ] [[package]] @@ -1760,9 +1976,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -1775,6 +1991,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tap" version = "1.0.1" @@ -1811,7 +2039,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -1895,7 +2123,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -2078,7 +2306,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] @@ -2153,6 +2381,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -2168,8 +2402,15 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.5.0" @@ -2178,6 +2419,7 @@ checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", "rand", + "serde", ] [[package]] @@ -2204,6 +2446,56 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webauthn-rs" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2db00711c712414e93b019c4596315085792215bc2ac2d5872f9e8913b0a6316" +dependencies = [ + "base64urlsafedata", + "serde", + "tracing", + "url", + "uuid", + "webauthn-rs-core", +] + +[[package]] +name = "webauthn-rs-core" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "294c78c83f12153a51e1cf1e6970b5da1397645dada39033a9c3173a8fc4fc2b" +dependencies = [ + "base64 0.13.1", + "base64urlsafedata", + "compact_jwt", + "der-parser", + "nom 7.1.3", + "openssl", + "rand", + "serde", + "serde_cbor_2", + "serde_json", + "thiserror", + "tracing", + "url", + "uuid", + "webauthn-rs-proto", + "x509-parser", +] + +[[package]] +name = "webauthn-rs-proto" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24e638361a63ba5c0a0be6a60229490fcdf33740ed63df5bb6bdb627b52a138" +dependencies = [ + "base64urlsafedata", + "serde", + "serde_json", + "url", +] + [[package]] name = "whoami" version = "1.4.1" @@ -2305,23 +2597,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" [[package]] -name = "zerocopy" -version = "0.7.18" +name = "x509-parser" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7d7c7970ca2215b8c1ccf4d4f354c4733201dfaaba72d44ae5b37472e4901" +checksum = "9fb9bace5b5589ffead1afb76e43e34cff39cd0f3ce7e170ae0c29e53b88eb1c" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.18" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b27b1bb92570f989aac0ab7e9cbfbacdd65973f7ee920d9f0e71ebac878fd0b" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.52", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ede8804..75236fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,9 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -axum = {version = "0.7.4", features = [ "default", "form" ]} +axum = {version = "0.7.4", features = [ "default", "form", "tracing", "macros" ]} tokio = { version = "1.0", features = ["full"] } -sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } +sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] } tower-cookies = "0.10.0" serde = "1.0.190" uuid = { version = "1.5.0", features = ["v4", "fast-rng"] } @@ -20,3 +20,5 @@ askama = "0.10" minijinja = "1.0.9" once_cell = "1.18.0" tower-sessions = "0.11.0" +webauthn-rs = { version = "0.4.8", features = [ "danger-allow-state-serialisation" ]} +serde_json = "1.0.114" diff --git a/src/config.rs b/src/config.rs index e69de29..547c405 100644 --- a/src/config.rs +++ b/src/config.rs @@ -0,0 +1,22 @@ +use once_cell::sync::Lazy; +use std::env; +pub const COOKIE_NAME: Lazy = + Lazy::new(|| env::var("COOKIE_NAME").unwrap_or("aaron_auth".to_string())); +pub const SESSION_ACTIVE_TIME: Lazy = Lazy::new(|| { + env::var("SESSION_ACTIVE_TIME") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(600) +}); +pub const COOKIE_DOMAIN: Lazy = + Lazy::new(|| env::var("DOMAIN").ok().unwrap_or(".aaronhu.cn".to_owned())); +pub const LOGIN_PAGE_HTML: &str = include_str!("../loginpage.html"); +pub const PORT: Lazy = Lazy::new(|| env::var("PORT").unwrap_or("3000".to_string())); +pub const DATABASE_URL: Lazy = + Lazy::new(|| env::var("DATABASE_URL").unwrap_or("".to_owned())); +pub const RP_ID: Lazy = + Lazy::new(|| env::var("RP_ID").unwrap_or("localhost".to_owned())); +pub const RP_ORIGIN: Lazy = + Lazy::new(|| env::var("RP_ORIGIN").unwrap_or("http://localhost:8080".to_owned())); +pub const RP_NAME: Lazy = + Lazy::new(|| env::var("RP_NAME").unwrap_or("Localhost".to_owned())); diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 4432afa..be38210 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,14 +1,52 @@ use serde::Deserialize; -use std::time::Instant; +use sqlx::SqlitePool; use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; use tokio::sync::Mutex; use uuid::Uuid; +use webauthn_rs::prelude::Url; +use webauthn_rs::{Webauthn, WebauthnBuilder}; + +use crate::config::{DATABASE_URL, RP_ID, RP_NAME, RP_ORIGIN}; pub struct ServerState { pub db: sqlx::Pool, pub session: Mutex>, + pub webauthn: Arc, // started: Instant, } + +impl ServerState { + pub async fn new() -> Self { + // Effective domain name. + let rp_id = RP_ID; + // Url containing the effective domain name + // MUST include the port number! + let rp_origin = Url::parse(&RP_ORIGIN).expect("Invalid URL"); + let builder = WebauthnBuilder::new(&rp_id, &rp_origin).expect("Invalid configuration"); + + // Now, with the builder you can define other options. + // Set a "nice" relying party name. Has no security properties and + // may be changed in the future. + let _RP_NAME = RP_NAME.to_string(); + let builder = builder.rp_name(&_RP_NAME); + + // Consume the builder and create our webauthn instance. + let webauthn = Arc::new(builder.build().expect("Invalid configuration")); + + let session = Mutex::new(HashMap::new()); + let db = SqlitePool::connect(&DATABASE_URL) + .await + .expect("DB OPEN FAILURE"); // our router + ServerState { + db, + session, + webauthn, + } + } +} + #[derive(Deserialize, sqlx::FromRow, Debug)] pub struct UserLoginForm { #[sqlx(rename = "NAME")] @@ -23,4 +61,4 @@ pub struct User { pub username: String, #[sqlx(rename = "KEY")] pub uid: String, -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index cdd25e6..b42a121 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,36 +1,22 @@ -use axum::extract::Query; -use axum::http::{HeaderMap, HeaderValue}; -use axum::response::{Html, Redirect}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Form, Router}; + + + +use axum::{routing::{get, post, Route}, Router}; use entities::*; -use minijinja::{context, Environment}; -use once_cell::sync::Lazy; -use services::{auth, gc_task, login, login_page, login_with_passkey}; -use sqlx::sqlite::SqlitePool; + +use services::{auth::{self, finish_authentication, start_authentication}, gc_task, login, login_page}; use std::net::SocketAddr; use std::sync::Arc; -use std::time::{Duration, Instant}; -use std::{borrow::BorrowMut, env}; -use std::{collections::HashMap, str::FromStr}; -use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; +use tower_cookies::{CookieManagerLayer}; use tower_http::trace::{self, TraceLayer}; -use tower_sessions::{Expiry, MemoryStore, Session, SessionManagerLayer}; +use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer}; use tracing::Level; -use uuid::Uuid; + +pub mod config; pub mod controllers; pub mod entities; pub mod services; -static COOKIE_NAME: Lazy = - Lazy::new(|| env::var("COOKIE_NAME").unwrap_or("aaron_auth".to_string())); -const SESSION_ACTIVE_TIME: Lazy = Lazy::new(|| { - env::var("SESSION_ACTIVE_TIME") - .ok() - .and_then(|value| value.parse().ok()) - .unwrap_or(600) -}); -const COOKIE_DOMAIN: Lazy = - Lazy::new(|| env::var("DOMAIN").ok().unwrap_or(".aaronhu.cn".to_owned())); -const LOGIN_PAGE_HTML: &str = include_str!("../loginpage.html"); +use config::{PORT}; #[tokio::main] async fn main() { // 初始化日志记录器 @@ -39,17 +25,15 @@ async fn main() { let session_layer = SessionManagerLayer::new(session_store) .with_secure(false) .with_expiry(Expiry::OnSessionEnd); - let pool = SqlitePool::connect(&env::var("DATABASE_URL").expect("DB URL NOT SPECIFIED")) - .await - .expect("DB OPEN FAILURE"); // our router - let state = Arc::new(ServerState { - db: pool, - session: HashMap::new().into(), - // started: std::time::Instant::now(), - }); + let state = Arc::new(ServerState::new().await); let app = Router::new() - .route("/auth", get(auth)) // http://127.0.0.1:3000 + .route("/auth", get(crate::services::auth_otp)) // http://127.0.0.1:3000 .route("/login", get(login_page).post(login)) + .route("/register_start/:username", post(auth::start_register)) + .route("/register_finish/:username", post(auth::finish_register)) + .route("/webauthn_login_start/:username", post(start_authentication)) + .route("/webauthn_login_finish/:username", post(finish_authentication)) + .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) @@ -58,11 +42,11 @@ async fn main() { .with_state(state.clone()) .layer(CookieManagerLayer::new()) .layer(session_layer); - let port = env::var("PORT").unwrap_or("3000".to_string()); - let port = port.parse::().unwrap_or(3000); + let aaronhu = Router::new().nest("/aaron/", app); + let port = PORT.parse::().unwrap_or(3000); let addr = SocketAddr::from(([127, 0, 0, 1], port)); // run it with hyper on localhost:3000 tokio::spawn(gc_task(state.clone())); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); + axum::serve(listener, aaronhu).await.unwrap(); } diff --git a/src/services/auth.rs b/src/services/auth.rs new file mode 100644 index 0000000..2e4646d --- /dev/null +++ b/src/services/auth.rs @@ -0,0 +1,413 @@ +#![allow(non_snake_case)] +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + +use axum::debug_handler; +use axum::extract::State; +use axum::{ + extract::{Json, Path}, + http::StatusCode, + response::IntoResponse, +}; +use std::time::Instant; +use tower_cookies::Cookies; +use tower_sessions::Session; +use tracing::*; + +/* + * Webauthn RS auth handlers. + * These files use webauthn to process the data received from each route, and are closely tied to axum + */ + +// 1. Import the prelude - this contains everything needed for the server to function. +use webauthn_rs::prelude::*; + +use crate::config::{COOKIE_NAME, SESSION_ACTIVE_TIME}; +use crate::ServerState; + +// 2. The first step a client (user) will carry out is requesting a credential to be +// registered. We need to provide a challenge for this. The work flow will be: +// +// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +// │ Authenticator │ │ Browser │ │ Site │ +// └───────────────┘ └───────────────┘ └───────────────┘ +// │ │ │ +// │ │ 1. Start Reg │ +// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ +// │ │ │ +// │ │ 2. Challenge │ +// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ +// │ │ │ +// │ 3. Select Token │ │ +// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ +// 4. Verify │ │ │ │ +// │ 4. Yield PubKey │ │ +// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │ +// │ │ │ +// │ │ 5. Send Reg Opts │ +// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─ +// │ │ │ │ 5. Verify +// │ │ │ PubKey +// │ │ │◀─ ─ ┘ +// │ │ │─ ─ ─ +// │ │ │ │ 6. Persist +// │ │ │ Credential +// │ │ │◀─ ─ ┘ +// │ │ │ +// │ │ │ +// +// In this step, we are responding to the start reg(istration) request, and providing +// the challenge to the browser. + +// TODO - Improve error handling and messages +fn auth_user(cookie_content:String,session_table:&mut HashMap) -> bool { + let Ok(uuid) = Uuid::parse_str(&cookie_content) else { + return false; + }; + let Some(expire) = session_table.get(&uuid) else { + return false; + }; + if *expire <= Instant::now() { + return false; + } + session_table.insert(uuid, Instant::now()+Duration::from_secs(*SESSION_ACTIVE_TIME)); + tracing::info!("valid cookie {}",uuid); + return true; + +} +#[debug_handler] +pub async fn start_register( + State(state): State>, + cookies: Cookies, + session: Session, + Path(username): Path, +) -> Result, String> { + tracing::info!("Start register"); + // todo!("Auth User"); + let Some(cookie_content) = cookies.get(&COOKIE_NAME) else { + return Err(WebauthnError::AuthenticationFailure.to_string()); + }; + let mut session_table = state.session.lock().await; + if !auth_user(cookie_content.value().to_string(), &mut session_table){ + return Err(WebauthnError::AuthenticationFailure.to_string()); + } + // Remove any previous registrations that may have occurred from the session. + let _ = session + .remove::<(String, Uuid, PasskeyRegistration)>("reg_state") + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + let pool = &state.db; + let username = username.clone(); + let (user_id, exclude_credentials): (Uuid, Option>) = + match sqlx::query!("SELECT key FROM users WHERE name = $1;", username) + .fetch_optional(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())? + { + Some(record) => { + let uid = record.KEY.clone(); + let records = sqlx::query!( + "SELECT credential FROM CREDENTIALS WHERE user_id = $1;", + uid + ) + .fetch_all(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + + ( + Uuid::parse_str(&record.KEY) + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?, + Some( + records + .iter() + .map(|record| serde_json::from_str::(&record.CREDENTIAL)) + .collect::, _>>() + .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())? + .iter() + .map(|passkey| passkey.cred_id().clone()) + .collect(), + ), + ) + } + None => (Uuid::new_v4(), None), + }; + + let res = match state.webauthn.start_passkey_registration( + user_id, + &username, + &username, + exclude_credentials, + ) { + Ok((ccr, reg_state)) => { + // Note that due to the session store in use being a server side memory store, this is + // safe to store the reg_state into the session since it is not client controlled and + // not open to replay attacks. If this was a cookie store, this would be UNSAFE. + session + .insert("reg_state", (username, user_id, reg_state)) + .await + .map_err(|_| WebauthnError::ChallengePersistenceError.to_string())?; + Json(ccr) + } + Err(e) => { + debug!("challenge_register -> {:?}", e); + return Err(WebauthnError::ChallengePersistenceError.to_string()); + } + }; + Ok(res) +} + +// 3. The browser has completed it's steps and the user has created a public key +// on their device. Now we have the registration options sent to us, and we need +// to verify these and persist them. + +pub async fn finish_register( + State(state): State>, + session: Session, + Json(reg): Json, +) -> Result { + info!("Confirming registration...."); + let pool = &state.db; + let Ok(Some((user_name, user_id, reg_state))) = session + .get::<(String, Uuid, PasskeyRegistration)>("reg_state") + .await + else { + return Err(WebauthnError::AuthenticationFailure.to_string()); //Corrupt Session + }; + + let _ = session.remove::<(Uuid, PasskeyAuthentication)>("reg_state").await; + + let res = match state.webauthn.finish_passkey_registration(®, ®_state) { + Ok(key) => { + info!("Passkey is okay"); + let uid = &user_id.to_string(); + let username = &user_name.to_string(); + // Check if the user_id already exists + let record = sqlx::query!( + "SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", + uid + ) + .fetch_one(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + + // If the user doesn't exist, insert them into the users table + if record.count == 0 + && sqlx::query!( + "INSERT INTO users(KEY, NAME) VALUES($1, $2);", + uid, + username + ) + .execute(pool) + .await + .map_err(|_| WebauthnError::UserNotPresent.to_string())? + .rows_affected() + != 1 + { + return Err(WebauthnError::AuthenticationFailure.to_string()); + } + + // Serialise the key + let serialised_key = serde_json::ser::to_string(&key) + .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; + + // Insert the key into the auth table + if sqlx::query!( + "INSERT INTO CREDENTIALS(user_id, credential) VALUES($1, $2);", + uid, + serialised_key + ) + .execute(pool) + .await + .map_err(|_| WebauthnError::UserNotPresent.to_string())? + .rows_affected() + != 1 + { + return Err(WebauthnError::AuthenticationFailure.to_string()); + } + + StatusCode::OK + } + Err(e) => { + debug!("challenge_register -> {:?}", e); + StatusCode::BAD_REQUEST + } + }; + + Ok(res) +} + +// 4. Now that our public key has been registered, we can authenticate a user and verify +// that they are the holder of that security token. The work flow is similar to registration. +// +// ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ +// │ Authenticator │ │ Browser │ │ Site │ +// └───────────────┘ └───────────────┘ └───────────────┘ +// │ │ │ +// │ │ 1. Start Auth │ +// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│ +// │ │ │ +// │ │ 2. Challenge │ +// │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ +// │ │ │ +// │ 3. Select Token │ │ +// ─ ─ ─│◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │ +// 4. Verify │ │ │ │ +// │ 4. Yield Sig │ │ +// └ ─ ─▶│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶ │ +// │ │ 5. Send Auth │ +// │ │ Opts │ +// │ │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─▶│─ ─ ─ +// │ │ │ │ 5. Verify +// │ │ │ Sig +// │ │ │◀─ ─ ┘ +// │ │ │ +// │ │ │ +// +// The user indicates the wish to start authentication and we need to provide a challenge. + +pub async fn start_authentication( + State(state): State>, + session: Session, + Path(user_name): Path, +) -> Result { + info!("Start Authentication"); + let pool = &state.db; + // Remove any previous authentication that may have occurred from the session. + let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state"); + let user_id = sqlx::query!("SELECT KEY FROM users WHERE NAME = $1;", user_name) + .fetch_one(pool) + .await + .map_err(|_| WebauthnError::UserNotPresent.to_string())? + .KEY; + + let records = sqlx::query!( + "SELECT credential FROM CREDENTIALS WHERE user_id = $1;", + user_id + ) + .fetch_all(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + + if records.is_empty() { + return Err(WebauthnError::CredentialNotFound.to_string()); + } + + let allow_credentials: Vec = records + .iter() + .map(|record| serde_json::de::from_str::(&record.CREDENTIAL)) + .collect::, _>>() + .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; + + let res = match state + .webauthn + .start_passkey_authentication(&allow_credentials) + { + Ok((rcr, auth_state)) => { + // Note that due to the session store in use being a server side memory store, this is + // safe to store the auth_state into the session since it is not client controlled and + // not open to replay attacks. If this was a cookie store, this would be UNSAFE. + session + .insert("auth_state", (&user_id, auth_state)) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + Json(rcr) + } + Err(e) => { + debug!("challenge_authenticate -> {:?}", e); + return Err(WebauthnError::MismatchedChallenge.to_string()); + } + }; + Ok(res) +} + +// 5. The browser and user have completed their part of the processing. Only in the +// case that the webauthn authenticate call returns Ok, is authentication considered +// a success. If the browser does not complete this call, or *any* error occurs, +// this is an authentication failure. + +pub async fn finish_authentication( + State(state): State>, + session: Session, + Json(auth): Json, +) -> Result { + let pool = &state.db; + let (user_id, auth_state): (Uuid, PasskeyAuthentication) = session + .get("auth_state") + .await + .unwrap() + .ok_or(WebauthnError::AuthenticationFailure.to_string())?; + + let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state"); + + let res = match state + .webauthn + .finish_passkey_authentication(&auth, &auth_state) + { + Ok(auth_result) => { + let uid = user_id.clone().to_string(); + let records = sqlx::query!( + "SELECT CREDENTIAL FROM CREDENTIALS WHERE USER_ID = $1;", + uid + ) + .fetch_all(pool) + .await + .map_err(|_| WebauthnError::AuthenticatorDataMissingExtension.to_string())?; + if records.is_empty() { + return Err(WebauthnError::UserNotPresent.to_string()); + } + + for record in records { + let mut credential = serde_json::from_str::(&record.CREDENTIAL) + .map_err(|_| WebauthnError::CredentialExistCheckError.to_string())?; + + if credential.cred_id() == auth_result.cred_id() { + credential.update_credential(&auth_result); + + let credential = serde_json::to_string(&credential) + .map_err(|_| WebauthnError::CredentialPersistenceError.to_string())?; + + if sqlx::query!( + "UPDATE CREDENTIALS SET credential = $1 WHERE user_id = $2 AND credential = $3;", + credential, + uid, + record.CREDENTIAL + ) + .execute(pool) + .await.is_ok_and(|e|e.rows_affected() !=1 ) + { + return Err(WebauthnError::AuthenticationFailure.to_string()); + } + + break; + } + } + + let user_name = sqlx::query!( + "SELECT NAME FROM users WHERE KEY = $1;", + uid + ) + .fetch_one(pool) + .await + .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; + // Add our own values to the session + session + .insert("user_id", user_id) + .await + .map_err(|_| WebauthnError::InvalidUserUniqueId.to_string())?; + session + .insert("user_name", user_name.NAME) + .await + .map_err(|_| WebauthnError::InvalidUsername.to_string())?; + + StatusCode::OK + } + Err(e) => { + debug!("challenge_register -> {:?}", e); + StatusCode::BAD_REQUEST + } + }; + + info!("Authentication Successful!"); + Ok(res) +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 90dc00b..4a1b0c2 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -1,22 +1,23 @@ use axum::extract::Query; use axum::http::{HeaderMap, HeaderValue}; use axum::response::{Html, Redirect}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Form, Router}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Form}; use minijinja::{context, Environment}; -use serde::Deserialize; -use sqlx::sqlite::SqlitePool; -use std::net::SocketAddr; + + + use std::sync::Arc; use std::time::{Duration, Instant}; -use std::{borrow::BorrowMut, env}; +use std::borrow::BorrowMut; use std::{collections::HashMap, str::FromStr}; -use tokio::sync::Mutex; -use tower_cookies::{Cookie, CookieManagerLayer, Cookies}; -use tower_http::trace::{self, TraceLayer}; -use tracing::Level; + +use tower_cookies::{Cookie, Cookies}; + + use uuid::Uuid; -use crate::{ServerState, UserLoginForm, COOKIE_DOMAIN, COOKIE_NAME, LOGIN_PAGE_HTML, SESSION_ACTIVE_TIME}; +use crate::{ServerState, UserLoginForm}; +use super::config::{COOKIE_DOMAIN, COOKIE_NAME, LOGIN_PAGE_HTML, SESSION_ACTIVE_TIME}; pub async fn gc_task(state: Arc) { @@ -32,7 +33,7 @@ pub async fn gc_task(state: Arc) { } // 处理/auth -pub async fn auth( +pub async fn auth_otp( State(state): State>, // Form(frm): Form, cookies: Cookies, @@ -69,9 +70,9 @@ pub async fn login( }; tracing::info!("{:?}", &frm); let target = sqlx::query_as::<_, UserLoginForm>( - r#" + r" SELECT NAME, KEY FROM USERS WHERE NAME = ? - "#, + ", ) .bind(frm.username) .fetch_optional(&mut *conn) @@ -86,7 +87,7 @@ pub async fn login( s, Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME), ); - let mut new_cookie = Cookie::new(&*COOKIE_NAME, s.to_string()); + let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), s.to_string()); new_cookie.set_domain(COOKIE_DOMAIN.to_string()); cookies.add(new_cookie); if let Some(original_uri) = params.get("original_url") { @@ -166,9 +167,9 @@ pub async fn login_with_passkey( }; tracing::info!("{:?}", &frm); let target = sqlx::query_as::<_, UserLoginForm>( - r#" + r" SELECT NAME, KEY FROM USERS WHERE NAME = ? - "#, + ", ) .bind(frm.username) .fetch_optional(&mut *conn) @@ -183,7 +184,7 @@ pub async fn login_with_passkey( s, Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME), ); - let mut new_cookie = Cookie::new(&*COOKIE_NAME, s.to_string()); + let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), s.to_string()); new_cookie.set_domain(COOKIE_DOMAIN.to_string()); cookies.add(new_cookie); if let Some(original_uri) = params.get("original_url") { @@ -199,3 +200,4 @@ pub async fn login_with_passkey( } +pub mod auth; \ No newline at end of file