Allow to login with OIDC
This commit is contained in:
parent
f556fec3bb
commit
7a2ff7ad1d
12 changed files with 782 additions and 29 deletions
288
Cargo.lock
generated
288
Cargo.lock
generated
|
|
@ -739,6 +739,7 @@ dependencies = [
|
||||||
"ident_case",
|
"ident_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
"strsim",
|
||||||
"syn 2.0.16",
|
"syn 2.0.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1000,6 +1001,7 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"serde",
|
"serde",
|
||||||
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
@ -1030,6 +1032,12 @@ 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.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ecdsa"
|
name = "ecdsa"
|
||||||
version = "0.16.7"
|
version = "0.16.7"
|
||||||
|
|
@ -1081,6 +1089,15 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumset"
|
name = "enumset"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -1410,8 +1427,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1509,6 +1528,25 @@ dependencies = [
|
||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.3.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"indexmap",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
|
@ -1715,6 +1753,7 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
|
@ -1728,6 +1767,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 0.21.5",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.56"
|
version = "0.1.56"
|
||||||
|
|
@ -1792,6 +1845,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"hashbrown 0.12.3",
|
"hashbrown 0.12.3",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1870,6 +1924,12 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnet"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
|
@ -1921,7 +1981,7 @@ dependencies = [
|
||||||
"p256",
|
"p256",
|
||||||
"p384",
|
"p384",
|
||||||
"rand",
|
"rand",
|
||||||
"rsa",
|
"rsa 0.7.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"spki 0.6.0",
|
"spki 0.6.0",
|
||||||
|
|
@ -2275,12 +2335,63 @@ 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 = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.17.1"
|
version = "1.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||||
|
|
||||||
|
[[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 0.10.5",
|
||||||
|
"log",
|
||||||
|
"oauth2",
|
||||||
|
"p256",
|
||||||
|
"p384",
|
||||||
|
"rand",
|
||||||
|
"rsa 0.9.2",
|
||||||
|
"serde",
|
||||||
|
"serde-value",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_path_to_error",
|
||||||
|
"serde_plain",
|
||||||
|
"serde_with",
|
||||||
|
"sha2",
|
||||||
|
"subtle",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-float"
|
name = "ordered-float"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
|
|
@ -2515,6 +2626,17 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pkcs1"
|
||||||
|
version = "0.7.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
|
||||||
|
dependencies = [
|
||||||
|
"der 0.7.6",
|
||||||
|
"pkcs8 0.10.2",
|
||||||
|
"spki 0.7.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -2737,6 +2859,8 @@ dependencies = [
|
||||||
"envious",
|
"envious",
|
||||||
"jwt-simple",
|
"jwt-simple",
|
||||||
"migration",
|
"migration",
|
||||||
|
"openidconnect",
|
||||||
|
"parking_lot 0.12.1",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"sea-query",
|
"sea-query",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2746,6 +2870,7 @@ dependencies = [
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2790,6 +2915,45 @@ dependencies = [
|
||||||
"bytecheck",
|
"bytecheck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.11.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.0",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"ipnet",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustls 0.21.5",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls 0.24.1",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
"winreg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rfc6979"
|
name = "rfc6979"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -2855,7 +3019,7 @@ dependencies = [
|
||||||
"num-integer",
|
"num-integer",
|
||||||
"num-iter",
|
"num-iter",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"pkcs1",
|
"pkcs1 0.4.1",
|
||||||
"pkcs8 0.9.0",
|
"pkcs8 0.9.0",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"signature 1.6.4",
|
"signature 1.6.4",
|
||||||
|
|
@ -2864,6 +3028,28 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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 0.7.5",
|
||||||
|
"pkcs8 0.10.2",
|
||||||
|
"rand_core",
|
||||||
|
"signature 2.1.0",
|
||||||
|
"spki 0.7.2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.29.1"
|
version = "1.29.1"
|
||||||
|
|
@ -2923,6 +3109,18 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.21.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"rustls-webpki",
|
||||||
|
"sct",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
@ -2932,6 +3130,16 @@ dependencies = [
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.101.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
|
|
@ -3219,6 +3427,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_plain"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6018081315db179d0ce57b1fe4b62a12a0028c9cf9bbef868c9cf477b3c34ae"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|
@ -3242,6 +3459,34 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.0",
|
||||||
|
"chrono",
|
||||||
|
"hex",
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_with_macros",
|
||||||
|
"time 0.3.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_with_macros"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.16",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
|
|
@ -3486,7 +3731,7 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rand",
|
"rand",
|
||||||
"rust_decimal",
|
"rust_decimal",
|
||||||
"rustls",
|
"rustls 0.20.8",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -3532,7 +3777,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.23.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3571,6 +3816,12 @@ dependencies = [
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strsim"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
|
|
@ -3753,11 +4004,21 @@ version = "0.23.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls 0.20.8",
|
||||||
"tokio",
|
"tokio",
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||||
|
dependencies = [
|
||||||
|
"rustls 0.21.5",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
|
|
@ -3780,6 +4041,7 @@ dependencies = [
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3982,8 +4244,15 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"idna",
|
"idna",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf-8"
|
name = "utf-8"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
|
|
@ -4360,6 +4629,15 @@ version = "0.48.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wyz"
|
name = "wyz"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ tower-http = { version = "0.4.0", features = ["cors", "fs"] }
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
sea-query = "0.28"
|
sea-query = "0.28"
|
||||||
|
openidconnect = "3.3.0"
|
||||||
envious = "0.2.2"
|
envious = "0.2.2"
|
||||||
|
parking_lot = "0.12.1"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
[dependencies.sea-orm]
|
[dependencies.sea-orm]
|
||||||
version = "0.11"
|
version = "0.11"
|
||||||
|
|
|
||||||
|
|
@ -21,5 +21,6 @@ itertools = "0.11.0"
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
pulldown-cmark = "0.9.3"
|
pulldown-cmark = "0.9.3"
|
||||||
serde = { version = "1.0.164", features = ["derive"] }
|
serde = { version = "1.0.164", features = ["derive"] }
|
||||||
|
urlencoding = "2.1.3"
|
||||||
uuid = "1.4.0"
|
uuid = "1.4.0"
|
||||||
wasm-bindgen = "0.2.87"
|
wasm-bindgen = "0.2.87"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use api::{
|
||||||
LoginResponse, UserInfo,
|
LoginResponse, UserInfo,
|
||||||
};
|
};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_router::{use_router, Route, Router};
|
use dioxus_router::{use_route, use_router, Redirect, Route, Router};
|
||||||
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
|
use gloo_storage::{errors::StorageError, LocalStorage, Storage};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -33,6 +33,11 @@ const API_ROUTE: &str = match option_env!("REGALADE_API_SERVER_BASE") {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const FRONTEND_ROOT: &str = match option_env!("REGALADE_FRONTEND_DOMAIN") {
|
||||||
|
None => "http://localhost:8080",
|
||||||
|
Some(v) => v,
|
||||||
|
};
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! api {
|
macro_rules! api {
|
||||||
($($arg:tt)*) => {{
|
($($arg:tt)*) => {{
|
||||||
|
|
@ -204,6 +209,37 @@ async fn do_login(username: String, password: String) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn check_oidc() -> anyhow::Result<bool> {
|
||||||
|
let rsp = gloo_net::http::Request::get(api!("login/has_oidc"))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(rsp.status() == 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Openid(cx: Scope) -> Element {
|
||||||
|
let has = use_future(cx, (), |()| check_oidc());
|
||||||
|
|
||||||
|
cx.render(match has.value().unwrap_or(&Ok(false)) {
|
||||||
|
Ok(true) => {
|
||||||
|
let route = api!("login/oidc").to_owned();
|
||||||
|
let ret = urlencoding::encode(&format!("{FRONTEND_ROOT}/login/oidc")).to_string();
|
||||||
|
rsx! {
|
||||||
|
a {
|
||||||
|
href: "{route}?return={ret}",
|
||||||
|
class: "mt-1 w-100 btn btn-lg btn-primary",
|
||||||
|
"Login with OpenID"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => rsx! {{}},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Could not check oidc status: {e:?}");
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn Login(cx: Scope) -> Element {
|
fn Login(cx: Scope) -> Element {
|
||||||
let error = use_state(cx, || None::<String>);
|
let error = use_state(cx, || None::<String>);
|
||||||
let router = use_router(cx);
|
let router = use_router(cx);
|
||||||
|
|
@ -256,6 +292,7 @@ fn Login(cx: Scope) -> Element {
|
||||||
label { "for": "floatingPass", "Password" }
|
label { "for": "floatingPass", "Password" }
|
||||||
}
|
}
|
||||||
button { class: "w-100 btn btn-lg btn-primary", "type": "submit", "Login" }
|
button { class: "w-100 btn btn-lg btn-primary", "type": "submit", "Login" }
|
||||||
|
Openid {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -505,9 +542,38 @@ fn Index(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {"INDEX"})
|
cx.render(rsx! {"INDEX"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OidcQuery {
|
||||||
|
token: String,
|
||||||
|
username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn OidcRedirect(cx: Scope) -> Element {
|
||||||
|
let auth = use_route(cx).query::<OidcQuery>();
|
||||||
|
|
||||||
|
cx.render(match auth {
|
||||||
|
None => rsx! {"No authentication query, internal error."},
|
||||||
|
Some(v) => {
|
||||||
|
match LocalStorage::set(
|
||||||
|
"token",
|
||||||
|
LoginInfo {
|
||||||
|
token: v.token,
|
||||||
|
name: v.username,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Ok(_) => {
|
||||||
|
gloo_utils::window().location().replace("/").unwrap();
|
||||||
|
rsx! {{}}
|
||||||
|
}
|
||||||
|
Err(_) => rsx! {"Could not store authentication, try again."},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn App(cx: Scope) -> Element {
|
fn App(cx: Scope) -> Element {
|
||||||
cx.render(rsx! {
|
cx.render(rsx! {
|
||||||
Router {
|
Router {
|
||||||
Route { to: Page::Home.to(),
|
Route { to: Page::Home.to(),
|
||||||
RegaladeSidebar { current: Page::Home, Index {} }
|
RegaladeSidebar { current: Page::Home, Index {} }
|
||||||
}
|
}
|
||||||
|
|
@ -524,6 +590,7 @@ fn App(cx: Scope) -> Element {
|
||||||
RegaladeSidebar { current: Page::RecipeList, recipe::RecipeView {} }
|
RegaladeSidebar { current: Page::RecipeList, recipe::RecipeView {} }
|
||||||
}
|
}
|
||||||
Route { to: "/login", Login {} }
|
Route { to: "/login", Login {} }
|
||||||
|
Route { to: "/login/oidc", OidcRedirect {} }
|
||||||
Route { to: "/household_selection",
|
Route { to: "/household_selection",
|
||||||
LoginRedirect { HouseholdSelection {} }
|
LoginRedirect { HouseholdSelection {} }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -341,15 +341,15 @@ pub fn RecipeCreator(cx: Scope) -> Element {
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
h2 { "Steps" }
|
h2 { "Steps" }
|
||||||
div {class: "text-start",
|
div { class: "text-start",
|
||||||
textarea {
|
textarea {
|
||||||
class: "form-control",
|
class: "form-control",
|
||||||
id: "steps-area",
|
id: "steps-area",
|
||||||
value: "{steps}",
|
value: "{steps}",
|
||||||
rows: "10",
|
rows: "10",
|
||||||
oninput: move |e| steps.set(e.value.clone())
|
oninput: move |e| steps.set(e.value.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hr {}
|
hr {}
|
||||||
ModalToggleButton { class: "btn btn-lg btn-primary", modal_id: "newRcpModal", "Create Recipe" }
|
ModalToggleButton { class: "btn btn-lg btn-primary", modal_id: "newRcpModal", "Create Recipe" }
|
||||||
|
|
|
||||||
|
|
@ -700,9 +700,7 @@ fn RecipeViewer(cx: Scope<RecipeViewerProps>) -> Element {
|
||||||
hr {}
|
hr {}
|
||||||
div { class: "text-start",
|
div { class: "text-start",
|
||||||
h2 { "Steps" }
|
h2 { "Steps" }
|
||||||
div {
|
div { dangerous_inner_html: "{steps_rendered}" }
|
||||||
dangerous_inner_html: "{steps_rendered}"
|
|
||||||
}
|
|
||||||
EditSteps {
|
EditSteps {
|
||||||
recipe: cx.props.id,
|
recipe: cx.props.id,
|
||||||
refresh: cx.props.refresh.clone(),
|
refresh: cx.props.refresh.clone(),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ mod m20230520_203638_household;
|
||||||
mod m20230529_184433_ingredients;
|
mod m20230529_184433_ingredients;
|
||||||
mod m20230618_163416_recipe;
|
mod m20230618_163416_recipe;
|
||||||
mod m20230629_151746_recipe_person_count;
|
mod m20230629_151746_recipe_person_count;
|
||||||
|
mod m20230726_203858_oidc;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
|
|
@ -17,6 +18,7 @@ impl MigratorTrait for Migrator {
|
||||||
Box::new(m20230529_184433_ingredients::Migration),
|
Box::new(m20230529_184433_ingredients::Migration),
|
||||||
Box::new(m20230618_163416_recipe::Migration),
|
Box::new(m20230618_163416_recipe::Migration),
|
||||||
Box::new(m20230629_151746_recipe_person_count::Migration),
|
Box::new(m20230629_151746_recipe_person_count::Migration),
|
||||||
|
Box::new(m20230726_203858_oidc::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub(crate) enum User {
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
Password,
|
Password,
|
||||||
|
OpenIdSubject,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|
|
||||||
40
migration/src/m20230726_203858_oidc.rs
Normal file
40
migration/src/m20230726_203858_oidc.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use sea_orm_migration::prelude::*;
|
||||||
|
|
||||||
|
use crate::m20220101_000001_account::User;
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(User::Table)
|
||||||
|
.modify_column(ColumnDef::new(User::Password).null())
|
||||||
|
.add_column(ColumnDef::new(User::OpenIdSubject).string().null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager.exec_stmt(
|
||||||
|
Query::delete()
|
||||||
|
.from_table(User::Table)
|
||||||
|
.cond_where(Expr::col(User::Password).is_null())
|
||||||
|
.to_owned(),
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.alter_table(
|
||||||
|
Table::alter()
|
||||||
|
.table(User::Table)
|
||||||
|
.drop_column(User::OpenIdSubject)
|
||||||
|
.modify_column(ColumnDef::new(User::Password).not_null())
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,8 +9,9 @@ pub struct Model {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
#[sea_orm(unique)]
|
#[sea_orm(unique)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[sea_orm(column_type = "Binary(BlobSize::Blob(None))")]
|
#[sea_orm(column_type = "Binary(BlobSize::Blob(None))", nullable)]
|
||||||
pub password: Vec<u8>,
|
pub password: Option<Vec<u8>>,
|
||||||
|
pub open_id_subject: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
|
|
||||||
258
src/main.rs
258
src/main.rs
|
|
@ -1,13 +1,26 @@
|
||||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
net::SocketAddr,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use base64::{engine::general_purpose, Engine};
|
use base64::{engine::general_purpose, Engine};
|
||||||
use jwt_simple::prelude::HS256Key;
|
use jwt_simple::prelude::HS256Key;
|
||||||
use migration::{Migrator, MigratorTrait};
|
use migration::{Migrator, MigratorTrait};
|
||||||
|
use openidconnect::{
|
||||||
|
core::{CoreAuthenticationFlow, CoreClient, CoreProviderMetadata},
|
||||||
|
url::Url,
|
||||||
|
AccessTokenHash, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce,
|
||||||
|
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, TokenResponse,
|
||||||
|
};
|
||||||
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub(crate) mod entity;
|
pub(crate) mod entity;
|
||||||
mod routes;
|
mod routes;
|
||||||
|
|
@ -65,6 +78,44 @@ impl<'de> Deserialize<'de> for Base64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deserialize_comma<'de, D>(de: D) -> Result<Vec<openidconnect::Scope>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de::Visitor;
|
||||||
|
|
||||||
|
struct CommaVisitor;
|
||||||
|
impl<'de> Visitor<'de> for CommaVisitor {
|
||||||
|
type Value = Vec<openidconnect::Scope>;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("string containg comma separated strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: serde::de::Error,
|
||||||
|
{
|
||||||
|
Ok(v.split(',')
|
||||||
|
.map(|v| openidconnect::Scope::new(v.to_string()))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
de.deserialize_str(CommaVisitor)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(rename_all = "UPPERCASE")]
|
||||||
|
struct OpenidConnectSettings {
|
||||||
|
url: String,
|
||||||
|
id: ClientId,
|
||||||
|
secret: ClientSecret,
|
||||||
|
domain: String,
|
||||||
|
#[serde(deserialize_with = "deserialize_comma")]
|
||||||
|
scopes: Vec<openidconnect::Scope>,
|
||||||
|
}
|
||||||
|
|
||||||
fn default_host() -> String {
|
fn default_host() -> String {
|
||||||
"0.0.0.0".into()
|
"0.0.0.0".into()
|
||||||
}
|
}
|
||||||
|
|
@ -87,6 +138,8 @@ struct Settings {
|
||||||
serve_app: Option<PathBuf>,
|
serve_app: Option<PathBuf>,
|
||||||
database_url: String,
|
database_url: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
oidc: Option<OpenidConnectSettings>,
|
||||||
|
#[serde(default)]
|
||||||
sqlx_logging: bool,
|
sqlx_logging: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +155,198 @@ impl Settings {
|
||||||
struct AppState {
|
struct AppState {
|
||||||
jwt_secret: Base64,
|
jwt_secret: Base64,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
|
oidc: Option<OpenidConnector>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OpenidConnector {
|
||||||
|
provider: CoreClient,
|
||||||
|
scopes: Vec<openidconnect::Scope>,
|
||||||
|
domain: Url,
|
||||||
|
inflight: parking_lot::Mutex<FifoMap>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FifoMap {
|
||||||
|
max: usize,
|
||||||
|
order: VecDeque<Uuid>,
|
||||||
|
values: HashMap<Uuid, OpenidAuthState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FifoMapInsert<'a> {
|
||||||
|
pub id: Uuid,
|
||||||
|
map: &'a mut FifoMap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FifoMapInsert<'a> {
|
||||||
|
pub fn insert(self, state: OpenidAuthState) {
|
||||||
|
let FifoMapInsert { id, map } = self;
|
||||||
|
|
||||||
|
map.order.push_back(id);
|
||||||
|
if map.order.len() > map.max {
|
||||||
|
let first = map.order.pop_front().unwrap();
|
||||||
|
map.values.remove(&first);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.values.insert(id, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FifoMap {
|
||||||
|
pub fn new(max: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
max,
|
||||||
|
order: VecDeque::new(),
|
||||||
|
values: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_entry(&mut self) -> FifoMapInsert {
|
||||||
|
let id = loop {
|
||||||
|
let id = Uuid::new_v4();
|
||||||
|
if !self.values.contains_key(&id) {
|
||||||
|
break id;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
FifoMapInsert { id, map: self }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, id: Uuid) -> Option<OpenidAuthState> {
|
||||||
|
let state = self.values.remove(&id);
|
||||||
|
|
||||||
|
if let Some(idx) = self.order.iter().position(|&v| v == id) {
|
||||||
|
self.order.remove(idx);
|
||||||
|
};
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OpenidAuthState {
|
||||||
|
pkce_verifier: PkceCodeVerifier,
|
||||||
|
csrf_token: CsrfToken,
|
||||||
|
nonce: Nonce,
|
||||||
|
source_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OpenidAccount {
|
||||||
|
pub sub: String,
|
||||||
|
pub name: String,
|
||||||
|
pub source_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenidConnector {
|
||||||
|
async fn new(settings: OpenidConnectSettings) -> anyhow::Result<Self> {
|
||||||
|
let metadata = CoreProviderMetadata::discover_async(
|
||||||
|
IssuerUrl::new(settings.url)?,
|
||||||
|
openidconnect::reqwest::async_http_client,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let provider =
|
||||||
|
CoreClient::from_provider_metadata(metadata, settings.id, Some(settings.secret));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
provider,
|
||||||
|
scopes: settings.scopes,
|
||||||
|
domain: settings.domain.parse()?,
|
||||||
|
inflight: parking_lot::Mutex::new(FifoMap::new(1024)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_auth(&self, source_url: String) -> Url {
|
||||||
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
|
||||||
|
let mut inflight = self.inflight.lock();
|
||||||
|
|
||||||
|
let slot = inflight.new_entry();
|
||||||
|
|
||||||
|
tracing::info!("Login flow with id {}", slot.id);
|
||||||
|
|
||||||
|
let (url, csrf_token, nonce) = self
|
||||||
|
.provider
|
||||||
|
.authorize_url(
|
||||||
|
CoreAuthenticationFlow::AuthorizationCode,
|
||||||
|
CsrfToken::new_random,
|
||||||
|
Nonce::new_random,
|
||||||
|
)
|
||||||
|
.add_scopes(self.scopes.iter().cloned())
|
||||||
|
.set_pkce_challenge(pkce_challenge)
|
||||||
|
.set_redirect_uri(std::borrow::Cow::Owned(RedirectUrl::from_url(
|
||||||
|
self.domain
|
||||||
|
.join(&format!("api/login/redirect/{}", slot.id))
|
||||||
|
.unwrap(),
|
||||||
|
)))
|
||||||
|
.url();
|
||||||
|
|
||||||
|
slot.insert(OpenidAuthState {
|
||||||
|
pkce_verifier,
|
||||||
|
csrf_token,
|
||||||
|
nonce,
|
||||||
|
source_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn redirected(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
csrf_state: String,
|
||||||
|
code: String,
|
||||||
|
) -> anyhow::Result<OpenidAccount> {
|
||||||
|
let Some(state) = self.inflight.lock().remove(id) else {
|
||||||
|
anyhow::bail!("Redirect has expired or has already been submitted")
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("Redirected to continue login flow for {id}");
|
||||||
|
|
||||||
|
if state.csrf_token.secret() != &csrf_state {
|
||||||
|
anyhow::bail!(
|
||||||
|
"CRSF state does not match, expected: {} got {csrf_state}",
|
||||||
|
state.csrf_token.secret()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let token_response = self
|
||||||
|
.provider
|
||||||
|
.exchange_code(AuthorizationCode::new(code))
|
||||||
|
.set_pkce_verifier(state.pkce_verifier)
|
||||||
|
.set_redirect_uri(std::borrow::Cow::Owned(RedirectUrl::from_url(
|
||||||
|
self.domain
|
||||||
|
.join(&format!("api/login/redirect/{}", id))
|
||||||
|
.unwrap(),
|
||||||
|
)))
|
||||||
|
.request_async(openidconnect::reqwest::async_http_client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let Some(id_token) = token_response.id_token() else {
|
||||||
|
anyhow::bail!("Server did not return an ID token")
|
||||||
|
};
|
||||||
|
|
||||||
|
let claims = id_token.claims(&self.provider.id_token_verifier(), &state.nonce)?;
|
||||||
|
|
||||||
|
if let Some(expected_access_token_hash) = claims.access_token_hash() {
|
||||||
|
let actual_access_token_hash = AccessTokenHash::from_token(
|
||||||
|
token_response.access_token(),
|
||||||
|
&id_token.signing_alg()?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if actual_access_token_hash != *expected_access_token_hash {
|
||||||
|
anyhow::bail!("Invalid access token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(OpenidAccount {
|
||||||
|
sub: claims.subject().to_string(),
|
||||||
|
name: claims
|
||||||
|
.name()
|
||||||
|
.and_then(|v| v.get(None))
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.or(claims.preferred_username().map(|v| v.to_string()))
|
||||||
|
.ok_or_else(|| anyhow!("No name or preferred_username"))?,
|
||||||
|
source_url: state.source_url,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -121,9 +366,15 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let mut opt = ConnectOptions::new(config.database_url);
|
let mut opt = ConnectOptions::new(config.database_url);
|
||||||
opt.sqlx_logging(config.sqlx_logging);
|
opt.sqlx_logging(config.sqlx_logging);
|
||||||
|
|
||||||
|
let oidc = match config.oidc {
|
||||||
|
None => None,
|
||||||
|
Some(settings) => Some(OpenidConnector::new(settings).await?),
|
||||||
|
};
|
||||||
|
|
||||||
let state = Arc::new(AppState {
|
let state = Arc::new(AppState {
|
||||||
jwt_secret: config.jwt_secret,
|
jwt_secret: config.jwt_secret,
|
||||||
db: Database::connect(opt).await?,
|
db: Database::connect(opt).await?,
|
||||||
|
oidc,
|
||||||
});
|
});
|
||||||
|
|
||||||
Migrator::up(&state.db, None).await?;
|
Migrator::up(&state.db, None).await?;
|
||||||
|
|
@ -131,7 +382,10 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.nest(
|
.nest(
|
||||||
"/api",
|
"/api",
|
||||||
routes::router(config.api_allowed.map(|s| s.parse()).transpose()?),
|
routes::router(
|
||||||
|
config.api_allowed.map(|s| s.parse()).transpose()?,
|
||||||
|
state.oidc.is_some(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,19 @@ use std::sync::Arc;
|
||||||
use api::{LoginRequest, LoginResponse, UserInfo};
|
use api::{LoginRequest, LoginResponse, UserInfo};
|
||||||
use axum::{
|
use axum::{
|
||||||
async_trait,
|
async_trait,
|
||||||
extract::{FromRef, FromRequestParts, Path, State},
|
extract::{FromRef, FromRequestParts, Path, Query, State},
|
||||||
headers::{authorization::Bearer, Authorization},
|
headers::{authorization::Bearer, Authorization},
|
||||||
http::{
|
http::{
|
||||||
header::{AUTHORIZATION, CONTENT_TYPE},
|
header::{AUTHORIZATION, CONTENT_TYPE},
|
||||||
request::Parts,
|
request::Parts,
|
||||||
HeaderValue, Method, StatusCode,
|
HeaderValue, Method, StatusCode,
|
||||||
},
|
},
|
||||||
response::IntoResponse,
|
response::{IntoResponse, Redirect},
|
||||||
routing::{delete, get, patch, post, put},
|
routing::{delete, get, patch, post, put},
|
||||||
Json, Router, TypedHeader,
|
Json, Router, TypedHeader,
|
||||||
};
|
};
|
||||||
use jwt_simple::prelude::*;
|
use jwt_simple::prelude::*;
|
||||||
use sea_orm::{prelude::*, TransactionError};
|
use sea_orm::{prelude::*, ActiveValue, TransactionError};
|
||||||
use sha2::{Digest, Sha512};
|
use sha2::{Digest, Sha512};
|
||||||
use tower_http::cors::{self, AllowOrigin, CorsLayer};
|
use tower_http::cors::{self, AllowOrigin, CorsLayer};
|
||||||
|
|
||||||
|
|
@ -45,6 +45,8 @@ enum RouteError {
|
||||||
RessourceNotFound,
|
RessourceNotFound,
|
||||||
#[error("The request was malformed")]
|
#[error("The request was malformed")]
|
||||||
InvalidRequest(String),
|
InvalidRequest(String),
|
||||||
|
#[error("A normal account with this name already exists")]
|
||||||
|
NormalAccount,
|
||||||
#[error("Error in DB transaction")]
|
#[error("Error in DB transaction")]
|
||||||
TxnError(#[from] TransactionError<Box<RouteError>>),
|
TxnError(#[from] TransactionError<Box<RouteError>>),
|
||||||
}
|
}
|
||||||
|
|
@ -77,6 +79,9 @@ impl IntoResponse for RouteError {
|
||||||
.into_response(),
|
.into_response(),
|
||||||
RouteError::RessourceNotFound => StatusCode::NOT_FOUND.into_response(),
|
RouteError::RessourceNotFound => StatusCode::NOT_FOUND.into_response(),
|
||||||
RouteError::InvalidRequest(reason) => (StatusCode::BAD_REQUEST, reason).into_response(),
|
RouteError::InvalidRequest(reason) => (StatusCode::BAD_REQUEST, reason).into_response(),
|
||||||
|
s @ RouteError::NormalAccount => {
|
||||||
|
(StatusCode::BAD_REQUEST, s.to_string()).into_response()
|
||||||
|
}
|
||||||
e => {
|
e => {
|
||||||
tracing::error!("Internal error: {e:?}");
|
tracing::error!("Internal error: {e:?}");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
StatusCode::INTERNAL_SERVER_ERROR.into_response()
|
||||||
|
|
@ -138,12 +143,16 @@ async fn login(
|
||||||
return Err(RouteError::UnknownAccount)
|
return Err(RouteError::UnknownAccount)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(password) = user.password.as_ref() else {
|
||||||
|
return Err(RouteError::UnknownAccount);
|
||||||
|
};
|
||||||
|
|
||||||
let mut hasher = Sha512::new();
|
let mut hasher = Sha512::new();
|
||||||
hasher.update(user.id.as_bytes());
|
hasher.update(user.id.as_bytes());
|
||||||
hasher.update(req.password.as_bytes());
|
hasher.update(req.password.as_bytes());
|
||||||
let hash = hasher.finalize();
|
let hash = hasher.finalize();
|
||||||
|
|
||||||
if hash[..] != user.password {
|
if &hash[..] != password {
|
||||||
return Err(RouteError::UnknownAccount);
|
return Err(RouteError::UnknownAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +164,92 @@ async fn login(
|
||||||
Ok(Json(LoginResponse { token }))
|
Ok(Json(LoginResponse { token }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OidcStartParam {
|
||||||
|
r#return: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(start): Query<OidcStartParam>,
|
||||||
|
) -> Result<Redirect, RouteError> {
|
||||||
|
tracing::info!("Starting OIDC login");
|
||||||
|
let oidc = state.oidc.as_ref().unwrap();
|
||||||
|
|
||||||
|
let redirect_url = oidc.start_auth(start.r#return);
|
||||||
|
|
||||||
|
Ok(Redirect::to(redirect_url.as_str()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OidcRedirectParams {
|
||||||
|
state: String,
|
||||||
|
code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn oidc_login_finish(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
Query(redirect): Query<OidcRedirectParams>,
|
||||||
|
) -> Result<Redirect, RouteError> {
|
||||||
|
match state
|
||||||
|
.oidc
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.redirected(id, redirect.state, redirect.code)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error when finishing OAuth2 flow {e:?}");
|
||||||
|
Err(RouteError::Unauthorized)
|
||||||
|
}
|
||||||
|
Ok(account) => {
|
||||||
|
let user = User::find()
|
||||||
|
.filter(
|
||||||
|
user::Column::Name
|
||||||
|
.eq(&account.name)
|
||||||
|
.or(user::Column::OpenIdSubject.eq(&account.sub)),
|
||||||
|
)
|
||||||
|
.one(&state.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let user = match user {
|
||||||
|
None => {
|
||||||
|
let model = user::ActiveModel {
|
||||||
|
id: ActiveValue::Set(Uuid::new_v4()),
|
||||||
|
name: ActiveValue::Set(account.name),
|
||||||
|
password: ActiveValue::NotSet,
|
||||||
|
open_id_subject: ActiveValue::Set(Some(account.sub)),
|
||||||
|
};
|
||||||
|
|
||||||
|
model.insert(&state.db).await?
|
||||||
|
}
|
||||||
|
Some(user) => {
|
||||||
|
if user.open_id_subject.as_ref() != Some(&account.sub) {
|
||||||
|
return Err(RouteError::NormalAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
user
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut claims = Claims::create(Duration::from_secs(3600 * 24 * 31 * 6));
|
||||||
|
claims.subject = Some(user.id.to_string());
|
||||||
|
|
||||||
|
let token = state.jwt_secret.0.authenticate(claims)?;
|
||||||
|
|
||||||
|
let redirect = format!(
|
||||||
|
"{}?token={}&username={}",
|
||||||
|
account.source_url,
|
||||||
|
urlencoding::encode(&token),
|
||||||
|
urlencoding::encode(&user.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Redirect::to(&redirect))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_user_id(
|
async fn get_user_id(
|
||||||
_: AuthenticatedUser,
|
_: AuthenticatedUser,
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
|
|
@ -173,7 +268,7 @@ async fn get_user_id(
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
pub(crate) fn router(api_allowed: Option<HeaderValue>, has_oidc: bool) -> Router<AppState> {
|
||||||
let origin: AllowOrigin = match api_allowed {
|
let origin: AllowOrigin = match api_allowed {
|
||||||
Some(n) => n.into(),
|
Some(n) => n.into(),
|
||||||
None => cors::Any.into(),
|
None => cors::Any.into(),
|
||||||
|
|
@ -185,7 +280,7 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
||||||
|
|
||||||
let mk_service = |m: Vec<Method>| cors_base.clone().allow_methods(m);
|
let mk_service = |m: Vec<Method>| cors_base.clone().allow_methods(m);
|
||||||
|
|
||||||
Router::new()
|
let router = Router::new()
|
||||||
.route(
|
.route(
|
||||||
"/search/user/:name",
|
"/search/user/:name",
|
||||||
get(get_user_id).layer(mk_service(vec![Method::GET])),
|
get(get_user_id).layer(mk_service(vec![Method::GET])),
|
||||||
|
|
@ -246,5 +341,18 @@ pub(crate) fn router(api_allowed: Option<HeaderValue>) -> Router<AppState> {
|
||||||
.delete(recipe::delete_ig)
|
.delete(recipe::delete_ig)
|
||||||
.put(recipe::add_ig_request)
|
.put(recipe::add_ig_request)
|
||||||
.layer(mk_service(vec![Method::PATCH, Method::DELETE, Method::PUT])),
|
.layer(mk_service(vec![Method::PATCH, Method::DELETE, Method::PUT])),
|
||||||
)
|
);
|
||||||
|
|
||||||
|
if has_oidc {
|
||||||
|
async fn unit() {}
|
||||||
|
router
|
||||||
|
.route("/login/oidc", get(oidc_login))
|
||||||
|
.route(
|
||||||
|
"/login/has_oidc",
|
||||||
|
get(unit).layer(mk_service(vec![Method::GET])),
|
||||||
|
)
|
||||||
|
.route("/login/redirect/:id", get(oidc_login_finish))
|
||||||
|
} else {
|
||||||
|
router
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue