Compare commits

...

10 Commits

Author SHA1 Message Date
mortis-0
1831720b24 Refactor; Fix CORS when login via passkeys
Some checks are pending
Build and Upload Binary / build (x86_64-unknown-linux-musl-rust-auth, auth, x86, stable, x86_64-unknown-linux-musl) (push) Waiting to run
2024-03-17 16:42:32 +08:00
mortis-0
3798e573b6 Refactor; Fix CORS when login via passkeys 2024-03-16 20:16:33 +08:00
mortis-0
e494d71dc3 Refactor Done 2024-03-16 17:34:43 +08:00
mortis-0
409b152fce 修复Passkey登录的UserHandle 编码 2024-03-15 18:45:54 +08:00
yly
0880718125 Fix Cookie设置问题修复 2024-03-14 23:07:35 +08:00
yly
2f58427025 add log when setting Cookies 2024-03-14 22:43:33 +08:00
yly
d119f958f6 DONE,处理重定向 2024-03-14 22:32:02 +08:00
yly
2b7932dd49 完成 Passkey 登录 2024-03-14 17:06:55 +08:00
mortis-0
75e616201b fuck WIP shit 2024-03-14 00:24:04 +08:00
yly
8a900fe7a7 TEMP WIP 2024-03-13 15:24:55 +08:00
34 changed files with 2096 additions and 362 deletions

View File

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT COUNT(id) AS count FROM users WHERE id = $1;",
"describe": {
"columns": [
{
"name": "count",
"ordinal": 0,
"type_info": "Int"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "02e3c3e3c5ed47a4fbed6d00a85bb2581417b471dc702d84faa06d3a80a73626"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT id FROM users WHERE name = $1;",
"describe": {
"columns": [
{
"name": "ID",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "13e243d5dedaaca85414aeba7883df7099dee9ac1243301baa5847cf91029178"
}

View File

@ -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": "5b96d31e66cc895d1f8835d777c7e7bea86ba430d2e284631214751571b4f71f"
}

View File

@ -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"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO users(id, name) VALUES($1, $2);",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "765a6500f280d08e51e9457e7851a3dd8d2d3805e314cb84d589713bc5ebd7df"
}

View File

@ -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": "bd837e755c015654011c0827cc73e28064aa04f38e7d6e8c37f574196e0e6b98"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT name FROM users WHERE id = $1;",
"describe": {
"columns": [
{
"name": "NAME",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "c2ee6dfd462023f43f562277b198e912f6ed52fd9cd1dbe902eaef02416a04ba"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO credentials(user_id, credential) VALUES($1, $2);",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "ceb63ff524b09830ab5978258451d597ffc097990591b3964eab20eb41bb7ff9"
}

705
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,16 +6,19 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
axum = {version = "0.6.20", features = [ "default", "headers" ]}
axum = {version = "0.7.4", features = [ "default", "form", "tracing", "macros" ]}
tokio = { version = "1.0", features = ["full"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
tower-cookies = "0.9.0"
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"] }
totp-rs = "5.4.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tower-http = { version = "0.4.4", features = ["trace"] }
tower-http = { version = "0.5", features = ["trace", "fs"] }
tracing = "0.1.40"
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"

View File

@ -5,13 +5,13 @@ upstream protected {
server {
listen 8080;
location / {
auth_request /auth;
auth_request /aaron/auth;
set $original_full_url $scheme://$host$request_uri;
error_page 401 =200 /login;
proxy_set_header X-Original-URI $scheme://$host$request_uri;
proxy_pass http://protected/;
}
location = /auth {
location = /aaron/auth {
internal;
proxy_pass http://localhost:3000/auth;
proxy_pass_request_body off;
@ -20,7 +20,7 @@ server {
proxy_set_header X-Original-Remote-Addr $remote_addr;
proxy_set_header X-Original-Host $host;
}
location /login {
location /aaron/login {
proxy_pass http://localhost:3000/login;
proxy_set_header X-Original-Remote-Addr $remote_addr;
proxy_set_header X-Original-Host $host;

BIN
auth.db

Binary file not shown.

View File

@ -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

View File

@ -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;
}
# 保留现有的其它配置 ...
}

View File

@ -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;
# }
#}

View File

@ -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;
# }
#}

View File

@ -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;
# }
#}

View File

@ -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;
}
}

View File

@ -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;
# }
#}

View File

@ -63,13 +63,15 @@
<body>
<div class="container">
<h1>Login</h1>
<form action="/login{{ url }}" method="POST">
<form action="{{ home_url|safe }}/login" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="otp" required>
<input type="submit" value="Submit">
</form>
<p>Or:</p>
<a href="{{ home_url|safe }}/register">Continue with Passkey</a>
</div>
</body>
</html>

149
register.html Normal file

File diff suppressed because one or more lines are too long

25
src/config.rs Normal file
View File

@ -0,0 +1,25 @@
use once_cell::sync::Lazy;
use std::env;
pub static COOKIE_NAME: Lazy<String> =
Lazy::new(|| env::var("COOKIE_NAME").unwrap_or("aaron_auth".to_string()));
pub static SESSION_ACTIVE_TIME: Lazy<u64> = Lazy::new(|| {
env::var("SESSION_ACTIVE_TIME")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(600)
});
pub static COOKIE_DOMAIN: Lazy<String> =
Lazy::new(|| env::var("DOMAIN").ok().unwrap_or(".aaronhu.cn".to_owned()));
pub static LOGIN_PAGE_HTML: &str = include_str!("../loginpage.html");
pub static REGISTER_PAGE_HTML: &str = include_str!("../register.html");
pub static PORT: Lazy<String> = Lazy::new(|| env::var("PORT").unwrap_or("3000".to_string()));
// 部署此应用的URL,默认为/aaron
pub static HOME_URL: Lazy<String> =
Lazy::new(|| env::var("HOME_URL").unwrap_or("/aaron".to_owned()));
pub static DATABASE_URL: Lazy<String> =
Lazy::new(|| env::var("DATABASE_URL").unwrap_or("".to_owned()));
pub static RP_ID: Lazy<String> = Lazy::new(|| env::var("RP_ID").unwrap_or("localhost".to_owned()));
pub static RP_ORIGIN: Lazy<String> =
Lazy::new(|| env::var("RP_ORIGIN").unwrap_or("http://localhost:8080".to_owned()));
pub static RP_NAME: Lazy<String> =
Lazy::new(|| env::var("RP_NAME").unwrap_or("Localhost".to_owned()));

3
src/controllers/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod otp_controllers;
pub mod passkey_login_controllers;
pub mod passkey_register_controllers;

View File

@ -0,0 +1 @@

View File

@ -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<Arc<ServerState>>,
session: Session,
Path(user_name): Path<String>,
) -> Result<impl IntoResponse, String> {
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<Passkey> = 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<Arc<ServerState>>,
session: Session,
cookies: Cookies,
Json(auth): Json<PublicKeyCredential>,
) -> Result<impl IntoResponse, String> {
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(format!("{{\"status\": \"ok\", \"redirect\":\"{}\"}}",redirect.value().to_string())),
_ => (),
};
// 处理完成重定向后清除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())
}

View File

@ -0,0 +1,169 @@
use axum::http::StatusCode;
use uuid::Uuid;
use webauthn_rs::prelude::{CreationChallengeResponse, CredentialID, PasskeyAuthentication, PasskeyRegistration, RegisterPublicKeyCredential, WebauthnError};
use crate::dto::credential_mapper::add_credential_by_id;
use crate::dto::user_mapper::{create_user_if_non_existent, get_user_count_by_uid};
use crate::SESSION_ACTIVE_TIME;
use axum::response::IntoResponse;
use crate::dto::{credential_mapper::get_credential_from_uid, user_mapper::get_uid_by_name};
use crate::config::COOKIE_NAME;
use tracing::{info,debug,error};
use axum::extract::{Json, Path};
use tower_sessions::Session;
use tower_cookies::Cookies;
use crate::entities::ServerState;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::extract::State;
// TODO - Improve error handling and messages
fn auth_user(cookie_content: String, session_table: &mut HashMap<Uuid, Instant>) -> bool {
let Ok(uuid) = Uuid::parse_str(&cookie_content) else {
info!("此用户Session不在表中");
return false;
};
let Some(expire) = session_table.get(&uuid) else {
info!("此用户Session已过期");
return false;
};
if *expire <= Instant::now() {
info!("此用户Session已过期");
return false;
}
session_table.insert(
uuid,
Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME),
);
tracing::info!("valid cookie {}", uuid);
true
}
pub async fn start_register(
State(state): State<Arc<ServerState>>,
cookies: Cookies,
session: Session,
Path(username): Path<String>,
) -> Result<Json<CreationChallengeResponse>, String> {
tracing::info!("开始注册");
// todo!("Auth User");
let Some(cookie_content) = cookies.get(&COOKIE_NAME) else {
tracing::info!("用户没有Cookie");
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) {
tracing::info!("用户Cookie无效不在Session表中");
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_tmp = username.clone();
let (user_id, exclude_credentials): (Uuid, Option<Vec<CredentialID>>) =
match get_uid_by_name(username_tmp, pool).await {
Ok(uid) => {
let exclude_passkeys = get_credential_from_uid(uid, pool).await?;
(
uid,
Some(
exclude_passkeys
.into_iter()
.map(|k| k.cred_id().clone())
.collect(),
),
)
}
Err(_) => (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)
}
pub async fn finish_register(
State(state): State<Arc<ServerState>>,
session: Session,
Json(reg): Json<RegisterPublicKeyCredential>,
) -> Result<impl IntoResponse, String> {
info!("完成注册...");
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(&reg, &reg_state) {
Ok(key) => {
info!("Passkey 正常");
info!("检查用户是否存在");
// Check if the user_id already exists
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 user_count == 0
&& create_user_if_non_existent(user_id, user_name.to_string(), pool).await? != 1
{
return Err(WebauthnError::AuthenticationFailure.to_string());
}
// Insert the key into the auth table
if add_credential_by_id(user_id, &key, pool).await? != 1 {
error!("将用户凭据持久化时失败,rows_affected!=1");
return Err(WebauthnError::AuthenticationFailure.to_string());
}
StatusCode::OK
}
Err(e) => {
error!("challenge_register -> {:?}", e);
StatusCode::BAD_REQUEST
}
};
Ok(res)
}

View File

@ -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<Sqlite>,
) -> Result<Vec<Passkey>, 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::<Passkey>(&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<sqlx::Sqlite>,
) -> Result<String, String> {
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<sqlx::Sqlite>,
) -> Result<u64, String> {
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)
}

2
src/dto/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod credential_mapper;
pub mod user_mapper;

68
src/dto/user_mapper.rs Normal file
View File

@ -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<Sqlite>) -> Result<String, String> {
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<Sqlite>) -> Result<Uuid, String> {
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<Sqlite>) -> Result<i32, String> {
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<Sqlite>,
) -> Result<u64, String> {
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)
}
}

64
src/entities/mod.rs Normal file
View File

@ -0,0 +1,64 @@
use crate::config::{DATABASE_URL, RP_ID, RP_NAME, RP_ORIGIN};
use serde::Deserialize;
use sqlx::SqlitePool;
use std::time::Instant;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::Mutex;
use uuid::Uuid;
use webauthn_rs::{prelude::Url, Webauthn, WebauthnBuilder};
pub struct ServerState {
pub db: sqlx::Pool<sqlx::Sqlite>,
pub session: Mutex<HashMap<Uuid, Instant>>,
pub webauthn: Arc<Webauthn>,
// 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")]
pub username: String,
#[sqlx(rename = "OTP_KEY")]
pub otp: String,
}
#[derive(Deserialize, sqlx::FromRow, Debug)]
pub struct User {
#[sqlx(rename = "NAME")]
pub username: String,
#[sqlx(rename = "ID")]
pub uid: String,
#[sqlx(rename = "OTP_KEY")]
pub otp_key: String,
}

View File

@ -1,206 +1,69 @@
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 minijinja::{context, Environment};
use serde::Deserialize;
use sqlx::sqlite::SqlitePool;
use axum::{
routing::{get, post},
Router,
};
use entities::*;
use controllers::passkey_login_controllers::{start_authentication, finish_authentication};
use services::{
gc_services::gc_task,
login, login_page, register_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 tokio::sync::Mutex;
use tower_cookies::{Cookie, CookieManagerLayer, Cookies};
use tower_cookies::CookieManagerLayer;
use tower_http::trace::{self, TraceLayer};
use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
use tracing::Level;
use uuid::Uuid;
pub struct ServerState {
pub db: sqlx::Pool<sqlx::Sqlite>,
pub session: Mutex<HashMap<Uuid, Instant>>,
// started: Instant,
}
#[derive(Deserialize, sqlx::FromRow, Debug)]
pub struct UserLoginForm {
#[sqlx(rename = "NAME")]
pub username: String,
#[sqlx(rename = "KEY")]
pub otp: String,
}
use once_cell::sync::Lazy;
static COOKIE_NAME: Lazy<String> =
Lazy::new(|| env::var("COOKIE_NAME").unwrap_or("aaron_auth".to_string()));
const SESSION_ACTIVE_TIME: Lazy<u64> = Lazy::new(|| {
env::var("SESSION_ACTIVE_TIME")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(600)
});
const LOGIN_PAGE_HTML: &str = include_str!("../loginpage.html");
pub mod config;
pub mod controllers;
pub mod dto;
pub mod entities;
pub mod services;
use config::*;
use controllers::passkey_register_controllers::{finish_register,start_register};
#[tokio::main]
async fn main() {
// 初始化日志记录器
tracing_subscriber::fmt::init();
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 session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store)
.with_secure(false)
.with_expiry(Expiry::OnSessionEnd);
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))
.with_state(state.clone())
.layer(CookieManagerLayer::new())
.route("/register", get(register_page))
.route("/register_start/:username", post(start_register))
.route("/register_finish", post(finish_register))
.route(
"/webauthn_login_start/:username",
post(start_authentication),
)
.route("/webauthn_login_finish", post(finish_authentication))
.layer(
TraceLayer::new_for_http()
.make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
.on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
);
let port = env::var("PORT").unwrap_or("3000".to_string());
let port = port.parse::<u16>().unwrap_or(3000);
)
.with_state(state.clone())
.layer(CookieManagerLayer::new())
.layer(session_layer);
// 嵌套防止撞路由
let aaronhu = Router::new().nest(&HOME_URL, app);
let port = PORT.parse::<u16>().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()));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
async fn gc_task(state: Arc<ServerState>) {
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
async fn auth(
State(state): State<Arc<ServerState>>,
// Form(frm): Form<UserLoginForm>,
cookies: Cookies,
) -> StatusCode {
if let Some(session_token) = cookies.get(&COOKIE_NAME) {
tracing::info!("session:{}", session_token.value());
let Ok(s) = uuid::Uuid::from_str(session_token.value()) else {
return StatusCode::UNAUTHORIZED;
};
let mut locked = state.session.lock().await;
if let std::collections::hash_map::Entry::Occupied(mut e) = locked.entry(s) {
// FIX, when accessed /auth with correct cookie, the cookie's expiration is delayed
let Some(v) = Some(e.insert(Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME))) else {
tracing::info!("session:{} extended", session_token.value());
return StatusCode::UNAUTHORIZED;
};
if Instant::now() < v {
return StatusCode::OK;
}
}
}
StatusCode::UNAUTHORIZED
}
async fn login(
State(state): State<Arc<ServerState>>,
cookies: Cookies,
Query(params): Query<HashMap<String, String>>,
Form(frm): Form<UserLoginForm>,
) -> Result<Redirect, (StatusCode, &'static str)> {
let conn = state.db.acquire().await;
let Ok(mut conn) = conn else {
return Err((StatusCode::BAD_GATEWAY, "db连接错误"));
};
tracing::info!("{:?}", &frm);
let target = sqlx::query_as::<_, UserLoginForm>(
r#"
SELECT NAME, KEY FROM USERS WHERE NAME = ?
"#,
)
.bind(frm.username)
.fetch_optional(&mut *conn)
.await;
tracing::info!("{:?}", &target);
if let Ok(Some(target)) = target {
if check_otp(target.otp, frm.otp) {
let s = Uuid::new_v4();
let mut locked = state.session.lock().await;
locked.insert(
s,
Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME),
);
let mut new_cookie = Cookie::new(&*COOKIE_NAME, s.to_string());
new_cookie.set_domain(".aaronhu.cn");
cookies.add(new_cookie);
if let Some(original_uri) = params.get("original_url") {
return Ok(Redirect::to(original_uri));
}
return Err((StatusCode::ACCEPTED, "ok"));
} else {
return Err((StatusCode::UNAUTHORIZED, "wrong password"));
}
}
Err((StatusCode::BAD_GATEWAY, "unreachable"))
}
async fn login_page(headers: HeaderMap<HeaderValue>) -> impl IntoResponse {
tracing::info!("Headers: {:#?}", headers);
let mut env = Environment::new();
env.add_template("login.html", LOGIN_PAGE_HTML).unwrap();
let template = env.get_template("login.html").unwrap();
if let Some(original_uri) = headers.get("X-Original-URI") {
if let Ok(uri) = original_uri.to_str() {
tracing::info!("redirect to {}", uri);
if !uri.is_empty() {
let uri = "?original_url=".to_owned() + uri;
return Html(
template
.render(context! { url => uri })
.unwrap_or("Error".to_string()),
);
}
}
}
Html(
template
.render(context! { url => String::new() })
.unwrap_or("Error".to_string()),
)
}
pub fn check_otp(key_from_db: String, user_input_otp: String) -> bool {
use totp_rs::{Algorithm, Secret, TOTP};
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
Secret::Raw(key_from_db.as_bytes().to_vec())
.to_bytes()
.unwrap(),
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
tracing::info!(
"配置如下 COOKIE_DOMAIN={}\nRP_ID={}\nRP_ORIGIN={}\nCOOKIE_NAME={}\n",
COOKIE_DOMAIN.to_string(),
RP_ID.to_string(),
RP_ORIGIN.to_string(),
COOKIE_NAME.to_string()
);
if let Ok(otp) = totp {
if let Ok(token) = otp.generate_current() {
return token == user_input_otp;
}
}
false
}
async fn gc(state: Arc<ServerState>) -> 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(())
axum::serve(listener, aaronhu).await.unwrap();
}

View File

@ -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
}

View File

@ -0,0 +1,30 @@
use std::{borrow::BorrowMut, time::Instant};
use crate::ServerState;
use std::sync::Arc;
pub async fn gc(state: Arc<ServerState>) -> 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<ServerState>) {
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),
}
}
}

137
src/services/mod.rs Normal file
View File

@ -0,0 +1,137 @@
use axum::http::{HeaderMap, HeaderValue};
use axum::response::{Html, Redirect};
use axum::{extract::State, http::StatusCode, response::IntoResponse, Form};
use minijinja::{context, Environment};
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tower_cookies::{Cookie, Cookies};
use uuid::Uuid;
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};
// 处理/auth
pub async fn auth_otp(
State(state): State<Arc<ServerState>>,
// Form(frm): Form<UserLoginForm>,
cookies: Cookies,
) -> StatusCode {
if let Some(session_token) = cookies.get(&COOKIE_NAME) {
tracing::info!("session:{}", session_token.value());
let Ok(s) = uuid::Uuid::from_str(session_token.value()) else {
return StatusCode::UNAUTHORIZED;
};
let mut locked = state.session.lock().await;
if let std::collections::hash_map::Entry::Occupied(mut e) = locked.entry(s) {
// FIX, when accessed /auth with correct cookie, the cookie's expiration is delayed
let Some(v) =
Some(e.insert(Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME)))
else {
tracing::info!("session:{} extended", session_token.value());
return StatusCode::UNAUTHORIZED;
};
if Instant::now() < v {
return StatusCode::OK;
}
}
}
StatusCode::UNAUTHORIZED
}
pub async fn login(
State(state): State<Arc<ServerState>>,
cookies: Cookies,
Form(frm): Form<UserLoginForm>,
) -> Result<Redirect, (StatusCode, &'static str)> {
let conn = state.db.acquire().await;
let Ok(mut conn) = conn else {
return Err((StatusCode::BAD_GATEWAY, "db连接错误"));
};
tracing::info!("{:?}", &frm);
let target = sqlx::query_as::<_, UserLoginForm>(
r"
SELECT NAME, OTP_KEY FROM USERS WHERE NAME = ?
",
)
.bind(frm.username)
.fetch_optional(&mut *conn)
.await;
tracing::info!("{:?}", &target);
if let Ok(Some(target)) = target {
if check_otp(target.otp, frm.otp) {
let s = Uuid::new_v4();
let mut locked = state.session.lock().await;
locked.insert(
s,
Instant::now() + Duration::from_secs(*SESSION_ACTIVE_TIME),
);
let original_uri = cookies.get("OriginalURL");
let new_cookie = crate::services::cookie_services::create_cookie_session(s);
cookies.add(new_cookie);
tracing::info!("登录成功设置Cookie for {}", COOKIE_DOMAIN.to_string());
// 从Cookie中恢复重定向信息
let res = match original_uri {
Some(redirect) => Ok(Redirect::to(redirect.value())),
None => Err((StatusCode::ACCEPTED, "ok")),
};
// 处理完成重定向后清除Cookie
cookies.remove(Cookie::new("OriginalURL", ""));
return res;
} else {
return Err((StatusCode::UNAUTHORIZED, "wrong password"));
}
}
Err((StatusCode::BAD_GATEWAY, "unreachable"))
}
pub async fn login_page(headers: HeaderMap<HeaderValue>) -> impl IntoResponse {
tracing::info!("Headers: {:#?}", headers);
let mut env = Environment::new();
env.add_template("login.html", LOGIN_PAGE_HTML).unwrap();
let template = env.get_template("login.html").unwrap();
Html(
template
.render(context! { home_url => HOME_URL.to_string() })
.unwrap_or("Error".to_string()),
)
}
pub async fn register_page(headers: HeaderMap<HeaderValue>) -> impl IntoResponse {
tracing::info!("Headers: {:#?}", headers);
let mut env = Environment::new();
env.add_template("register.html", REGISTER_PAGE_HTML)
.unwrap();
let template = env.get_template("register.html").unwrap();
Html(
template
.render(context! { url => String::new(), home_url => HOME_URL.to_string() })
.unwrap_or("Error".to_string()),
)
}
pub fn check_otp(key_from_db: String, user_input_otp: String) -> bool {
use totp_rs::{Algorithm, Secret, TOTP};
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
Secret::Raw(key_from_db.as_bytes().to_vec())
.to_bytes()
.unwrap(),
);
if let Ok(otp) = totp {
if let Ok(token) = otp.generate_current() {
return token == user_input_otp;
}
}
false
}
pub mod cookie_services;
pub mod gc_services;