mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-15 06:02:42 +00:00
Add SSO functionality using OpenID Connect
Co-authored-by: Pablo Ovelleiro Corral <mail@pablo.tools> Co-authored-by: Stuart Heap <sheap13@gmail.com> Co-authored-by: Alex Moore <skiepp@my-dockerfarm.cloud> Co-authored-by: Brian Munro <brian.alexander.munro@gmail.com> Co-authored-by: Jacques B. <timshel@github.com>
This commit is contained in:
parent
3d2df6ce11
commit
f59b11ae76
31 changed files with 1225 additions and 59 deletions
|
@ -352,6 +352,17 @@
|
||||||
## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`.
|
## Allow a burst of requests of up to this size, while maintaining the average indicated by `ADMIN_RATELIMIT_SECONDS`.
|
||||||
# ADMIN_RATELIMIT_MAX_BURST=3
|
# ADMIN_RATELIMIT_MAX_BURST=3
|
||||||
|
|
||||||
|
## SSO settings (OpenID Connect)
|
||||||
|
## Controls whether users can login using an OpenID Connect identity provider
|
||||||
|
# SSO_ENABLED=true
|
||||||
|
## Prevent users from logging in directly without going through SSO
|
||||||
|
# SSO_ONLY=false
|
||||||
|
## Base URL of the OIDC server (auto-discovery is used)
|
||||||
|
# SSO_AUTHORITY=https://auth.example.com
|
||||||
|
## Set your Client ID and Client Key
|
||||||
|
# SSO_CLIENT_ID=11111
|
||||||
|
# SSO_CLIENT_SECRET=AAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
|
||||||
## Set the lifetime of admin sessions to this value (in minutes).
|
## Set the lifetime of admin sessions to this value (in minutes).
|
||||||
# ADMIN_SESSION_LIFETIME=20
|
# ADMIN_SESSION_LIFETIME=20
|
||||||
|
|
||||||
|
|
502
Cargo.lock
generated
502
Cargo.lock
generated
|
@ -268,6 +268,12 @@ dependencies = [
|
||||||
"rustc-demangle",
|
"rustc-demangle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base16ct"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
|
@ -400,7 +406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082"
|
checksum = "b48814962d2fd604c50d2b9433c2a41a0ab567779ee2c02f7fba6eca1221f082"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cached_proc_macro_types",
|
"cached_proc_macro_types",
|
||||||
"darling",
|
"darling 0.14.4",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
|
@ -471,6 +477,12 @@ dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
version = "0.16.2"
|
version = "0.16.2"
|
||||||
|
@ -581,6 +593,18 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-bigint"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
@ -597,8 +621,18 @@ version = "0.14.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.14.4",
|
||||||
"darling_macro",
|
"darling_macro 0.14.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.3",
|
||||||
|
"darling_macro 0.20.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -615,17 +649,42 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.28",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.14.4"
|
version = "0.14.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.14.4",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.3",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.28",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.0"
|
version = "5.5.0"
|
||||||
|
@ -651,11 +710,25 @@ version = "0.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f"
|
checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "der"
|
||||||
|
version = "0.7.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c"
|
||||||
|
dependencies = [
|
||||||
|
"const-oid",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929"
|
checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "devise"
|
name = "devise"
|
||||||
|
@ -759,6 +832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
@ -769,12 +843,53 @@ version = "0.15.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dyn-clone"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ecdsa"
|
||||||
|
version = "0.16.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"digest",
|
||||||
|
"elliptic-curve",
|
||||||
|
"rfc6979",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "elliptic-curve"
|
||||||
|
version = "0.13.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"crypto-bigint",
|
||||||
|
"digest",
|
||||||
|
"ff",
|
||||||
|
"generic-array",
|
||||||
|
"group",
|
||||||
|
"hkdf",
|
||||||
|
"pem-rfc7468",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core",
|
||||||
|
"sec1",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email-encoding"
|
name = "email-encoding"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -882,6 +997,16 @@ dependencies = [
|
||||||
"syslog",
|
"syslog",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ff"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "figment"
|
name = "figment"
|
||||||
version = "0.10.10"
|
version = "0.10.10"
|
||||||
|
@ -1067,6 +1192,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check",
|
"version_check",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1076,8 +1202,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1122,6 +1250,17 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "group"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||||
|
dependencies = [
|
||||||
|
"ff",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.20"
|
version = "0.3.20"
|
||||||
|
@ -1192,6 +1331,21 @@ version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hkdf"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hmac"
|
name = "hmac"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
@ -1279,6 +1433,20 @@ dependencies = [
|
||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-tls"
|
name = "hyper-tls"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
@ -1371,6 +1539,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.14.0",
|
"hashbrown 0.14.0",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1428,6 +1597,15 @@ dependencies = [
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
|
@ -1488,6 +1666,9 @@ name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
dependencies = [
|
||||||
|
"spin 0.5.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lettre"
|
name = "lettre"
|
||||||
|
@ -1524,6 +1705,12 @@ version = "0.2.147"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libmimalloc-sys"
|
name = "libmimalloc-sys"
|
||||||
version = "0.1.33"
|
version = "0.1.33"
|
||||||
|
@ -1795,6 +1982,23 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint-dig"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"lazy_static",
|
||||||
|
"libm",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-traits",
|
||||||
|
"rand",
|
||||||
|
"smallvec",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1816,6 +2020,17 @@ dependencies = [
|
||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-iter"
|
||||||
|
version = "0.1.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
|
@ -1823,6 +2038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1844,6 +2060,26 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oauth2"
|
||||||
|
version = "4.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09a6e2a2b13a56ebeabba9142f911745be6456163fd6c3d361274ebcd891a80c"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"chrono",
|
||||||
|
"getrandom",
|
||||||
|
"http",
|
||||||
|
"rand",
|
||||||
|
"reqwest",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"sha2",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.31.1"
|
version = "0.31.1"
|
||||||
|
@ -1859,6 +2095,37 @@ version = "1.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openidconnect"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03335ade401352b354b017e7597ddb40040091da445b031bf659e597e032b1fc"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"chrono",
|
||||||
|
"dyn-clone",
|
||||||
|
"hmac",
|
||||||
|
"http",
|
||||||
|
"itertools",
|
||||||
|
"log",
|
||||||
|
"oauth2",
|
||||||
|
"p256",
|
||||||
|
"p384",
|
||||||
|
"rand",
|
||||||
|
"rsa",
|
||||||
|
"serde",
|
||||||
|
"serde-value",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_plain",
|
||||||
|
"serde_with",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.56"
|
version = "0.10.56"
|
||||||
|
@ -1913,12 +2180,45 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-float"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "overload"
|
name = "overload"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p256"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "p384"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
|
||||||
|
dependencies = [
|
||||||
|
"ecdsa",
|
||||||
|
"elliptic-curve",
|
||||||
|
"primeorder",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -2006,6 +2306,15 @@ dependencies = [
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pem-rfc7468"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
@ -2112,6 +2421,27 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs1"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"pkcs8",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs8"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||||
|
dependencies = [
|
||||||
|
"der",
|
||||||
|
"spki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.27"
|
version = "0.3.27"
|
||||||
|
@ -2149,6 +2479,15 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "primeorder"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3"
|
||||||
|
dependencies = [
|
||||||
|
"elliptic-curve",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
|
@ -2365,6 +2704,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"hyper",
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
"hyper-tls",
|
"hyper-tls",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -2374,11 +2714,14 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls",
|
||||||
"tokio-socks",
|
"tokio-socks",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
@ -2388,6 +2731,7 @@ dependencies = [
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
"winreg 0.10.1",
|
"winreg 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2401,6 +2745,16 @@ dependencies = [
|
||||||
"quick-error",
|
"quick-error",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfc6979"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||||
|
dependencies = [
|
||||||
|
"hmac",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.16.20"
|
version = "0.16.20"
|
||||||
|
@ -2539,6 +2893,28 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsa"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"const-oid",
|
||||||
|
"digest",
|
||||||
|
"num-bigint-dig",
|
||||||
|
"num-integer",
|
||||||
|
"num-iter",
|
||||||
|
"num-traits",
|
||||||
|
"pkcs1",
|
||||||
|
"pkcs8",
|
||||||
|
"rand_core",
|
||||||
|
"signature",
|
||||||
|
"spki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rtoolbox"
|
name = "rtoolbox"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
@ -2674,6 +3050,20 @@ dependencies = [
|
||||||
"untrusted",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sec1"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||||
|
dependencies = [
|
||||||
|
"base16ct",
|
||||||
|
"der",
|
||||||
|
"generic-array",
|
||||||
|
"pkcs8",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.9.2"
|
version = "2.9.2"
|
||||||
|
@ -2712,6 +3102,16 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-value"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
|
||||||
|
dependencies = [
|
||||||
|
"ordered-float",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_cbor"
|
name = "serde_cbor"
|
||||||
version = "0.11.2"
|
version = "0.11.2"
|
||||||
|
@ -2744,6 +3144,25 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_path_to_error"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
|
@ -2765,6 +3184,35 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.2",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap 1.9.3",
|
||||||
|
"indexmap 2.0.0",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.3",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.28",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha-1"
|
name = "sha-1"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
@ -2826,6 +3274,16 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signature"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
@ -2891,6 +3349,16 @@ version = "0.9.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "spki"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"der",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable-pattern"
|
name = "stable-pattern"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -3473,6 +3941,7 @@ dependencies = [
|
||||||
"num-derive",
|
"num-derive",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"openidconnect",
|
||||||
"openssl",
|
"openssl",
|
||||||
"paste",
|
"paste",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
@ -3652,6 +4121,25 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.22.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
|
||||||
|
dependencies = [
|
||||||
|
"webpki",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
|
@ -3830,3 +4318,9 @@ dependencies = [
|
||||||
"sha1",
|
"sha1",
|
||||||
"threadpool",
|
"threadpool",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
|
||||||
|
|
|
@ -149,6 +149,9 @@ pico-args = "0.5.0"
|
||||||
paste = "1.0.14"
|
paste = "1.0.14"
|
||||||
governor = "0.6.0"
|
governor = "0.6.0"
|
||||||
|
|
||||||
|
# OIDC for SSO
|
||||||
|
openidconnect = "3.3.0"
|
||||||
|
|
||||||
# Check client versions for specific features.
|
# Check client versions for specific features.
|
||||||
semver = "1.0.18"
|
semver = "1.0.18"
|
||||||
|
|
||||||
|
|
1
migrations/mysql/2023-02-01-133000_add_sso/down.sql
Normal file
1
migrations/mysql/2023-02-01-133000_add_sso/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE sso_nonce;
|
3
migrations/mysql/2023-02-01-133000_add_sso/up.sql
Normal file
3
migrations/mysql/2023-02-01-133000_add_sso/up.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY
|
||||||
|
);
|
1
migrations/postgresql/2023-02-01-133000_add_sso/down.sql
Normal file
1
migrations/postgresql/2023-02-01-133000_add_sso/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE sso_nonce;
|
3
migrations/postgresql/2023-02-01-133000_add_sso/up.sql
Normal file
3
migrations/postgresql/2023-02-01-133000_add_sso/up.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY
|
||||||
|
);
|
1
migrations/sqlite/2023-02-01-133000_add_sso/down.sql
Normal file
1
migrations/sqlite/2023-02-01-133000_add_sso/down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE sso_nonce;
|
3
migrations/sqlite/2023-02-01-133000_add_sso/up.sql
Normal file
3
migrations/sqlite/2023-02-01-133000_add_sso/up.sql
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
CREATE TABLE sso_nonce (
|
||||||
|
nonce CHAR(36) NOT NULL PRIMARY KEY
|
||||||
|
);
|
|
@ -28,6 +28,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
get_public_keys,
|
get_public_keys,
|
||||||
post_keys,
|
post_keys,
|
||||||
post_password,
|
post_password,
|
||||||
|
post_set_password,
|
||||||
post_kdf,
|
post_kdf,
|
||||||
post_rotatekey,
|
post_rotatekey,
|
||||||
post_sstamp,
|
post_sstamp,
|
||||||
|
@ -78,6 +79,21 @@ pub struct RegisterData {
|
||||||
OrganizationUserId: Option<String>,
|
OrganizationUserId: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct SetPasswordData {
|
||||||
|
Kdf: Option<i32>,
|
||||||
|
KdfIterations: Option<i32>,
|
||||||
|
KdfMemory: Option<i32>,
|
||||||
|
KdfParallelism: Option<i32>,
|
||||||
|
Key: String,
|
||||||
|
Keys: Option<KeysData>,
|
||||||
|
MasterPasswordHash: String,
|
||||||
|
MasterPasswordHint: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
orgIdentifier: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct KeysData {
|
struct KeysData {
|
||||||
|
@ -85,6 +101,13 @@ struct KeysData {
|
||||||
PublicKey: String,
|
PublicKey: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct TokenPayload {
|
||||||
|
exp: i64,
|
||||||
|
email: String,
|
||||||
|
nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
||||||
fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
|
fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
|
||||||
match password_hint {
|
match password_hint {
|
||||||
|
@ -215,6 +238,50 @@ pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> Json
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/accounts/set-password", data = "<data>")]
|
||||||
|
async fn post_set_password(data: JsonUpcase<SetPasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
|
let data: SetPasswordData = data.into_inner().data;
|
||||||
|
let mut user = headers.user;
|
||||||
|
|
||||||
|
// Check against the password hint setting here so if it fails, the user
|
||||||
|
// can retry without losing their invitation below.
|
||||||
|
let password_hint = clean_password_hint(&data.MasterPasswordHint);
|
||||||
|
enforce_password_hint_setting(&password_hint)?;
|
||||||
|
|
||||||
|
if let Some(client_kdf_iter) = data.KdfIterations {
|
||||||
|
user.client_kdf_iter = client_kdf_iter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(client_kdf_type) = data.Kdf {
|
||||||
|
user.client_kdf_type = client_kdf_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to allow revision-date to use the old security_timestamp
|
||||||
|
let routes = vec!["revision_date"];
|
||||||
|
let routes: Option<Vec<String>> = Some(routes.iter().map(ToString::to_string).collect());
|
||||||
|
|
||||||
|
user.client_kdf_memory = data.KdfMemory;
|
||||||
|
user.client_kdf_parallelism = data.KdfParallelism;
|
||||||
|
|
||||||
|
user.set_password(&data.MasterPasswordHash, Some(data.Key), false, routes);
|
||||||
|
user.password_hint = password_hint;
|
||||||
|
|
||||||
|
if let Some(keys) = data.Keys {
|
||||||
|
user.private_key = Some(keys.EncryptedPrivateKey);
|
||||||
|
user.public_key = Some(keys.PublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() {
|
||||||
|
mail::send_set_password(&user.email.to_lowercase(), &user.name).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.save(&mut conn).await?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Object": "set-password",
|
||||||
|
"CaptchaBypassToken": "",
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/accounts/profile")]
|
#[get("/accounts/profile")]
|
||||||
async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
||||||
Json(headers.user.to_json(&mut conn).await)
|
Json(headers.user.to_json(&mut conn).await)
|
||||||
|
@ -835,7 +902,7 @@ struct SecretVerificationRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/accounts/verify-password", data = "<data>")]
|
#[post("/accounts/verify-password", data = "<data>")]
|
||||||
fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
|
fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> JsonResult {
|
||||||
let data: SecretVerificationRequest = data.into_inner().data;
|
let data: SecretVerificationRequest = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
|
@ -843,7 +910,9 @@ fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers
|
||||||
err!("Invalid password")
|
err!("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(Json(json!({
|
||||||
|
"MasterPasswordPolicy": {}, // Required for SSO login with mobile apps
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn _api_key(
|
async fn _api_key(
|
||||||
|
|
|
@ -40,6 +40,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
post_organization_collection_delete,
|
post_organization_collection_delete,
|
||||||
bulk_delete_organization_collections,
|
bulk_delete_organization_collections,
|
||||||
get_org_details,
|
get_org_details,
|
||||||
|
get_org_domain_sso_details,
|
||||||
get_org_users,
|
get_org_users,
|
||||||
send_invite,
|
send_invite,
|
||||||
reinvite_user,
|
reinvite_user,
|
||||||
|
@ -56,6 +57,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
post_org_import,
|
post_org_import,
|
||||||
list_policies,
|
list_policies,
|
||||||
list_policies_token,
|
list_policies_token,
|
||||||
|
list_policies_invited_user,
|
||||||
get_policy,
|
get_policy,
|
||||||
put_policy,
|
put_policy,
|
||||||
get_organization_tax,
|
get_organization_tax,
|
||||||
|
@ -95,6 +97,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
get_org_export,
|
get_org_export,
|
||||||
api_key,
|
api_key,
|
||||||
rotate_api_key,
|
rotate_api_key,
|
||||||
|
get_auto_enroll_status,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +307,13 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json<Value>
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/organizations/<_identifier>/auto-enroll-status")]
|
||||||
|
fn get_auto_enroll_status(_identifier: String) -> JsonResult {
|
||||||
|
Ok(Json(json!({
|
||||||
|
"ResetPasswordEnabled": false, // Not implemented
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/collections")]
|
#[get("/organizations/<org_id>/collections")]
|
||||||
async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
|
async fn get_org_collections(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
|
||||||
Json(json!({
|
Json(json!({
|
||||||
|
@ -781,6 +791,14 @@ async fn _get_org_details(org_id: &str, host: &str, user_uuid: &str, conn: &mut
|
||||||
json!(ciphers_json)
|
json!(ciphers_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[post("/organizations/domain/sso/details")]
|
||||||
|
fn get_org_domain_sso_details() -> JsonResult {
|
||||||
|
Ok(Json(json!({
|
||||||
|
"organizationIdentifier": "vaultwarden",
|
||||||
|
"ssoAvailable": CONFIG.sso_enabled()
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
struct GetOrgUserData {
|
struct GetOrgUserData {
|
||||||
#[field(name = "includeCollections")]
|
#[field(name = "includeCollections")]
|
||||||
|
@ -1660,6 +1678,25 @@ async fn list_policies_token(org_id: &str, token: &str, mut conn: DbConn) -> Jso
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[get("/organizations/<org_id>/policies/invited-user?<userId>")]
|
||||||
|
async fn list_policies_invited_user(org_id: String, userId: String, mut conn: DbConn) -> JsonResult {
|
||||||
|
// We should confirm the user is part of the organization, but unique domain_hints must be supported first.
|
||||||
|
|
||||||
|
if userId.is_empty() {
|
||||||
|
err!("userId must not be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
|
||||||
|
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"Data": policies_json,
|
||||||
|
"Object": "list",
|
||||||
|
"ContinuationToken": null
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/organizations/<org_id>/policies/<pol_type>")]
|
#[get("/organizations/<org_id>/policies/<pol_type>")]
|
||||||
async fn get_policy(org_id: &str, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
async fn get_policy(org_id: &str, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
|
||||||
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
let pol_type_enum = match OrgPolicyType::from_i32(pol_type) {
|
||||||
|
|
|
@ -26,9 +26,7 @@ async fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let type_ = TwoFactorType::Authenticator as i32;
|
let type_ = TwoFactorType::Authenticator as i32;
|
||||||
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await;
|
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &mut conn).await;
|
||||||
|
@ -60,15 +58,12 @@ async fn activate_authenticator(
|
||||||
mut conn: DbConn,
|
mut conn: DbConn,
|
||||||
) -> JsonResult {
|
) -> JsonResult {
|
||||||
let data: EnableAuthenticatorData = data.into_inner().data;
|
let data: EnableAuthenticatorData = data.into_inner().data;
|
||||||
let password_hash = data.MasterPasswordHash;
|
|
||||||
let key = data.Key;
|
let key = data.Key;
|
||||||
let token = data.Token.into_string();
|
let token = data.Token.into_string();
|
||||||
|
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&password_hash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate key as base32 and 20 bytes length
|
// Validate key as base32 and 20 bytes length
|
||||||
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
|
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
|
||||||
|
|
|
@ -95,9 +95,7 @@ const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leav
|
||||||
async fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&headers.user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let data = get_user_duo_data(&headers.user.uuid, &mut conn).await;
|
let data = get_user_duo_data(&headers.user.uuid, &mut conn).await;
|
||||||
|
|
||||||
|
@ -159,9 +157,7 @@ async fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, mut con
|
||||||
let data: EnableDuoData = data.into_inner().data;
|
let data: EnableDuoData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (data, data_str) = if check_duo_fields_custom(&data) {
|
let (data, data_str) = if check_duo_fields_custom(&data) {
|
||||||
let data_req: DuoData = data.into();
|
let data_req: DuoData = data.into();
|
||||||
|
|
|
@ -80,9 +80,7 @@ async fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: D
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (enabled, mfa_email) =
|
let (enabled, mfa_email) =
|
||||||
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &mut conn).await {
|
match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::Email as i32, &mut conn).await {
|
||||||
|
@ -114,9 +112,7 @@ async fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, mut conn:
|
||||||
let data: SendEmailData = data.into_inner().data;
|
let data: SendEmailData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !CONFIG._enable_email_2fa() {
|
if !CONFIG._enable_email_2fa() {
|
||||||
err!("Email 2FA is disabled")
|
err!("Email 2FA is disabled")
|
||||||
|
@ -154,9 +150,7 @@ async fn email(data: JsonUpcase<EmailData>, headers: Headers, mut conn: DbConn)
|
||||||
let data: EmailData = data.into_inner().data;
|
let data: EmailData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
||||||
let mut twofactor =
|
let mut twofactor =
|
||||||
|
|
|
@ -5,7 +5,7 @@ use rocket::Route;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{core::log_user_event, JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
api::{core::log_user_event, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData},
|
||||||
auth::{ClientHeaders, Headers},
|
auth::{ClientHeaders, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
db::{models::*, DbConn, DbPool},
|
db::{models::*, DbConn, DbPool},
|
||||||
|
@ -54,9 +54,7 @@ fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(json!({
|
Ok(Json(json!({
|
||||||
"Code": user.totp_recover,
|
"Code": user.totp_recover,
|
||||||
|
@ -84,10 +82,7 @@ async fn recover(data: JsonUpcase<RecoverTwoFactor>, client_headers: ClientHeade
|
||||||
None => err!("Username or password is incorrect. Try again."),
|
None => err!("Username or password is incorrect. Try again."),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check password
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
||||||
err!("Username or password is incorrect. Try again.")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if recovery code is correct
|
// Check if recovery code is correct
|
||||||
if !user.check_valid_recovery_code(&data.RecoveryCode) {
|
if !user.check_valid_recovery_code(&data.RecoveryCode) {
|
||||||
|
@ -224,3 +219,16 @@ fn get_device_verification_settings(_headers: Headers, _conn: DbConn) -> Json<Va
|
||||||
"object":"deviceVerificationSettings"
|
"object":"deviceVerificationSettings"
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2FA is broken with SSO, prevent activation if password login is disabled
|
||||||
|
fn authenticator_activation_check(user: &User, password_hash: &str) -> EmptyResult {
|
||||||
|
if !user.check_valid_password(password_hash) {
|
||||||
|
err!("Invalid password");
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.sso_enabled() && CONFIG.sso_only() {
|
||||||
|
err!("Cannot activate 2FA when SSO is the only login option");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -108,9 +108,7 @@ async fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, mut conn
|
||||||
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
err!("`DOMAIN` environment variable is not set. Webauthn disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&headers.user, &data.data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &mut conn).await?;
|
let (enabled, registrations) = get_webauthn_registrations(&headers.user.uuid, &mut conn).await?;
|
||||||
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
let registrations_json: Vec<Value> = registrations.iter().map(WebauthnRegistration::to_json).collect();
|
||||||
|
@ -124,9 +122,7 @@ async fn get_webauthn(data: JsonUpcase<PasswordData>, headers: Headers, mut conn
|
||||||
|
|
||||||
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
#[post("/two-factor/get-webauthn-challenge", data = "<data>")]
|
||||||
async fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
async fn generate_webauthn_challenge(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
||||||
if !headers.user.check_valid_password(&data.data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&headers.user, &data.data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let registrations = get_webauthn_registrations(&headers.user.uuid, &mut conn)
|
let registrations = get_webauthn_registrations(&headers.user.uuid, &mut conn)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
@ -90,9 +90,7 @@ async fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, mut
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_uuid = &user.uuid;
|
let user_uuid = &user.uuid;
|
||||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
let yubikey_type = TwoFactorType::YubiKey as i32;
|
||||||
|
@ -122,9 +120,7 @@ async fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers,
|
||||||
let data: EnableYubikeyData = data.into_inner().data;
|
let data: EnableYubikeyData = data.into_inner().data;
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
|
|
||||||
if !user.check_valid_password(&data.MasterPasswordHash) {
|
crate::api::core::two_factor::authenticator_activation_check(&user, &data.MasterPasswordHash)?;
|
||||||
err!("Invalid password");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we already have some data
|
// Check if we already have some data
|
||||||
let mut yubikey_data =
|
let mut yubikey_data =
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use jsonwebtoken::DecodingKey;
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
form::{Form, FromForm},
|
form::{Form, FromForm},
|
||||||
|
http::CookieJar,
|
||||||
Route,
|
Route,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
@ -14,14 +16,16 @@ use crate::{
|
||||||
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
core::two_factor::{duo, email, email::EmailTokenData, yubikey},
|
||||||
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
ApiResult, EmptyResult, JsonResult, JsonUpcase,
|
||||||
},
|
},
|
||||||
auth::{generate_organization_api_key_login_claims, ClientHeaders, ClientIp},
|
auth::{encode_jwt, generate_organization_api_key_login_claims, generate_ssotoken_claims, ClientHeaders, ClientIp},
|
||||||
db::{models::*, DbConn},
|
db::{models::*, DbConn},
|
||||||
error::MapResult,
|
error::MapResult,
|
||||||
mail, util, CONFIG,
|
mail, util,
|
||||||
|
util::{CookieManager, CustomRedirect},
|
||||||
|
CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login, prelogin, identity_register]
|
routes![login, prelogin, identity_register, prevalidate, authorize, oidcsignin]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/connect/token", data = "<data>")]
|
#[post("/connect/token", data = "<data>")]
|
||||||
|
@ -58,6 +62,15 @@ async fn login(data: Form<ConnectData>, client_header: ClientHeaders, mut conn:
|
||||||
|
|
||||||
_api_key_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
_api_key_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||||
}
|
}
|
||||||
|
"authorization_code" => {
|
||||||
|
_check_is_some(&data.client_id, "client_id cannot be blank")?;
|
||||||
|
_check_is_some(&data.code, "code cannot be blank")?;
|
||||||
|
|
||||||
|
_check_is_some(&data.device_identifier, "device_identifier cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_name, "device_name cannot be blank")?;
|
||||||
|
_check_is_some(&data.device_type, "device_type cannot be blank")?;
|
||||||
|
_authorization_login(data, &mut user_uuid, &mut conn, &client_header.ip).await
|
||||||
|
}
|
||||||
t => err!("Invalid type", t),
|
t => err!("Invalid type", t),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -127,6 +140,142 @@ async fn _refresh_login(data: ConnectData, conn: &mut DbConn) -> JsonResult {
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct TokenPayload {
|
||||||
|
exp: i64,
|
||||||
|
email: Option<String>,
|
||||||
|
nonce: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _authorization_login(
|
||||||
|
data: ConnectData,
|
||||||
|
user_uuid: &mut Option<String>,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
ip: &ClientIp,
|
||||||
|
) -> JsonResult {
|
||||||
|
let scope = match data.scope.as_ref() {
|
||||||
|
None => err!("Got no scope in OIDC data"),
|
||||||
|
Some(scope) => scope,
|
||||||
|
};
|
||||||
|
if scope != "api offline_access" {
|
||||||
|
err!("Scope not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope_vec = vec!["api".into(), "offline_access".into()];
|
||||||
|
let code = match data.code.as_ref() {
|
||||||
|
None => err!("Got no code in OIDC data"),
|
||||||
|
Some(code) => code,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (refresh_token, id_token, user_info) = match get_auth_code_access_token(code).await {
|
||||||
|
Ok((refresh_token, id_token, user_info)) => (refresh_token, id_token, user_info),
|
||||||
|
Err(_err) => err!("Could not retrieve access token"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut validation = jsonwebtoken::Validation::default();
|
||||||
|
validation.insecure_disable_signature_validation();
|
||||||
|
|
||||||
|
let token =
|
||||||
|
match jsonwebtoken::decode::<TokenPayload>(id_token.as_str(), &DecodingKey::from_secret(&[]), &validation) {
|
||||||
|
Err(_err) => err!("Could not decode id token"),
|
||||||
|
Ok(payload) => payload.claims,
|
||||||
|
};
|
||||||
|
|
||||||
|
// let expiry = token.exp;
|
||||||
|
let nonce = token.nonce;
|
||||||
|
let mut new_user = false;
|
||||||
|
|
||||||
|
match SsoNonce::find(&nonce, conn).await {
|
||||||
|
Some(sso_nonce) => {
|
||||||
|
match sso_nonce.delete(conn).await {
|
||||||
|
Ok(_) => {
|
||||||
|
let user_email = match token.email {
|
||||||
|
Some(email) => email,
|
||||||
|
None => match user_info.email() {
|
||||||
|
None => err!("Neither id token nor userinfo contained an email"),
|
||||||
|
Some(email) => email.to_owned().to_string(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let now = Utc::now().naive_utc();
|
||||||
|
|
||||||
|
let mut user = match User::find_by_mail(&user_email, conn).await {
|
||||||
|
Some(user) => user,
|
||||||
|
None => {
|
||||||
|
new_user = true;
|
||||||
|
User::new(user_email.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if new_user {
|
||||||
|
user.verified_at = Some(Utc::now().naive_utc());
|
||||||
|
user.save(conn).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the user_uuid here to be passed back used for event logging.
|
||||||
|
*user_uuid = Some(user.uuid.clone());
|
||||||
|
|
||||||
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
|
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, true, conn).await?;
|
||||||
|
|
||||||
|
if CONFIG.mail_enabled() && new_device {
|
||||||
|
if let Err(e) =
|
||||||
|
mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await
|
||||||
|
{
|
||||||
|
error!("Error sending new device email: {:#?}", e);
|
||||||
|
|
||||||
|
if CONFIG.require_device_email() {
|
||||||
|
err!("Could not send login notification email. Please contact your administrator.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CONFIG.sso_acceptall_invites() {
|
||||||
|
for user_org in UserOrganization::find_invited_by_user(&user.uuid, conn).await.iter_mut() {
|
||||||
|
user_org.status = UserOrgStatus::Accepted as i32;
|
||||||
|
user_org.save(conn).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device.refresh_token = refresh_token.clone();
|
||||||
|
device.save(conn).await?;
|
||||||
|
|
||||||
|
let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, conn).await;
|
||||||
|
let (access_token, expires_in) = device.refresh_tokens(&user, orgs, scope_vec);
|
||||||
|
device.save(conn).await?;
|
||||||
|
|
||||||
|
let mut result = json!({
|
||||||
|
"access_token": access_token,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"refresh_token": device.refresh_token,
|
||||||
|
"expires_in": expires_in,
|
||||||
|
"Key": user.akey,
|
||||||
|
"PrivateKey": user.private_key,
|
||||||
|
"Kdf": user.client_kdf_type,
|
||||||
|
"KdfIterations": user.client_kdf_iter,
|
||||||
|
"KdfMemory": user.client_kdf_memory,
|
||||||
|
"KdfParallelism": user.client_kdf_parallelism,
|
||||||
|
"ResetMasterPassword": user.password_hash.is_empty(),
|
||||||
|
"scope": scope,
|
||||||
|
"unofficialServer": true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(token) = twofactor_token {
|
||||||
|
result["TwoFactorToken"] = Value::String(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("User {} logged in successfully. IP: {}", user.email, ip.ip);
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
Err(_) => err!("Failed to delete nonce"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
err!("Invalid nonce")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn _password_login(
|
async fn _password_login(
|
||||||
data: ConnectData,
|
data: ConnectData,
|
||||||
user_uuid: &mut Option<String>,
|
user_uuid: &mut Option<String>,
|
||||||
|
@ -143,6 +292,10 @@ async fn _password_login(
|
||||||
// Ratelimit the login
|
// Ratelimit the login
|
||||||
crate::ratelimit::check_limit_login(&ip.ip)?;
|
crate::ratelimit::check_limit_login(&ip.ip)?;
|
||||||
|
|
||||||
|
if CONFIG.sso_enabled() && CONFIG.sso_only() {
|
||||||
|
err!("SSO sign-in is required");
|
||||||
|
}
|
||||||
|
|
||||||
// Get the user
|
// Get the user
|
||||||
let username = data.username.as_ref().unwrap().trim();
|
let username = data.username.as_ref().unwrap().trim();
|
||||||
let mut user = match User::find_by_mail(username, conn).await {
|
let mut user = match User::find_by_mail(username, conn).await {
|
||||||
|
@ -242,7 +395,7 @@ async fn _password_login(
|
||||||
|
|
||||||
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
let (mut device, new_device) = get_device(&data, conn, &user).await;
|
||||||
|
|
||||||
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, conn).await?;
|
let twofactor_token = twofactor_auth(&user.uuid, &data, &mut device, ip, false, conn).await?;
|
||||||
|
|
||||||
if CONFIG.mail_enabled() && new_device {
|
if CONFIG.mail_enabled() && new_device {
|
||||||
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
if let Err(e) = mail::send_new_device_logged_in(&user.email, &ip.ip.to_string(), &now, &device.name).await {
|
||||||
|
@ -453,6 +606,7 @@ async fn twofactor_auth(
|
||||||
data: &ConnectData,
|
data: &ConnectData,
|
||||||
device: &mut Device,
|
device: &mut Device,
|
||||||
ip: &ClientIp,
|
ip: &ClientIp,
|
||||||
|
is_sso: bool,
|
||||||
conn: &mut DbConn,
|
conn: &mut DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn).await;
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn).await;
|
||||||
|
@ -469,7 +623,17 @@ async fn twofactor_auth(
|
||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn).await?, "2FA token not provided"),
|
None => {
|
||||||
|
if is_sso {
|
||||||
|
if CONFIG.sso_only() {
|
||||||
|
err!("2FA not supported with SSO login, contact your administrator");
|
||||||
|
} else {
|
||||||
|
err!("2FA not supported with SSO login, log in directly using email and master password");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn).await?, "2FA token not provided");
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
let selected_twofactor = twofactors.into_iter().find(|tf| tf.atype == selected_id && tf.enabled);
|
||||||
|
@ -668,11 +832,187 @@ struct ConnectData {
|
||||||
two_factor_remember: Option<i32>,
|
two_factor_remember: Option<i32>,
|
||||||
#[field(name = uncased("authrequest"))]
|
#[field(name = uncased("authrequest"))]
|
||||||
auth_request: Option<String>,
|
auth_request: Option<String>,
|
||||||
|
// Needed for authorization code
|
||||||
|
#[form(field = uncased("code"))]
|
||||||
|
code: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
fn _check_is_some<T>(value: &Option<T>, msg: &str) -> EmptyResult {
|
||||||
if value.is_none() {
|
if value.is_none() {
|
||||||
err!(msg)
|
err!(msg)
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/account/prevalidate")]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn prevalidate() -> JsonResult {
|
||||||
|
let claims = generate_ssotoken_claims();
|
||||||
|
let ssotoken = encode_jwt(&claims);
|
||||||
|
Ok(Json(json!({
|
||||||
|
"token": ssotoken,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
use openidconnect::core::{CoreClient, CoreProviderMetadata, CoreResponseType, CoreUserInfoClaims};
|
||||||
|
use openidconnect::reqwest::async_http_client;
|
||||||
|
use openidconnect::{
|
||||||
|
AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, OAuth2TokenResponse,
|
||||||
|
RedirectUrl, Scope,
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn get_client_from_sso_config() -> ApiResult<CoreClient> {
|
||||||
|
let redirect = CONFIG.sso_callback_path();
|
||||||
|
let client_id = ClientId::new(CONFIG.sso_client_id());
|
||||||
|
let client_secret = ClientSecret::new(CONFIG.sso_client_secret());
|
||||||
|
let issuer_url = match IssuerUrl::new(CONFIG.sso_authority()) {
|
||||||
|
Ok(issuer) => issuer,
|
||||||
|
Err(_err) => err!("invalid issuer URL"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let provider_metadata = match CoreProviderMetadata::discover_async(issuer_url, async_http_client).await {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(_err) => {
|
||||||
|
err!("Failed to discover OpenID provider")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let redirect_uri = match RedirectUrl::new(redirect) {
|
||||||
|
Ok(uri) => uri,
|
||||||
|
Err(err) => err!("Invalid redirection url: {}", err.to_string()),
|
||||||
|
};
|
||||||
|
let client = CoreClient::from_provider_metadata(provider_metadata, client_id, Some(client_secret))
|
||||||
|
.set_redirect_uri(redirect_uri);
|
||||||
|
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/connect/oidc-signin?<code>")]
|
||||||
|
fn oidcsignin(code: String, jar: &CookieJar<'_>, _conn: DbConn) -> ApiResult<CustomRedirect> {
|
||||||
|
let cookiemanager = CookieManager::new(jar);
|
||||||
|
|
||||||
|
let redirect_uri = match cookiemanager.get_cookie("redirect_uri".to_string()) {
|
||||||
|
None => err!("No redirect_uri in cookie"),
|
||||||
|
Some(uri) => uri,
|
||||||
|
};
|
||||||
|
let orig_state = match cookiemanager.get_cookie("state".to_string()) {
|
||||||
|
None => err!("No state in cookie"),
|
||||||
|
Some(state) => state,
|
||||||
|
};
|
||||||
|
|
||||||
|
cookiemanager.delete_cookie("redirect_uri".to_string());
|
||||||
|
cookiemanager.delete_cookie("state".to_string());
|
||||||
|
|
||||||
|
let redirect = CustomRedirect {
|
||||||
|
url: format!("{redirect_uri}?code={code}&state={orig_state}"),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(redirect)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct AuthorizeData {
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("client_id"))]
|
||||||
|
#[field(name = uncased("clientid"))]
|
||||||
|
client_id: Option<String>,
|
||||||
|
#[field(name = uncased("redirect_uri"))]
|
||||||
|
#[field(name = uncased("redirecturi"))]
|
||||||
|
redirect_uri: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("response_type"))]
|
||||||
|
#[field(name = uncased("responsetype"))]
|
||||||
|
response_type: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("scope"))]
|
||||||
|
scope: Option<String>,
|
||||||
|
#[field(name = uncased("state"))]
|
||||||
|
state: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("code_challenge"))]
|
||||||
|
code_challenge: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("code_challenge_method"))]
|
||||||
|
code_challenge_method: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("response_mode"))]
|
||||||
|
response_mode: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("domain_hint"))]
|
||||||
|
domain_hint: Option<String>,
|
||||||
|
#[allow(unused)]
|
||||||
|
#[field(name = uncased("ssoToken"))]
|
||||||
|
ssoToken: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/connect/authorize?<data..>")]
|
||||||
|
async fn authorize(data: AuthorizeData, jar: &CookieJar<'_>, mut conn: DbConn) -> ApiResult<CustomRedirect> {
|
||||||
|
let cookiemanager = CookieManager::new(jar);
|
||||||
|
match get_client_from_sso_config().await {
|
||||||
|
Ok(client) => {
|
||||||
|
let (auth_url, _csrf_state, nonce) = client
|
||||||
|
.authorize_url(
|
||||||
|
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
)
|
||||||
|
.add_scope(Scope::new("email".to_string()))
|
||||||
|
.add_scope(Scope::new("profile".to_string()))
|
||||||
|
.url();
|
||||||
|
|
||||||
|
let sso_nonce = SsoNonce::new(nonce.secret().to_string());
|
||||||
|
sso_nonce.save(&mut conn).await?;
|
||||||
|
|
||||||
|
let redirect_uri = match data.redirect_uri {
|
||||||
|
None => err!("No redirect_uri in data"),
|
||||||
|
Some(uri) => uri,
|
||||||
|
};
|
||||||
|
cookiemanager.set_cookie("redirect_uri".to_string(), redirect_uri);
|
||||||
|
let state = match data.state {
|
||||||
|
None => err!("No state in data"),
|
||||||
|
Some(state) => state,
|
||||||
|
};
|
||||||
|
cookiemanager.set_cookie("state".to_string(), state);
|
||||||
|
|
||||||
|
let redirect = CustomRedirect {
|
||||||
|
url: format!("{}", auth_url),
|
||||||
|
headers: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(redirect)
|
||||||
|
}
|
||||||
|
Err(_err) => err!("Unable to find client from identifier"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_auth_code_access_token(code: &str) -> ApiResult<(String, String, CoreUserInfoClaims)> {
|
||||||
|
let oidc_code = AuthorizationCode::new(String::from(code));
|
||||||
|
match get_client_from_sso_config().await {
|
||||||
|
Ok(client) => match client.exchange_code(oidc_code).request_async(async_http_client).await {
|
||||||
|
Ok(token_response) => {
|
||||||
|
let refresh_token = match token_response.refresh_token() {
|
||||||
|
Some(token) => token.secret().to_string(),
|
||||||
|
None => String::new(),
|
||||||
|
};
|
||||||
|
let id_token = match token_response.extra_fields().id_token() {
|
||||||
|
None => err!("Token response did not contain an id_token"),
|
||||||
|
Some(token) => token.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_info: CoreUserInfoClaims =
|
||||||
|
match client.user_info(token_response.access_token().to_owned(), None) {
|
||||||
|
Err(_err) => err!("Token response did not contain user_info"),
|
||||||
|
Ok(info) => match info.request_async(async_http_client).await {
|
||||||
|
Err(_err) => err!("Request to user_info endpoint failed"),
|
||||||
|
Ok(claim) => claim,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((refresh_token, id_token, user_info))
|
||||||
|
}
|
||||||
|
Err(err) => err!("Failed to contact token endpoint: {}", err.to_string()),
|
||||||
|
},
|
||||||
|
Err(_err) => err!("Unable to find client"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
src/auth.rs
23
src/auth.rs
|
@ -19,6 +19,7 @@ pub static JWT_LOGIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|login", CON
|
||||||
static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin()));
|
static JWT_INVITE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|invite", CONFIG.domain_origin()));
|
||||||
static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy<String> =
|
static JWT_EMERGENCY_ACCESS_INVITE_ISSUER: Lazy<String> =
|
||||||
Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin()));
|
Lazy::new(|| format!("{}|emergencyaccessinvite", CONFIG.domain_origin()));
|
||||||
|
static JWT_SSOTOKEN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|ssotoken", CONFIG.domain_origin()));
|
||||||
static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin()));
|
static JWT_DELETE_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|delete", CONFIG.domain_origin()));
|
||||||
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
static JWT_VERIFYEMAIL_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|verifyemail", CONFIG.domain_origin()));
|
||||||
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
static JWT_ADMIN_ISSUER: Lazy<String> = Lazy::new(|| format!("{}|admin", CONFIG.domain_origin()));
|
||||||
|
@ -287,6 +288,28 @@ pub fn generate_delete_claims(uuid: String) -> BasicJwtClaims {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SsoTokenJwtClaims {
|
||||||
|
// Not before
|
||||||
|
pub nbf: i64,
|
||||||
|
// Expiration time
|
||||||
|
pub exp: i64,
|
||||||
|
// Issuer
|
||||||
|
pub iss: String,
|
||||||
|
// Subject
|
||||||
|
pub sub: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_ssotoken_claims() -> SsoTokenJwtClaims {
|
||||||
|
let time_now = Utc::now().naive_utc();
|
||||||
|
SsoTokenJwtClaims {
|
||||||
|
nbf: time_now.timestamp(),
|
||||||
|
exp: (time_now + Duration::minutes(2)).timestamp(),
|
||||||
|
iss: JWT_SSOTOKEN_ISSUER.to_string(),
|
||||||
|
sub: "vaultwarden".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims {
|
pub fn generate_verify_email_claims(uuid: String) -> BasicJwtClaims {
|
||||||
let time_now = Utc::now().naive_utc();
|
let time_now = Utc::now().naive_utc();
|
||||||
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
let expire_hours = i64::from(CONFIG.invitation_expiration_hours());
|
||||||
|
|
|
@ -602,6 +602,24 @@ make_config! {
|
||||||
org_groups_enabled: bool, false, def, false;
|
org_groups_enabled: bool, false, def, false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// OpenID Connect SSO settings
|
||||||
|
sso {
|
||||||
|
/// Enabled
|
||||||
|
sso_enabled: bool, true, def, false;
|
||||||
|
/// Force SSO login
|
||||||
|
sso_only: bool, true, def, false;
|
||||||
|
/// Client ID
|
||||||
|
sso_client_id: String, true, def, String::new();
|
||||||
|
/// Client Key
|
||||||
|
sso_client_secret: Pass, true, def, String::new();
|
||||||
|
/// Authority Server
|
||||||
|
sso_authority: String, true, def, String::new();
|
||||||
|
/// CallBack Path
|
||||||
|
sso_callback_path: String, false, gen, |c| generate_sso_callback_path(&c.domain);
|
||||||
|
/// Allow workaround so SSO logins accept all invites
|
||||||
|
sso_acceptall_invites: bool, true, def, false;
|
||||||
|
},
|
||||||
|
|
||||||
/// Yubikey settings
|
/// Yubikey settings
|
||||||
yubico: _enable_yubico {
|
yubico: _enable_yubico {
|
||||||
/// Enabled
|
/// Enabled
|
||||||
|
@ -756,6 +774,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
err!("All Duo options need to be set for global Duo support")
|
err!("All Duo options need to be set for global Duo support")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.sso_enabled
|
||||||
|
&& (cfg.sso_client_id.is_empty() || cfg.sso_client_secret.is_empty() || cfg.sso_authority.is_empty())
|
||||||
|
{
|
||||||
|
err!("`SSO_CLIENT_ID`, `SSO_CLIENT_SECRET` and `SSO_AUTHORITY` must be set for SSO support")
|
||||||
|
}
|
||||||
|
|
||||||
if cfg._enable_yubico {
|
if cfg._enable_yubico {
|
||||||
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() {
|
||||||
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support")
|
err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` must be set for Yubikey OTP support")
|
||||||
|
@ -952,6 +976,10 @@ fn generate_smtp_img_src(embed_images: bool, domain: &str) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_sso_callback_path(domain: &str) -> String {
|
||||||
|
format!("{domain}/identity/connect/oidc-signin")
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate the correct URL for the icon service.
|
/// Generate the correct URL for the icon service.
|
||||||
/// This will be used within icons.rs to call the external icon service.
|
/// This will be used within icons.rs to call the external icon service.
|
||||||
fn generate_icon_service_url(icon_service: &str) -> String {
|
fn generate_icon_service_url(icon_service: &str) -> String {
|
||||||
|
@ -1247,6 +1275,7 @@ where
|
||||||
reg!("email/send_single_org_removed_from_org", ".html");
|
reg!("email/send_single_org_removed_from_org", ".html");
|
||||||
reg!("email/send_org_invite", ".html");
|
reg!("email/send_org_invite", ".html");
|
||||||
reg!("email/send_emergency_access_invite", ".html");
|
reg!("email/send_emergency_access_invite", ".html");
|
||||||
|
reg!("email/set_password", ".html");
|
||||||
reg!("email/twofactor_email", ".html");
|
reg!("email/twofactor_email", ".html");
|
||||||
reg!("email/verify_email", ".html");
|
reg!("email/verify_email", ".html");
|
||||||
reg!("email/welcome", ".html");
|
reg!("email/welcome", ".html");
|
||||||
|
|
|
@ -11,6 +11,7 @@ mod group;
|
||||||
mod org_policy;
|
mod org_policy;
|
||||||
mod organization;
|
mod organization;
|
||||||
mod send;
|
mod send;
|
||||||
|
mod sso_nonce;
|
||||||
mod two_factor;
|
mod two_factor;
|
||||||
mod two_factor_incomplete;
|
mod two_factor_incomplete;
|
||||||
mod user;
|
mod user;
|
||||||
|
@ -28,6 +29,7 @@ pub use self::group::{CollectionGroup, Group, GroupUser};
|
||||||
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType};
|
||||||
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
pub use self::organization::{Organization, OrganizationApiKey, UserOrgStatus, UserOrgType, UserOrganization};
|
||||||
pub use self::send::{Send, SendType};
|
pub use self::send::{Send, SendType};
|
||||||
|
pub use self::sso_nonce::SsoNonce;
|
||||||
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
pub use self::two_factor::{TwoFactor, TwoFactorType};
|
||||||
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
pub use self::two_factor_incomplete::TwoFactorIncomplete;
|
||||||
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
|
pub use self::user::{Invitation, User, UserKdfType, UserStampException};
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub enum OrgPolicyType {
|
||||||
MasterPassword = 1,
|
MasterPassword = 1,
|
||||||
PasswordGenerator = 2,
|
PasswordGenerator = 2,
|
||||||
SingleOrg = 3,
|
SingleOrg = 3,
|
||||||
// RequireSso = 4, // Not supported
|
RequireSso = 4,
|
||||||
PersonalOwnership = 5,
|
PersonalOwnership = 5,
|
||||||
DisableSend = 6,
|
DisableSend = 6,
|
||||||
SendOptions = 7,
|
SendOptions = 7,
|
||||||
|
|
|
@ -166,7 +166,7 @@ impl Organization {
|
||||||
"UseTotp": true,
|
"UseTotp": true,
|
||||||
"UsePolicies": true,
|
"UsePolicies": true,
|
||||||
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
// "UseScim": false, // Not supported (Not AGPLv3 Licensed)
|
||||||
"UseSso": false, // Not supported
|
"UseSso": CONFIG.sso_enabled(),
|
||||||
// "UseKeyConnector": false, // Not supported
|
// "UseKeyConnector": false, // Not supported
|
||||||
"SelfHost": true,
|
"SelfHost": true,
|
||||||
"UseApi": true,
|
"UseApi": true,
|
||||||
|
@ -346,7 +346,7 @@ impl UserOrganization {
|
||||||
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
"ResetPasswordEnrolled": self.reset_password_key.is_some(),
|
||||||
"UseResetPassword": CONFIG.mail_enabled(),
|
"UseResetPassword": CONFIG.mail_enabled(),
|
||||||
"SsoBound": false, // Not supported
|
"SsoBound": false, // Not supported
|
||||||
"UseSso": false, // Not supported
|
"UseSso": CONFIG.sso_enabled(),
|
||||||
"ProviderId": null,
|
"ProviderId": null,
|
||||||
"ProviderName": null,
|
"ProviderName": null,
|
||||||
// "KeyConnectorEnabled": false,
|
// "KeyConnectorEnabled": false,
|
||||||
|
|
60
src/db/models/sso_nonce.rs
Normal file
60
src/db/models/sso_nonce.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use crate::api::EmptyResult;
|
||||||
|
use crate::db::DbConn;
|
||||||
|
use crate::error::MapResult;
|
||||||
|
|
||||||
|
db_object! {
|
||||||
|
#[derive(Identifiable, Queryable, Insertable)]
|
||||||
|
#[diesel(table_name = sso_nonce)]
|
||||||
|
#[diesel(primary_key(nonce))]
|
||||||
|
pub struct SsoNonce {
|
||||||
|
pub nonce: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local methods
|
||||||
|
impl SsoNonce {
|
||||||
|
pub fn new(nonce: String) -> Self {
|
||||||
|
Self {
|
||||||
|
nonce,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Database methods
|
||||||
|
impl SsoNonce {
|
||||||
|
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn:
|
||||||
|
sqlite, mysql {
|
||||||
|
diesel::replace_into(sso_nonce::table)
|
||||||
|
.values(SsoNonceDb::to_db(self))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving SSO device")
|
||||||
|
}
|
||||||
|
postgresql {
|
||||||
|
let value = SsoNonceDb::to_db(self);
|
||||||
|
diesel::insert_into(sso_nonce::table)
|
||||||
|
.values(&value)
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error saving SSO nonce")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete(self, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::delete(sso_nonce::table.filter(sso_nonce::nonce.eq(self.nonce)))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error deleting SSO nonce")
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find(nonce: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
sso_nonce::table
|
||||||
|
.filter(sso_nonce::nonce.eq(nonce))
|
||||||
|
.first::<SsoNonceDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
|
@ -241,6 +241,12 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sso_nonce (nonce) {
|
||||||
|
nonce -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
|
|
@ -241,6 +241,12 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sso_nonce (nonce) {
|
||||||
|
nonce -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
|
|
@ -241,6 +241,12 @@ table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table! {
|
||||||
|
sso_nonce (nonce) {
|
||||||
|
nonce -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
emergency_access (uuid) {
|
emergency_access (uuid) {
|
||||||
uuid -> Text,
|
uuid -> Text,
|
||||||
|
|
12
src/mail.rs
12
src/mail.rs
|
@ -492,6 +492,18 @@ pub async fn send_change_email(address: &str, token: &str) -> EmptyResult {
|
||||||
send_email(address, &subject, body_html, body_text).await
|
send_email(address, &subject, body_html, body_text).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn send_set_password(address: &str, user_name: &str) -> EmptyResult {
|
||||||
|
let (subject, body_html, body_text) = get_text(
|
||||||
|
"email/set_password",
|
||||||
|
json!({
|
||||||
|
"url": CONFIG.domain(),
|
||||||
|
"img_src": CONFIG._smtp_img_src(),
|
||||||
|
"user_name": user_name,
|
||||||
|
}),
|
||||||
|
)?;
|
||||||
|
send_email(address, &subject, body_html, body_text).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_test(address: &str) -> EmptyResult {
|
pub async fn send_test(address: &str) -> EmptyResult {
|
||||||
let (subject, body_html, body_text) = get_text(
|
let (subject, body_html, body_text) = get_text(
|
||||||
"email/smtp_test",
|
"email/smtp_test",
|
||||||
|
|
6
src/static/templates/email/set_password.hbs
Normal file
6
src/static/templates/email/set_password.hbs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Master Password Has Been Changed
|
||||||
|
<!---------------->
|
||||||
|
The master password for {{user_name}} has been changed. If you did not initiate this request, please reach out to your administrator immediately.
|
||||||
|
|
||||||
|
===
|
||||||
|
{{> email/email_footer_text }}
|
11
src/static/templates/email/set_password.html.hbs
Normal file
11
src/static/templates/email/set_password.html.hbs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Master Password Has Been Changed
|
||||||
|
<!---------------->
|
||||||
|
{{> email/email_header }}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
The master password for <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{user_name}}</b> has been changed. If you did not initiate this request, please reach out to your administrator immediately.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{> email/email_footer }}
|
58
src/util.rs
58
src/util.rs
|
@ -8,7 +8,7 @@ use std::{
|
||||||
|
|
||||||
use rocket::{
|
use rocket::{
|
||||||
fairing::{Fairing, Info, Kind},
|
fairing::{Fairing, Info, Kind},
|
||||||
http::{ContentType, Header, HeaderMap, Method, Status},
|
http::{ContentType, Cookie, CookieJar, Header, HeaderMap, Method, SameSite, Status},
|
||||||
request::FromParam,
|
request::FromParam,
|
||||||
response::{self, Responder},
|
response::{self, Responder},
|
||||||
Data, Orbit, Request, Response, Rocket,
|
Data, Orbit, Request, Response, Rocket,
|
||||||
|
@ -115,8 +115,9 @@ impl Cors {
|
||||||
fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> {
|
fn get_allowed_origin(headers: &HeaderMap<'_>) -> Option<String> {
|
||||||
let origin = Cors::get_header(headers, "Origin");
|
let origin = Cors::get_header(headers, "Origin");
|
||||||
let domain_origin = CONFIG.domain_origin();
|
let domain_origin = CONFIG.domain_origin();
|
||||||
|
let sso_origin = CONFIG.sso_authority();
|
||||||
let safari_extension_origin = "file://";
|
let safari_extension_origin = "file://";
|
||||||
if origin == domain_origin || origin == safari_extension_origin {
|
if origin == domain_origin || origin == safari_extension_origin || origin == sso_origin {
|
||||||
Some(origin)
|
Some(origin)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -241,6 +242,33 @@ impl<'r> FromParam<'r> for SafeString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CustomRedirect {
|
||||||
|
pub url: String,
|
||||||
|
pub headers: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> rocket::response::Responder<'r, 'static> for CustomRedirect {
|
||||||
|
fn respond_to(self, _: &rocket::request::Request<'_>) -> rocket::response::Result<'static> {
|
||||||
|
let mut response = Response::build()
|
||||||
|
.status(rocket::http::Status {
|
||||||
|
code: 307,
|
||||||
|
})
|
||||||
|
.raw_header("Location", self.url)
|
||||||
|
.header(ContentType::HTML)
|
||||||
|
.finalize();
|
||||||
|
|
||||||
|
// Normal headers
|
||||||
|
response.set_raw_header("Referrer-Policy", "same-origin");
|
||||||
|
response.set_raw_header("X-XSS-Protection", "0");
|
||||||
|
|
||||||
|
for header in &self.headers {
|
||||||
|
response.set_raw_header(header.0.clone(), header.1.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Log all the routes from the main paths list, and the attachments endpoint
|
// Log all the routes from the main paths list, and the attachments endpoint
|
||||||
// Effectively ignores, any static file route, and the alive endpoint
|
// Effectively ignores, any static file route, and the alive endpoint
|
||||||
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
|
const LOGGED_ROUTES: [&str; 7] = ["/api", "/admin", "/identity", "/icons", "/attachments", "/events", "/notifications"];
|
||||||
|
@ -738,3 +766,29 @@ pub fn convert_json_key_lcase_first(src_json: Value) -> Value {
|
||||||
value => value,
|
value => value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct CookieManager<'a> {
|
||||||
|
jar: &'a CookieJar<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> CookieManager<'a> {
|
||||||
|
pub fn new(jar: &'a CookieJar<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
jar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cookie(&self, name: String, value: String) {
|
||||||
|
let cookie = Cookie::build(name, value).same_site(SameSite::Lax).finish();
|
||||||
|
|
||||||
|
self.jar.add(cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_cookie(&self, name: String) -> Option<String> {
|
||||||
|
self.jar.get(&name).map(|c| c.value().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_cookie(&self, name: String) {
|
||||||
|
self.jar.remove(Cookie::named(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue