From e494d71dc3c0e91890f054a95725dbad4badf020 Mon Sep 17 00:00:00 2001 From: mortis-0 <1190200714@stu.hit.edu.cn> Date: Sat, 16 Mar 2024 17:33:21 +0800 Subject: [PATCH] Refactor Done --- ...b2581417b471dc702d84faa06d3a80a73626.json} | 4 +- ...4efe82935f5f681c3cf54cbeaf845b70c7ff6.json | 12 - ...df7099dee9ac1243301baa5847cf91029178.json} | 6 +- ...e7f25415da7e916934e3cf983a9f590804a41.json | 20 -- ...e7bea86ba430d2e284631214751571b4f71f.json} | 4 +- ...1a3dd8d2d3805e314cb84d589713bc5ebd7df.json | 12 + ...e28064aa04f38e7d6e8c37f574196e0e6b98.json} | 4 +- ...e912f6ed52fd9cd1dbe902eaef02416a04ba.json} | 4 +- ...d597ffc097990591b3964eab20eb41bb7ff9.json} | 4 +- ...764a0965331c825faf0a86ca18331e4d8ebdf.json | 20 -- auth.db | Bin 32768 -> 40960 bytes example-config/rust-auth@3000.service | 19 ++ example-config/sites-enabled/acme | 14 + example-config/sites-enabled/headscale | 93 +++++++ example-config/sites-enabled/machine_status | 91 +++++++ example-config/sites-enabled/share | 86 ++++++ example-config/sites-enabled/sso | 38 +++ example-config/sites-enabled/vm | 114 ++++++++ src/config.rs | 3 +- src/controllers/mod.rs | 2 + src/controllers/otp_controllers.rs | 1 + src/controllers/passkey_login_controllers.rs | 156 +++++++++++ .../passkey_register_controller.rs | 0 src/dto/credential_mapper.rs | 75 ++++++ src/dto/mod.rs | 2 + src/dto/user_mapper.rs | 68 +++++ src/entities/mod.rs | 16 +- src/main.rs | 13 +- src/services/auth.rs | 254 ++---------------- src/services/cookie_services.rs | 19 ++ src/services/gc_services.rs | 30 +++ src/services/mod.rs | 40 +-- 32 files changed, 877 insertions(+), 347 deletions(-) rename .sqlx/{query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json => query-02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626.json} (62%) delete mode 100644 .sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json rename .sqlx/{query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json => query-13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178.json} (58%) delete mode 100644 .sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json rename .sqlx/{query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json => query-5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f.json} (53%) create mode 100644 .sqlx/query-765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df.json rename .sqlx/{query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json => query-bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98.json} (64%) rename .sqlx/{query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json => query-c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba.json} (65%) rename .sqlx/{query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json => query-ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9.json} (50%) delete mode 100644 .sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json create mode 100644 example-config/rust-auth@3000.service create mode 100644 example-config/sites-enabled/acme create mode 100644 example-config/sites-enabled/headscale create mode 100644 example-config/sites-enabled/machine_status create mode 100644 example-config/sites-enabled/share create mode 100644 example-config/sites-enabled/sso create mode 100644 example-config/sites-enabled/vm create mode 100644 src/controllers/otp_controllers.rs create mode 100644 src/controllers/passkey_login_controllers.rs create mode 100644 src/controllers/passkey_register_controller.rs create mode 100644 src/dto/credential_mapper.rs create mode 100644 src/dto/mod.rs create mode 100644 src/dto/user_mapper.rs create mode 100644 src/services/cookie_services.rs create mode 100644 src/services/gc_services.rs diff --git a/.sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json b/.sqlx/query-02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626.json similarity index 62% rename from .sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json rename to .sqlx/query-02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626.json index 0ea765a..78ec400 100644 --- a/.sqlx/query-db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731.json +++ b/.sqlx/query-02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT COUNT(KEY) AS count FROM users WHERE KEY = $1;", + "query": "SELECT COUNT(id) AS count FROM users WHERE id = $1;", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "db45619d7a6d50b845ed8f31f58b525ae6b4de88eba24b0d4126ba54d07b0731" + "hash": "02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626" } diff --git a/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json b/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json deleted file mode 100644 index f503a0d..0000000 --- a/.sqlx/query-0e032978b712ce5ad586ab15bdc4efe82935f5f681c3cf54cbeaf845b70c7ff6.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "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-13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178.json similarity index 58% rename from .sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json rename to .sqlx/query-13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178.json index 5d1cdec..323a2f4 100644 --- a/.sqlx/query-1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810.json +++ b/.sqlx/query-13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178.json @@ -1,10 +1,10 @@ { "db_name": "SQLite", - "query": "SELECT key FROM users WHERE name = $1;", + "query": "SELECT id FROM users WHERE name = $1;", "describe": { "columns": [ { - "name": "KEY", + "name": "ID", "ordinal": 0, "type_info": "Text" } @@ -16,5 +16,5 @@ false ] }, - "hash": "1bb4a0ea9450533e5d08db156a7440258c22cc6d14848d247a322a1ff2c73810" + "hash": "13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178" } diff --git a/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json b/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json deleted file mode 100644 index 7f11ffe..0000000 --- a/.sqlx/query-390c853d2fe8777bebbbe3ed5dae7f25415da7e916934e3cf983a9f590804a41.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json b/.sqlx/query-5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f.json similarity index 53% rename from .sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json rename to .sqlx/query-5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f.json index de3d125..527ff76 100644 --- a/.sqlx/query-810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc.json +++ b/.sqlx/query-5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "UPDATE CREDENTIALS SET credential = $1 WHERE user_id = $2 AND credential = $3;", + "query": "UPDATE credentials SET credential = $1 WHERE user_id = $2 AND credential = $3;", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "810f3f513dd59e14557979e50b72b0b40fa6a7ea682a74a806ebf2be30c6e4cc" + "hash": "5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f" } diff --git a/.sqlx/query-765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df.json b/.sqlx/query-765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df.json new file mode 100644 index 0000000..d5935e6 --- /dev/null +++ b/.sqlx/query-765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO users(id, name) VALUES($1, $2);", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df" +} diff --git a/.sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json b/.sqlx/query-bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98.json similarity index 64% rename from .sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json rename to .sqlx/query-bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98.json index deb3f2e..ae7a7a3 100644 --- a/.sqlx/query-1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21.json +++ b/.sqlx/query-bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT CREDENTIAL FROM CREDENTIALS WHERE USER_ID = $1;", + "query": "SELECT credential FROM credentials WHERE user_id = $1;", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "1ea5285096550f447c4ec8eed2aaf623fe2887015ea22f1579f477bc1aeb7a21" + "hash": "bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98" } diff --git a/.sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json b/.sqlx/query-c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba.json similarity index 65% rename from .sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json rename to .sqlx/query-c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba.json index 82161e4..59b9422 100644 --- a/.sqlx/query-e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801.json +++ b/.sqlx/query-c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT NAME FROM users WHERE KEY = $1;", + "query": "SELECT name FROM users WHERE id = $1;", "describe": { "columns": [ { @@ -16,5 +16,5 @@ false ] }, - "hash": "e8b97d3910dbfb9ea1dd5d6d8a105886df0573e55c9ac62ac1cc8ecd15ae9801" + "hash": "c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba" } diff --git a/.sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json b/.sqlx/query-ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9.json similarity index 50% rename from .sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json rename to .sqlx/query-ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9.json index 30d5f40..1c193ca 100644 --- a/.sqlx/query-62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc.json +++ b/.sqlx/query-ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO CREDENTIALS(user_id, credential) VALUES($1, $2);", + "query": "INSERT INTO credentials(user_id, credential) VALUES($1, $2);", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "62612ca2817b12ef492a450be661ad58037c7d5b433cba9855fff21461b9e7fc" + "hash": "ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9" } diff --git a/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json b/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json deleted file mode 100644 index e115a62..0000000 --- a/.sqlx/query-f1948f31683e327633ad9ae6b56764a0965331c825faf0a86ca18331e4d8ebdf.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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/auth.db b/auth.db index 22b571f96f9553896b631974cc71afcbbd9cf505..cf89c7b08f0f1dbb281e9fa7252f1acd3d7d4ad1 100644 GIT binary patch literal 40960 zcmeI4U5n$^8OQCJi8s5Oi3@>Dp-t;LknXahRPUA-3bkd~BU_ei$+E`|MM$#zZdulg z-^vg|Hz9?7gI=^3fkHn)FZuy`QwseErI)!2z3;JiCOb2k4HI@ZwCN)wOXpQbzw>_{ zaXgwwt4qVsN%dy_)C^Np?0d0zJobxJDi({$XupT{1lsq}{xh^6p#5TtUz~i+QI5T@ zoE^Sl#}fBHjJ+u*!^20(AK(A*@GslX{eOK;ODF;YKmZ5;0U!VbfB+Bx0^dCXADKV6 zbM)@J@n7E!O>5%ts!$N*hM1GIOQ-wjJl7CXja*(5Qt3-2>C}V6d#TG==~P2FZKPxc z?Ybl#?}~LzPz|w=PKC}YyjoUM#cED%rz%1__25$1>4&?fydrB2H7ClARC?P%QBei4 zB%fEm==Wht6^eo?$h@GX(%W1OjcnWC;o-f9ht1euzY8<>@F;Qg^P_ld=ysh|FrT1x zlVL{TY?n56cWn^6!)u3M#Cos(mYE+Wj>LDLH=s*0F^Efv*Wdg@?1NV{xY~Z==+UEi zdne{|M1eDp9M^WPB@eEM`dZ-w*PJ@XsO3X6&<|hHU~ki{_Wi`sPmbbeyE&F~RlyMD zf^fQ<KDeD{C#XMDaVp>@=vz`0(F1@5C8%|00;m9 zAOHk_01yBIKmZ5;0VRH*zooMz%ds>~VKkM^VziZ^G1Kbe7;ceS%Qo!{(WTdubuVjj zRE8ulI?Lb~P181JnGS<71kMo|$D}RJMo90UV{Pmm=5B3X{wiXIF6zYQSXV^BzCyC z_xbBq;_dxEqr@xPJ`ocKzg6^KrETBorXN2&8bfrN4#j9C*Ee)=bAq2bguIytVa0YU z8E?_rl=yz=kFesTB=L4Nef(wnaP2t;YOQJ{!=n7Lv)*>AYPs#fMJ1cMS+&by{gmXU zjc8{3b)NNlwW^7!g%Le1&z1bVzRA^w8C7X=Yqqga0%{euGgemfMS(r(gn}G(*CL+p zRT`PqDW_}RRO#2IRnjkHM!N44-BCPJbAnj>F;1HN4qm zXrq}VKfL5X72B;Wa^;}t=KMNf@L>1*(sBV9T48mRA!PK-3BtmorBXH2k z4RuV<+bQ!})2(z3)Uk&ylNS5#Yvzr2++Ejo(AcII-6Zjf3HWs5EM^owj^qHC&yzMQ#%fCbr1ptE}GTJ9f~mj7YXqwIf9e zc)HLug7j5&;h?S;R@ZHwAWy`DgHLbv)LbxYQI%!b859dkEEt%DQAzjI(sa3<-WOI= zr8OybhLWRTK}S0EhW=(wmpbTAVyE?GF(Vi(flcI`;LPeE(P{95AybEmLz>EtLl)D#JDxOwQJ_mHL9O)~bA)bjuw~s;%^} zRgr9URL)t7uXCk9IT4Hc%g|viv(gt~e@zfsWjt@=!=}E*^=UchjnGqDiB3F9s=7YT z7~;7O$6xzyQ25OnnPISH2 z%0%lz)-Gx5WpyJ3S~K4t_cPRTB!q14We8E0q)c{P^D300k!-8fC5u{ERxL_)#o9W= z^e9ItLf34s@N*$zeJPq!i%?aJ9=2NKSU-}g^G1&tY(y$>H4$qv@=#fGx$6#*VnK=D zk`m+3Q=*BpH4l?Tv83TyV(5p?X4H1X1;!Ixevprd4iogL&3HQHtvNH!jIi}cAxXvL zoSv3f00e*l5crk}JlNOc zw|UcU@2C6v-P^nfg~osJUj^s4vWJe_SbulVkK1$DfB+Bx0zd!=00AHX1b_e#00KY& z2mpca2!ZWFe=qrYEcpfcf(-}&0U!VbfB+Bx0zd!=00AHX1b_e#_@5_`*njITiaYqm z{yXuD5P^H!^?xER#15v3KfeC=!{6O!Py)7ZM&Oh8P+Y?}#5IJthR@;`LR`bI#5GJj z;ZV%N8|?&Q7J_n~MbP|;a$bA#9*ReJqjDe~;Wlf00e*l5C8%|00;m9AOHk_01yBIKmZ5;0U!VbfB+Bx0zd!={5KN#4;24I A=l}o! delta 505 zcmZoTz|_#dG(lQWl7WGN1Blsym}7{YDI~Lk%5VYrh=b;h=O0JkB<&WO=z%dP`sxLRDq#^kqMH5$$zyzx5aj9W z7!;`h)TN*S)~l)D@2BAG@8{;@=^UaE1TD_Q`MT0q%5o9wf LlR9%KC&(lK*=cv1 diff --git a/example-config/rust-auth@3000.service b/example-config/rust-auth@3000.service new file mode 100644 index 0000000..b1b8d35 --- /dev/null +++ b/example-config/rust-auth@3000.service @@ -0,0 +1,19 @@ +[Unit] +Description=Rust Auth Service for Port %I +After=network.target + +[Service] +ExecStart=/usr/local/bin/rust-auth +Environment="RUST_LOG=debug" +# Cookie 所允许的域名 +# Environment="DOMAIN=.aaronhu.cn" +# 部署在哪个子路由上? +Environment="HOME_URL=/aaron" +Environment="RP_ORIGIN=https://sso.aaronhu.cn" +Environment="RP_NAME=Huyunfan Auth" +Environment="RP_ID=aaronhu.cn" +Environment="DATABASE_URL=/var/auth/auth.db" +Environment="PORT=%I" + +[Install] +WantedBy=default.target diff --git a/example-config/sites-enabled/acme b/example-config/sites-enabled/acme new file mode 100644 index 0000000..72d2f29 --- /dev/null +++ b/example-config/sites-enabled/acme @@ -0,0 +1,14 @@ +server { + listen 80; + server_name _; # 通配符,表示匹配所有服务器名 + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/letsencrypt; # 确保这里的路径指向您存放ACME挑战文件的目录 + } + # Redirect all other requests to HTTPS + location / { + return 301 https://$host$request_uri; + } + # 保留现有的其它配置 ... +} diff --git a/example-config/sites-enabled/headscale b/example-config/sites-enabled/headscale new file mode 100644 index 0000000..890b20c --- /dev/null +++ b/example-config/sites-enabled/headscale @@ -0,0 +1,93 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# https://www.nginx.com/resources/wiki/start/ +# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ +# https://wiki.debian.org/Nginx/DirectoryStructure +# +# In most cases, administrators will remove this file from sites-enabled/ and +# leave it as reference inside of sites-available where it will continue to be +# updated by the nginx packaging team. +# +# This file will automatically load configuration files provided by other +# applications, such as Drupal or Wordpress. These applications will be made +# available underneath a path with that package name, such as /drupal8. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +map $http_upgrade $connection_upgrade { + default keep-alive; + 'websocket' upgrade; + '' close; +} +server { + listen 80; + server_name derp.aaronhu.cn; + location / { + return 301 https://$host$request_uri; + } +} +server { + # HEADSCALE + + # SSL configuration + # + listen 443 ssl http2; + server_name derp.aaronhu.cn; + ssl_certificate /root/.acme.sh/derp.aaronhu.cn_ecc/fullchain.cer; + #请填写私钥文件的相对路径或绝对路径 + ssl_certificate_key /root/.acme.sh/derp.aaronhu.cn_ecc/derp.aaronhu.cn.key; + ssl_session_timeout 5m; + #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + #请按照以下协议配置 + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + root /var/www/derp.aaronhu.cn; + } + # [Mon Nov 13 11:52:44 PM CST 2023] Your cert is in: /root/.acme.sh/derp.aaronhu.cn_ecc/derp.aaronhu.cn.cer + # [Mon Nov 13 11:52:44 PM CST 2023] Your cert key is in: /root/.acme.sh/derp.aaronhu.cn_ecc/derp.aaronhu.cn.key + # [Mon Nov 13 11:52:44 PM CST 2023] The intermediate CA cert is in: /root/.acme.sh/derp.aaronhu.cn_ecc/ca.cer + # [Mon Nov 13 11:52:44 PM CST 2023] And the full chain certs is there: /root/.acme.sh/derp.aaronhu.cn_ecc/fullchain.cer + + location / { + proxy_pass http://127.0.0.1:18080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $server_name; + proxy_redirect http:// https://; + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; + } + +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/example-config/sites-enabled/machine_status b/example-config/sites-enabled/machine_status new file mode 100644 index 0000000..6a3d4e9 --- /dev/null +++ b/example-config/sites-enabled/machine_status @@ -0,0 +1,91 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# https://www.nginx.com/resources/wiki/start/ +# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ +# https://wiki.debian.org/Nginx/DirectoryStructure +# +# In most cases, administrators will remove this file from sites-enabled/ and +# leave it as reference inside of sites-available where it will continue to be +# updated by the nginx packaging team. +# +# This file will automatically load configuration files provided by other +# applications, such as Drupal or Wordpress. These applications will be made +# available underneath a path with that package name, such as /drupal8. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +server { + listen 8899 default_server; + listen [::]:8899 default_server; + + root /var/www/machine_status; +} + +server{ + set $RUST_AUTH_HOME "/aaron"; + listen 443 ssl; + listen [::]:443 ssl; + server_name alive.aaronhu.cn; + if ($host != "alive.aaronhu.cn") { + return 404; + } + ssl_certificate /root/.acme.sh/alive.aaronhu.cn_ecc/fullchain.cer; + ssl_certificate_key /root/.acme.sh/alive.aaronhu.cn_ecc/alive.aaronhu.cn.key; + ssl_session_timeout 5m; + #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + #请按照以下协议配置 + ssl_protocols TLSv1.2 TLSv1.3; + + location / { + auth_request /auth; + set $original_full_url $scheme://$host$request_uri; + error_page 401 = @error401; + proxy_set_header X-Original-URI $scheme://$host$request_uri; + proxy_pass http://localhost:8083; + } + location /getall/alive/{ + proxy_pass http://11.11.11.1:5412/alive; + } + location /pic/{ + proxy_pass http://11.11.11.1:8899/; + } + location @error401 { + add_header Set-Cookie "OriginalURL=$scheme://$host$request_uri; Domain=.aaronhu.cn; Path=/aaron; Secure; HttpOnly; Max-Age=120"; + return 302 https://sso.aaronhu.cn/aaron/login; + } + location = /auth { + internal; + proxy_pass http://localhost:3000/aaron/auth; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; #可用来控制权限 + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + } + +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/example-config/sites-enabled/share b/example-config/sites-enabled/share new file mode 100644 index 0000000..cb34c8e --- /dev/null +++ b/example-config/sites-enabled/share @@ -0,0 +1,86 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# https://www.nginx.com/resources/wiki/start/ +# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ +# https://wiki.debian.org/Nginx/DirectoryStructure +# +# In most cases, administrators will remove this file from sites-enabled/ and +# leave it as reference inside of sites-available where it will continue to be +# updated by the nginx packaging team. +# +# This file will automatically load configuration files provided by other +# applications, such as Drupal or Wordpress. These applications will be made +# available underneath a path with that package name, such as /drupal8. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +#server { +# listen 8899 default_server; +# listen [::]:8899 default_server; +# +# root /var/www/machine_status; +#} + +server{ + listen 443 ssl; + listen [::]:443 ssl; + server_name share.aaronhu.cn; + if ($host != "share.aaronhu.cn") { + return 404; + } + client_max_body_size 3072M; # put the size that is enough + ssl_certificate /root/.acme.sh/share.aaronhu.cn_ecc/share.aaronhu.cn.cer; + ssl_certificate_key /root/.acme.sh/share.aaronhu.cn_ecc/share.aaronhu.cn.key; + ssl_session_timeout 5m; + #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + #请按照以下协议配置 + ssl_protocols TLSv1.2 TLSv1.3; + + location / { + client_max_body_size 3072M; # put the size that is enough + auth_request /auth; + set $original_full_url $scheme://$host$request_uri; + error_page 401 = @error401; + proxy_set_header X-Original-URI $scheme://$host$request_uri; + proxy_pass http://localhost:8080; + } + location = /auth { + internal; + proxy_pass http://localhost:3000/aaron/auth; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; #可用来控制权限 + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + } + location @error401 { + add_header Set-Cookie "OriginalURL=$scheme://$host$request_uri; Domain=.aaronhu.cn; Path=/aaron; Secure; HttpOnly; Max-Age=120"; + return 302 https://sso.aaronhu.cn/aaron/login; + } + +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/example-config/sites-enabled/sso b/example-config/sites-enabled/sso new file mode 100644 index 0000000..1487de8 --- /dev/null +++ b/example-config/sites-enabled/sso @@ -0,0 +1,38 @@ + +server{ + listen 443 ssl; + listen [::]:443 ssl; + server_name sso.aaronhu.cn; + if ($host != "sso.aaronhu.cn") { + return 404; + } + ssl_certificate /root/.acme.sh/sso.aaronhu.cn_ecc/sso.aaronhu.cn.cer; + ssl_certificate_key /root/.acme.sh/sso.aaronhu.cn_ecc/sso.aaronhu.cn.key; + ssl_session_timeout 5m; + #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + #请按照以下协议配置 + ssl_protocols TLSv1.2 TLSv1.3; + + location = /aaron/auth { + internal; + proxy_pass http://localhost:3000/aaron/auth; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; #可用来控制权限 + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + } + location /aaron { + proxy_pass http://localhost:3000/aaron; + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + proxy_set_header X-Original-URI $original_full_url; + } + location / { + proxy_pass http://localhost:3000; + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + proxy_set_header X-Original-URI $original_full_url; + } +} diff --git a/example-config/sites-enabled/vm b/example-config/sites-enabled/vm new file mode 100644 index 0000000..0e3827d --- /dev/null +++ b/example-config/sites-enabled/vm @@ -0,0 +1,114 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# https://www.nginx.com/resources/wiki/start/ +# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/ +# https://wiki.debian.org/Nginx/DirectoryStructure +# +# In most cases, administrators will remove this file from sites-enabled/ and +# leave it as reference inside of sites-available where it will continue to be +# updated by the nginx packaging team. +# +# This file will automatically load configuration files provided by other +# applications, such as Drupal or Wordpress. These applications will be made +# available underneath a path with that package name, such as /drupal8. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +#server { +# listen 8899 default_server; +# listen [::]:8899 default_server; +# +# root /var/www/machine_status; +#} + +map $http_upgrade $connection_upgrade { + default keep-alive; + 'websocket' upgrade; + '' close; +} +server{ + listen 443 ssl; + listen [::]:443 ssl; + server_name vm.aaronhu.cn; + if ($host != "vm.aaronhu.cn") { + return 404; + } + ssl_certificate /root/.acme.sh/vm.aaronhu.cn_ecc/vm.aaronhu.cn.cer; + ssl_certificate_key /root/.acme.sh/vm.aaronhu.cn_ecc/vm.aaronhu.cn.key; + ssl_session_timeout 5m; + #请按照以下套件配置,配置加密套件,写法遵循 openssl 标准。 + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; + #请按照以下协议配置 + ssl_protocols TLSv1.2 TLSv1.3; + + location / { + auth_request /auth; + set $original_full_url $scheme://$host$request_uri; + error_page 401 = @error401; + proxy_set_header X-Original-URI $scheme://$host$request_uri; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $server_name; + proxy_redirect http:// https://; + proxy_buffering off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_pass https://11.11.11.50:9090; + proxy_ssl_verify off; + proxy_ssl_verify_depth 0; + } + location @error401 { + add_header Set-Cookie "OriginalURL=$scheme://$host$request_uri; Domain=.aaronhu.cn; Path=/aaron; Secure; HttpOnly; Max-Age=120"; + return 302 https://sso.aaronhu.cn/aaron/login; + } + location = /auth { + internal; + proxy_pass http://localhost:3000/aaron/auth; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; #可用来控制权限 + proxy_set_header X-Original-Remote-Addr $remote_addr; + proxy_set_header X-Original-Host $host; + } + # location /shellinabox/ { + # auth_request /aaron/auth; + # error_page 401 =200 /login; + # proxy_pass http://localhost:4200/; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # # 可选:如果你的 Shell In A Box 服务中使用了 WebSocket,请添加以下配置 + # proxy_http_version 1.1; + # proxy_set_header Upgrade $http_upgrade; + # proxy_set_header Connection "upgrade"; + # } + +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} diff --git a/src/config.rs b/src/config.rs index 45d73da..fdddf2e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,8 +18,7 @@ pub static HOME_URL: Lazy = Lazy::new(|| env::var("HOME_URL").unwrap_or("/aaron".to_owned())); pub static DATABASE_URL: Lazy = Lazy::new(|| env::var("DATABASE_URL").unwrap_or("".to_owned())); -pub static RP_ID: Lazy = - Lazy::new(|| env::var("RP_ID").unwrap_or("localhost".to_owned())); +pub static RP_ID: Lazy = Lazy::new(|| env::var("RP_ID").unwrap_or("localhost".to_owned())); pub static RP_ORIGIN: Lazy = Lazy::new(|| env::var("RP_ORIGIN").unwrap_or("http://localhost:8080".to_owned())); pub static RP_NAME: Lazy = diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index e69de29..44d00a1 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -0,0 +1,2 @@ +pub mod otp_controllers; +pub mod passkey_login_controllers; diff --git a/src/controllers/otp_controllers.rs b/src/controllers/otp_controllers.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/controllers/otp_controllers.rs @@ -0,0 +1 @@ + diff --git a/src/controllers/passkey_login_controllers.rs b/src/controllers/passkey_login_controllers.rs new file mode 100644 index 0000000..19160e9 --- /dev/null +++ b/src/controllers/passkey_login_controllers.rs @@ -0,0 +1,156 @@ +// 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. + +use axum::http::StatusCode; +use tower_cookies::Cookie; +use axum::response::Redirect; +use tracing::{debug, info}; +use uuid::Uuid; +use webauthn_rs::prelude::{Passkey, PasskeyAuthentication, PublicKeyCredential, WebauthnError}; +use crate::dto::credential_mapper::update_credential_on_success; +use crate::dto::user_mapper::get_username_by_id; +use crate::services::cookie_services::create_cookie_session; +use crate::config::{COOKIE_DOMAIN, SESSION_ACTIVE_TIME}; +use std::time::{Duration, Instant}; +use axum::response::IntoResponse; +use axum::extract::Json; +use tower_cookies::Cookies; +use tower_sessions::Session; +use crate::ServerState; +use std::sync::Arc; +use axum::extract::State; +use crate::dto::credential_mapper::get_credential_from_uid; +use crate::dto::user_mapper::get_uid_by_name; +use axum::extract::Path; + +pub async fn start_authentication( + State(state): State>, + session: Session, + Path(user_name): Path, +) -> Result { + info!("开始passkey 验证"); + let pool = &state.db; + // Remove any previous authentication that may have occurred from the session. + let _ = session + .remove::<(Uuid, PasskeyAuthentication)>("auth_state") + .await; + let user_id = get_uid_by_name(user_name, pool).await?; + let available_creds = get_credential_from_uid(user_id, pool).await?; + + if available_creds.is_empty() { + return Err(WebauthnError::CredentialNotFound.to_string()); + } + + let allow_credentials: Vec = available_creds; + + 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) +} + +pub async fn finish_authentication( + State(state): State>, + session: Session, + cookies: Cookies, + 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())?; + info!("已获得认证状态:uid:{}", user_id); + let _ = session + .remove::<(Uuid, PasskeyAuthentication)>("auth_state") + .await; + + let res = match state + .webauthn + .finish_passkey_authentication(&auth, &auth_state) + { + Ok(auth_result) => { + info!("passkey认证通过:uid:{}", user_id); + let passkeys = crate::dto::credential_mapper::get_credential_from_uid(user_id, pool) + .await + .map_err(|_| WebauthnError::AuthenticatorDataMissingExtension.to_string())?; + if passkeys.is_empty() { + return Err(WebauthnError::UserNotPresent.to_string()); + } + + for passkey in passkeys { + let mut credential = passkey.clone(); + + if credential.cred_id() == auth_result.cred_id() { + credential.update_credential(&auth_result); + + match update_credential_on_success(credential, user_id, passkey, pool).await { + Ok(_) => break, + Err(x) => return Err(x), + } + } + } + + let user_name = get_username_by_id(user_id, pool).await?; + // 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) + .await + .map_err(|_| WebauthnError::InvalidUsername.to_string())?; + let mut lock = state.session.lock().await; + let uuid = Uuid::new_v4(); + let expires = Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME); + lock.insert(uuid, expires); + + let original_uri = cookies.get("OriginalURL"); + let new_cookie = create_cookie_session(uuid); + cookies.add(new_cookie); + // 从Cookie中恢复重定向信息 + match original_uri { + Some(redirect) => return Ok(Redirect::to(redirect.value())), + _ => (), + }; + // 处理完成重定向后,清除Cookie + cookies.remove(Cookie::new("OriginalURL", "")); + info!( + "Passkey登录成功,设置Cookie for {}", + COOKIE_DOMAIN.to_string() + ); + info!( + "从passkey登录创建了新Session{},过期时间{}s后", + uuid, *SESSION_ACTIVE_TIME + ); + StatusCode::OK + } + Err(e) => { + debug!("challenge_register -> {:?}", e); + StatusCode::BAD_REQUEST + } + }; + + info!("Authentication Successful!"); + Err(res.to_string()) +} diff --git a/src/controllers/passkey_register_controller.rs b/src/controllers/passkey_register_controller.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/dto/credential_mapper.rs b/src/dto/credential_mapper.rs new file mode 100644 index 0000000..345cda0 --- /dev/null +++ b/src/dto/credential_mapper.rs @@ -0,0 +1,75 @@ +#![allow(non_snake_case)] + +use uuid::Uuid; + +use sqlx::Pool; + +use sqlx::Sqlite; + +use webauthn_rs::prelude::Passkey; +use webauthn_rs::prelude::WebauthnError; + +pub async fn get_credential_from_uid( + uid: Uuid, + pool: &Pool, +) -> Result, String> { + let uid = uid.to_string(); + let result = sqlx::query!( + "SELECT credential FROM credentials WHERE user_id = $1;", + uid + ) + .fetch_all(pool) + .await + .map_err(|e| e.to_string())?; + result + .into_iter() + .map(|e| serde_json::from_str::(&e.CREDENTIAL).map_err(|e| e.to_string())) + .collect() +} +pub async fn update_credential_on_success( + new_cred: Passkey, + uid: Uuid, + old_cred: Passkey, + pool: &sqlx::Pool, +) -> Result { + let new_cred_str = + serde_json::to_string(&new_cred).map_err(|_| "Cannot Serialize new passkey")?; + let old_cred_str = + serde_json::to_string(&old_cred).map_err(|_| "Cannot Serialize old passkey")?; + let uid = uid.to_string(); + if sqlx::query!( + "UPDATE credentials SET credential = $1 WHERE user_id = $2 AND credential = $3;", + new_cred_str, + uid, + old_cred_str + ) + .execute(pool) + .await + .is_ok_and(|e| e.rows_affected() != 1) + { + return Err(WebauthnError::AuthenticationFailure.to_string()); + } + Ok("Successful Operation update_credential_on_success".to_owned()) +} + +/// Return Rows Affected +pub async fn add_credential_by_id( + uid: Uuid, + cred: &Passkey, + pool: &sqlx::Pool, +) -> Result { + let uid = uid.to_string(); + // Serialise the key + let serialised_key = + serde_json::ser::to_string(&cred).map_err(|_| "Key Serialisation failed")?; + let res = 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(); + Ok(res) +} diff --git a/src/dto/mod.rs b/src/dto/mod.rs new file mode 100644 index 0000000..8506457 --- /dev/null +++ b/src/dto/mod.rs @@ -0,0 +1,2 @@ +pub mod credential_mapper; +pub mod user_mapper; diff --git a/src/dto/user_mapper.rs b/src/dto/user_mapper.rs new file mode 100644 index 0000000..87b1a35 --- /dev/null +++ b/src/dto/user_mapper.rs @@ -0,0 +1,68 @@ +#![allow(non_snake_case)] +use sqlx::Acquire; +use uuid::Uuid; + +use sqlx::Pool; + +use sqlx::Sqlite; + +pub async fn get_username_by_id(id: Uuid, pool: &Pool) -> Result { + let id_str = id.to_string(); + let res = sqlx::query!("SELECT name FROM users WHERE id = $1;", id_str) + .fetch_one(pool) + .await + .map_err(|_| format!("Cannot find username from {}", id))?; + Ok(res.NAME) +} +pub async fn get_uid_by_name(name: String, pool: &Pool) -> Result { + let res = sqlx::query!("SELECT id FROM users WHERE name = $1;", name) + .fetch_optional(pool) + .await + .map_err(|_| format!("Cannot find Uid from {}", name))?; + match res { + Some(id) => { + Ok(Uuid::parse_str(&id.ID) + .map_err(|_| "Cannot parse uid string to uuid".to_string())?) + } + None => Err("cannot get uid from name".to_string()), + } +} + +pub async fn get_user_count_by_uid(id: Uuid, pool: &Pool) -> Result { + let id = id.to_string(); + let record = sqlx::query!("SELECT COUNT(id) AS count FROM users WHERE id = $1;", id) + .fetch_one(pool) + .await + .map_err(|_| "Error in db while checking usercount")?; + return Ok(record.count); +} + +/// Return rows affected and error reason +pub async fn create_user_if_non_existent( + id: Uuid, + name: String, + pool: &Pool, +) -> Result { + let uid = id.to_string(); + let mut tx = pool + .begin() + .await + .map_err(|_| "cannot start DB transaction")?; + let record = sqlx::query!("SELECT COUNT(id) AS count FROM users WHERE id = $1;", uid) + .fetch_one(&mut *tx) + .await + .map_err(|_| "Error in db while checking usercount")?; + let err_msg = "Transaction Failed".to_string(); + if record.count == 0 { + let res = sqlx::query!("INSERT INTO users(id, name) VALUES($1, $2);", uid, name) + .execute(&mut *tx) + .await + .map_err(|_| "error from db when adding user".to_string())? + .rows_affected(); + let _ = tx.commit().await.map_err(|_| err_msg)?; + Ok(res) + } else { + let _ = tx.rollback().await.map_err(|_| err_msg.to_string())?; + Err(err_msg) + } +} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index 7db862b..4a3466e 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -1,14 +1,11 @@ +use crate::config::{DATABASE_URL, RP_ID, RP_NAME, RP_ORIGIN}; use serde::Deserialize; use sqlx::SqlitePool; -use std::collections::HashMap; -use std::sync::Arc; use std::time::Instant; +use std::{collections::HashMap, sync::Arc}; 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}; +use webauthn_rs::{prelude::Url, Webauthn, WebauthnBuilder}; pub struct ServerState { pub db: sqlx::Pool, @@ -51,7 +48,7 @@ impl ServerState { pub struct UserLoginForm { #[sqlx(rename = "NAME")] pub username: String, - #[sqlx(rename = "KEY")] + #[sqlx(rename = "OTP_KEY")] pub otp: String, } @@ -59,6 +56,9 @@ pub struct UserLoginForm { pub struct User { #[sqlx(rename = "NAME")] pub username: String, - #[sqlx(rename = "KEY")] + #[sqlx(rename = "ID")] pub uid: String, + #[sqlx(rename = "OTP_KEY")] + pub otp_key: String, + } diff --git a/src/main.rs b/src/main.rs index d7d2cd6..f0a06bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ use axum::{ }; use entities::*; +use controllers::passkey_login_controllers::{start_authentication, finish_authentication}; use services::{ - auth::{self, finish_authentication, start_authentication}, - gc_task, login, login_page, register_page, + auth::start_register, + gc_services::gc_task, + login, login_page, register_page, }; use std::net::SocketAddr; use std::sync::Arc; @@ -17,9 +19,10 @@ use tracing::Level; pub mod config; pub mod controllers; +pub mod dto; pub mod entities; pub mod services; -use crate::config::*; +use crate::{config::*, services::auth::finish_register}; #[tokio::main] async fn main() { // 初始化日志记录器 @@ -33,8 +36,8 @@ async fn main() { .route("/auth", get(crate::services::auth_otp)) // http://127.0.0.1:3000 .route("/login", get(login_page).post(login)) .route("/register", get(register_page)) - .route("/register_start/:username", post(auth::start_register)) - .route("/register_finish", post(auth::finish_register)) + .route("/register_start/:username", post(start_register)) + .route("/register_finish", post(finish_register)) .route( "/webauthn_login_start/:username", post(start_authentication), diff --git a/src/services/auth.rs b/src/services/auth.rs index e830d8a..427edf1 100644 --- a/src/services/auth.rs +++ b/src/services/auth.rs @@ -1,18 +1,15 @@ -#![allow(non_snake_case)] use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use axum::extract::State; -use axum::response::Redirect; use axum::{ extract::{Json, Path}, http::StatusCode, response::IntoResponse, }; -use tower_cookies::cookie::SameSite; use std::time::Instant; -use tower_cookies::{Cookie, Cookies}; +use tower_cookies::Cookies; use tower_sessions::Session; use tracing::*; @@ -24,7 +21,11 @@ use tracing::*; // 1. Import the prelude - this contains everything needed for the server to function. use webauthn_rs::prelude::*; -use crate::config::{COOKIE_DOMAIN, COOKIE_NAME, SESSION_ACTIVE_TIME}; +use crate::config::{COOKIE_NAME, SESSION_ACTIVE_TIME}; +use crate::dto::credential_mapper::{add_credential_by_id, get_credential_from_uid}; +use crate::dto::user_mapper::{ + create_user_if_non_existent, get_uid_by_name, get_user_count_by_uid, +}; use crate::ServerState; // 2. The first step a client (user) will carry out is requesting a credential to be @@ -106,39 +107,22 @@ pub async fn start_register( .await .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?; let pool = &state.db; - let username = username.clone(); + let username_tmp = 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())?; - + match get_uid_by_name(username_tmp, pool).await { + Ok(uid) => { + let exclude_passkeys = get_credential_from_uid(uid, pool).await?; ( - Uuid::parse_str(&record.KEY) - .map_err(|_| WebauthnError::AuthenticationFailure.to_string())?, + uid, 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()) + exclude_passkeys + .into_iter() + .map(|k| k.cred_id().clone()) .collect(), ), ) } - None => (Uuid::new_v4(), None), + Err(_) => (Uuid::new_v4(), None), }; let res = match state.webauthn.start_passkey_registration( @@ -190,48 +174,19 @@ pub async fn finish_register( let res = match state.webauthn.finish_passkey_registration(®, ®_state) { Ok(key) => { info!("Passkey 正常"); - let uid = &user_id.to_string(); - let username = &user_name.to_string(); info!("检查用户是否存在"); // 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())?; - + let user_count = get_user_count_by_uid(user_id, pool).await?; // 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 + if user_count == 0 + && create_user_if_non_existent(user_id, user_name.to_string(), pool).await? != 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 - { - error!("将用户凭据持久化时失败"); + if add_credential_by_id(user_id, &key, pool).await? != 1 { + error!("将用户凭据持久化时失败,rows_affected!=1"); return Err(WebauthnError::AuthenticationFailure.to_string()); } @@ -274,172 +229,3 @@ pub async fn finish_register( // │ │ │ // // 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!("开始passkey 验证"); - let pool = &state.db; - // Remove any previous authentication that may have occurred from the session. - let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state").await; - 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, - cookies: Cookies, - 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())?; - info!("已获得认证状态:uid:{}",user_id); - let _ = session.remove::<(Uuid, PasskeyAuthentication)>("auth_state").await; - - let res = match state - .webauthn - .finish_passkey_authentication(&auth, &auth_state) - { - Ok(auth_result) => { - info!("passkey认证通过:uid:{}",user_id); - 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())?; - let mut lock = state.session.lock().await; - let uuid = Uuid::new_v4(); - let expires = Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME); - lock.insert( - uuid, - expires - ); - - let original_uri = cookies.get("OriginalURL"); - let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), uuid.to_string()); - new_cookie.set_domain(COOKIE_DOMAIN.to_string()); - new_cookie.set_http_only(true); - new_cookie.set_path("/"); - new_cookie.set_same_site(SameSite::None); - new_cookie.set_secure(Some(true)); - cookies.add(new_cookie); - // 从Cookie中恢复重定向信息 - match original_uri { - Some(redirect) => return Ok(Redirect::to(redirect.value())), - _ => (), - }; - // 处理完成重定向后,清除Cookie - cookies.remove(Cookie::new("OriginalURL", "")); - tracing::info!("Passkey登录成功,设置Cookie for {}", COOKIE_DOMAIN.to_string()); - info!("从passkey登录创建了新Session{},过期时间{}s后",uuid,*SESSION_ACTIVE_TIME); - StatusCode::OK - } - Err(e) => { - debug!("challenge_register -> {:?}", e); - StatusCode::BAD_REQUEST - } - }; - - info!("Authentication Successful!"); - Err(res.to_string()) -} diff --git a/src/services/cookie_services.rs b/src/services/cookie_services.rs new file mode 100644 index 0000000..710621b --- /dev/null +++ b/src/services/cookie_services.rs @@ -0,0 +1,19 @@ +use tower_cookies::cookie::SameSite; + +use crate::config::COOKIE_DOMAIN; + +use crate::config::COOKIE_NAME; + +use tower_cookies::Cookie; + +use uuid::Uuid; + +pub(crate) fn create_cookie_session<'a>(s: Uuid) -> Cookie<'a> { + let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), s.to_string()); + new_cookie.set_domain(COOKIE_DOMAIN.to_string()); + new_cookie.set_http_only(true); + new_cookie.set_path("/"); + new_cookie.set_same_site(SameSite::None); + new_cookie.set_secure(Some(true)); + new_cookie +} diff --git a/src/services/gc_services.rs b/src/services/gc_services.rs new file mode 100644 index 0000000..202cc29 --- /dev/null +++ b/src/services/gc_services.rs @@ -0,0 +1,30 @@ +use std::{borrow::BorrowMut, time::Instant}; + +use crate::ServerState; + +use std::sync::Arc; + +pub async fn gc(state: Arc) -> Result<(), String> { + let mut locked = state.session.lock().await; + let current_time = Instant::now(); + tracing::info!("before gc ,active Sessions {:?}", locked); + locked.borrow_mut().retain(|_, v| *v > current_time); + tracing::info!("gc fired,active Sessions {:?}", locked); + Ok(()) +} + +use crate::config::SESSION_ACTIVE_TIME; + +use std::time::Duration; + +pub async fn gc_task(state: Arc) { + let mut interval = tokio::time::interval(Duration::from_secs(*SESSION_ACTIVE_TIME)); + loop { + interval.tick().await; + let res = crate::services::gc_services::gc(state.clone()).await; + match res { + Ok(_) => tracing::info!("gc completed"), + Err(s) => tracing::error!("gc failed:{}", s), + } + } +} diff --git a/src/services/mod.rs b/src/services/mod.rs index 984f186..35aa2b3 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -2,33 +2,19 @@ use axum::http::{HeaderMap, HeaderValue}; use axum::response::{Html, Redirect}; use axum::{extract::State, http::StatusCode, response::IntoResponse, Form}; use minijinja::{context, Environment}; -use tower_cookies::cookie::SameSite; -use std::borrow::BorrowMut; +use std::str::FromStr; use std::sync::Arc; use std::time::{Duration, Instant}; -use std::str::FromStr; use tower_cookies::{Cookie, Cookies}; use uuid::Uuid; -use super::config::{COOKIE_DOMAIN, COOKIE_NAME, LOGIN_PAGE_HTML, SESSION_ACTIVE_TIME}; -use crate::config::{HOME_URL, REGISTER_PAGE_HTML}; +use super::config::{COOKIE_NAME, LOGIN_PAGE_HTML, SESSION_ACTIVE_TIME}; +use crate::config::{COOKIE_DOMAIN, HOME_URL, REGISTER_PAGE_HTML}; use crate::{ServerState, UserLoginForm}; -pub async fn gc_task(state: Arc) { - let mut interval = tokio::time::interval(Duration::from_secs(*SESSION_ACTIVE_TIME)); - loop { - interval.tick().await; - let res = gc(state.clone()).await; - match res { - Ok(_) => tracing::info!("gc completed"), - Err(s) => tracing::error!("gc failed:{}", s), - } - } -} - // 处理/auth pub async fn auth_otp( State(state): State>, @@ -69,7 +55,7 @@ pub async fn login( tracing::info!("{:?}", &frm); let target = sqlx::query_as::<_, UserLoginForm>( r" - SELECT NAME, KEY FROM USERS WHERE NAME = ? + SELECT NAME, OTP_KEY FROM USERS WHERE NAME = ? ", ) .bind(frm.username) @@ -86,12 +72,7 @@ pub async fn login( Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME), ); let original_uri = cookies.get("OriginalURL"); - let mut new_cookie = Cookie::new(COOKIE_NAME.to_string(), s.to_string()); - new_cookie.set_domain(COOKIE_DOMAIN.to_string()); - new_cookie.set_http_only(true); - new_cookie.set_path("/"); - new_cookie.set_same_site(SameSite::None); - new_cookie.set_secure(Some(true)); + let new_cookie = crate::services::cookie_services::create_cookie_session(s); cookies.add(new_cookie); tracing::info!("登录成功,设置Cookie for {}", COOKIE_DOMAIN.to_string()); // 从Cookie中恢复重定向信息 @@ -152,13 +133,6 @@ pub fn check_otp(key_from_db: String, user_input_otp: String) -> bool { false } -pub async fn gc(state: Arc) -> Result<(), String> { - let mut locked = state.session.lock().await; - let current_time = Instant::now(); - tracing::info!("before gc ,active Sessions {:?}", locked); - locked.borrow_mut().retain(|_, v| *v > current_time); - tracing::info!("gc fired,active Sessions {:?}", locked); - Ok(()) -} - pub mod auth; +pub mod cookie_services; +pub mod gc_services;