oidc authentication

This commit is contained in:
2025-08-09 00:31:35 +02:00
parent 2c9b44d215
commit 5f4201428a
34 changed files with 1766 additions and 84 deletions

519
backend/Cargo.lock generated
View File

@@ -186,6 +186,22 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
[[package]]
name = "biscuit"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e28fc7c56c61743a01d0d1b73e4fed68b8a4f032ea3a2d4bb8c6520a33fc05a"
dependencies = [
"chrono",
"data-encoding",
"num-bigint",
"num-traits",
"once_cell",
"ring",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.9.1" version = "2.9.1"
@@ -256,6 +272,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link",
] ]
@@ -275,6 +292,16 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.7" version = "0.8.7"
@@ -330,6 +357,47 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.10" version = "0.7.10"
@@ -491,6 +559,21 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@@ -602,7 +685,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
] ]
[[package]] [[package]]
@@ -739,6 +834,23 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
"tokio", "tokio",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
] ]
[[package]] [[package]]
@@ -747,14 +859,22 @@ version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
dependencies = [ dependencies = [
"base64",
"bytes", "bytes",
"futures-channel",
"futures-core", "futures-core",
"futures-util",
"http", "http",
"http-body", "http-body",
"hyper", "hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]
@@ -867,6 +987,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.0.3" version = "1.0.3"
@@ -909,6 +1035,22 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -1038,7 +1180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
@@ -1059,6 +1201,23 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@@ -1069,6 +1228,16 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.4"
@@ -1137,6 +1306,69 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "openid"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25913f03f3d3bcc2e55021067193b31f79a27283c186c91ece5f93b69b035b44"
dependencies = [
"base64",
"biscuit",
"chrono",
"lazy_static",
"mime",
"reqwest",
"serde",
"serde_json",
"thiserror 1.0.69",
"url",
"validator",
]
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@@ -1261,6 +1493,28 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.95" version = "1.0.95"
@@ -1279,6 +1533,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@@ -1306,7 +1566,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.16",
] ]
[[package]] [[package]]
@@ -1347,6 +1607,56 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [
"base64",
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-util",
"js-sys",
"log",
"native-tls",
"percent-encoding",
"pin-project-lite",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tokio-native-tls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.16",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rsa" name = "rsa"
version = "0.9.8" version = "0.9.8"
@@ -1386,6 +1696,15 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
dependencies = [
"zeroize",
]
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.21"
@@ -1398,12 +1717,44 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.219" version = "1.0.219"
@@ -1430,6 +1781,7 @@ version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"indexmap",
"itoa", "itoa",
"memchr", "memchr",
"ryu", "ryu",
@@ -1599,7 +1951,7 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"smallvec", "smallvec",
"thiserror", "thiserror 2.0.12",
"time", "time",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
@@ -1684,7 +2036,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 2.0.12",
"time", "time",
"tracing", "tracing",
"uuid", "uuid",
@@ -1724,7 +2076,7 @@ dependencies = [
"smallvec", "smallvec",
"sqlx-core", "sqlx-core",
"stringprep", "stringprep",
"thiserror", "thiserror 2.0.12",
"time", "time",
"tracing", "tracing",
"uuid", "uuid",
@@ -1751,7 +2103,7 @@ dependencies = [
"serde", "serde",
"serde_urlencoded", "serde_urlencoded",
"sqlx-core", "sqlx-core",
"thiserror", "thiserror 2.0.12",
"time", "time",
"tracing", "tracing",
"url", "url",
@@ -1775,6 +2127,12 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -1797,6 +2155,9 @@ name = "sync_wrapper"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "synstructure" name = "synstructure"
@@ -1809,13 +2170,46 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tempfile"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.12" version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -1925,6 +2319,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.17" version = "0.1.17"
@@ -1980,12 +2384,14 @@ dependencies = [
"http-body-util", "http-body-util",
"http-range-header", "http-range-header",
"httpdate", "httpdate",
"iri-string",
"mime", "mime",
"mime_guess", "mime_guess",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2061,6 +2467,12 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "try-lock"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.18.0" version = "1.18.0"
@@ -2106,6 +2518,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.4" version = "2.5.4"
@@ -2115,6 +2533,7 @@ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde",
] ]
[[package]] [[package]]
@@ -2134,6 +2553,36 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "validator"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
dependencies = [
"idna",
"once_cell",
"regex",
"serde",
"serde_derive",
"serde_json",
"url",
"validator_derive",
]
[[package]]
name = "validator_derive"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac855a2ce6f843beb229757e6e570a42e837bcb15e5f449dd48d5747d41bf77"
dependencies = [
"darling",
"once_cell",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.1" version = "0.1.1"
@@ -2152,6 +2601,15 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]] [[package]]
name = "warren" name = "warren"
version = "0.1.0" version = "0.1.0"
@@ -2168,18 +2626,20 @@ dependencies = [
"futures-util", "futures-util",
"hex", "hex",
"mime_guess", "mime_guess",
"openid",
"regex", "regex",
"rustix", "rustix",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
"thiserror", "thiserror 2.0.12",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"url",
"uuid", "uuid",
] ]
@@ -2189,6 +2649,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]] [[package]]
name = "wasite" name = "wasite"
version = "0.1.0" version = "0.1.0"
@@ -2221,6 +2690,19 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.100" version = "0.2.100"
@@ -2253,6 +2735,16 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "whoami" name = "whoami"
version = "1.6.0" version = "1.6.0"
@@ -2492,6 +2984,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.1" version = "0.6.1"

View File

@@ -24,6 +24,7 @@ dotenv = "0.15.0"
futures-util = "0.3.31" futures-util = "0.3.31"
hex = "0.4.3" hex = "0.4.3"
mime_guess = "2.0.5" mime_guess = "2.0.5"
openid = "0.17.0"
regex = "1.11.1" regex = "1.11.1"
rustix = { version = "1.0.8", features = ["fs"] } rustix = { version = "1.0.8", features = ["fs"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
@@ -36,4 +37,5 @@ tower = "0.5.2"
tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] } tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.19" tracing-subscriber = "0.3.19"
url = "2.5.4"
uuid = { version = "1.17.0", features = ["serde"] } uuid = { version = "1.17.0", features = ["serde"] }

View File

@@ -0,0 +1,2 @@
ALTER TABLE users ALTER COLUMN hash DROP NOT NULL;
ALTER TABLE users ADD COLUMN oidc_sub VARCHAR UNIQUE;

View File

@@ -6,6 +6,7 @@ use warren::{
file_system::{FileSystem, FileSystemConfig}, file_system::{FileSystem, FileSystemConfig},
metrics_debug_logger::MetricsDebugLogger, metrics_debug_logger::MetricsDebugLogger,
notifier_debug_logger::NotifierDebugLogger, notifier_debug_logger::NotifierDebugLogger,
oidc::{Oidc, OidcConfig},
postgres::{Postgres, PostgresConfig}, postgres::{Postgres, PostgresConfig},
}, },
}; };
@@ -39,8 +40,20 @@ async fn main() -> anyhow::Result<()> {
fs_service.clone(), fs_service.clone(),
); );
let auth_service = let oidc_service = if let Ok(oidc_config) = OidcConfig::from_env() {
domain::warren::service::auth::Service::new(postgres, metrics, notifier, config.auth); let repo = Oidc::new(&oidc_config).await?;
Some(domain::oidc::service::Service::new(repo, metrics, notifier))
} else {
None
};
let auth_service = domain::warren::service::auth::Service::new(
postgres,
metrics,
notifier,
config.auth,
oidc_service,
);
let server_config = HttpServerConfig::new( let server_config = HttpServerConfig::new(
&config.server_address, &config.server_address,

View File

@@ -1 +1,2 @@
pub mod oidc;
pub mod warren; pub mod warren;

View File

@@ -0,0 +1,4 @@
pub mod models;
pub mod ports;
pub mod requests;
pub mod service;

View File

@@ -0,0 +1,69 @@
use crate::domain::warren::models::user::{UserEmail, UserName};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct UserInfo {
sub: String,
name: UserName,
email: UserEmail,
preferred_username: Option<UserName>,
picture: Option<String>,
locale: Option<String>,
updated_at: Option<i64>,
warren_admin: Option<bool>,
}
impl UserInfo {
pub fn new(
sub: String,
name: UserName,
email: UserEmail,
preferred_username: Option<UserName>,
picture: Option<String>,
locale: Option<String>,
updated_at: Option<i64>,
warren_admin: Option<bool>,
) -> Self {
Self {
sub,
name,
email,
preferred_username,
picture,
locale,
updated_at,
warren_admin,
}
}
pub fn sub(&self) -> &String {
&self.sub
}
pub fn name(&self) -> &UserName {
&self.name
}
pub fn email(&self) -> &UserEmail {
&self.email
}
pub fn preferred_username(&self) -> Option<&UserName> {
self.preferred_username.as_ref()
}
pub fn picture(&self) -> Option<&String> {
self.picture.as_ref()
}
pub fn locale(&self) -> Option<&String> {
self.locale.as_ref()
}
pub fn updated_at(&self) -> Option<i64> {
self.updated_at
}
pub fn warren_admin(&self) -> Option<bool> {
self.warren_admin
}
}

View File

@@ -0,0 +1,39 @@
use super::requests::{
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
GetUserInfoRequest, GetUserInfoResponse,
};
pub trait OidcService: Clone + Send + Sync + 'static {
fn get_redirect(
&self,
request: GetRedirectRequest,
) -> impl Future<Output = Result<GetRedirectResponse, GetRedirectError>> + Send;
fn get_user_info(
&self,
request: GetUserInfoRequest,
) -> impl Future<Output = Result<GetUserInfoResponse, GetUserInfoError>> + Send;
}
pub trait OidcRepository: Clone + Send + Sync + 'static {
fn get_redirect(
&self,
request: GetRedirectRequest,
) -> impl Future<Output = Result<GetRedirectResponse, GetRedirectError>> + Send;
fn get_user_info(
&self,
request: GetUserInfoRequest,
) -> impl Future<Output = Result<GetUserInfoResponse, GetUserInfoError>> + Send;
}
pub trait OidcMetrics: Clone + Send + Sync + 'static {
fn record_get_redirect_success(&self) -> impl Future<Output = ()> + Send;
fn record_get_redirect_failure(&self) -> impl Future<Output = ()> + Send;
fn record_get_user_info_success(&self) -> impl Future<Output = ()> + Send;
fn record_get_user_info_failure(&self) -> impl Future<Output = ()> + Send;
}
pub trait OidcNotifier: Clone + Send + Sync + 'static {
fn get_redirect(&self, response: &GetRedirectResponse) -> impl Future<Output = ()> + Send;
fn get_user_info(&self, response: &GetUserInfoResponse) -> impl Future<Output = ()> + Send;
}

View File

@@ -0,0 +1,74 @@
use thiserror::Error;
use super::models::UserInfo;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetRedirectRequest {}
impl GetRedirectRequest {
pub fn new() -> Self {
Self {}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetRedirectResponse {
url: String,
}
impl GetRedirectResponse {
pub fn new(url: String) -> Self {
Self { url }
}
pub fn url(&self) -> &String {
&self.url
}
}
#[derive(Debug, Error)]
pub enum GetRedirectError {
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetUserInfoRequest {
code: String,
state: Option<String>,
}
impl GetUserInfoRequest {
pub fn new(code: String, state: Option<String>) -> Self {
Self { code, state }
}
pub fn code(&self) -> &String {
&self.code
}
pub fn state(&self) -> Option<&String> {
self.state.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetUserInfoResponse {
info: UserInfo,
}
impl GetUserInfoResponse {
pub fn new(info: UserInfo) -> Self {
Self { info }
}
pub fn info(&self) -> &UserInfo {
&self.info
}
}
#[derive(Debug, Error)]
pub enum GetUserInfoError {
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,73 @@
use super::{
ports::{OidcMetrics, OidcNotifier, OidcRepository, OidcService},
requests::{
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
GetUserInfoRequest, GetUserInfoResponse,
},
};
#[derive(Debug, Clone)]
pub struct Service<R, M, N>
where
R: OidcRepository,
M: OidcMetrics,
N: OidcNotifier,
{
repository: R,
metrics: M,
notifier: N,
}
impl<R, M, N> Service<R, M, N>
where
R: OidcRepository,
M: OidcMetrics,
N: OidcNotifier,
{
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
Self {
repository,
metrics,
notifier,
}
}
}
impl<R, M, N> OidcService for Service<R, M, N>
where
R: OidcRepository,
M: OidcMetrics,
N: OidcNotifier,
{
async fn get_redirect(
&self,
request: GetRedirectRequest,
) -> Result<GetRedirectResponse, GetRedirectError> {
let result = self.repository.get_redirect(request).await;
if let Ok(response) = result.as_ref() {
self.metrics.record_get_redirect_success().await;
self.notifier.get_redirect(response).await;
} else {
self.metrics.record_get_redirect_failure().await;
}
result
}
async fn get_user_info(
&self,
request: GetUserInfoRequest,
) -> Result<GetUserInfoResponse, GetUserInfoError> {
let result = self.repository.get_user_info(request).await;
if let Ok(response) = result.as_ref() {
self.metrics.record_get_user_info_success().await;
self.notifier.get_user_info(response).await;
} else {
self.metrics.record_get_user_info_failure().await;
}
result
}
}

View File

@@ -11,16 +11,21 @@ use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, sqlx::FromRow)]
pub struct User { pub struct User {
oidc_sub: Option<String>,
id: Uuid, id: Uuid,
name: UserName, name: UserName,
email: UserEmail, email: UserEmail,
hash: String, hash: Option<String>,
admin: bool, admin: bool,
updated_at: NaiveDateTime, updated_at: NaiveDateTime,
created_at: NaiveDateTime, created_at: NaiveDateTime,
} }
impl User { impl User {
pub fn oidc_sub(&self) -> Option<&String> {
self.oidc_sub.as_ref()
}
pub fn id(&self) -> &Uuid { pub fn id(&self) -> &Uuid {
&self.id &self.id
} }
@@ -33,8 +38,8 @@ impl User {
&self.email &self.email
} }
pub fn password_hash(&self) -> &str { pub fn password_hash(&self) -> Option<&String> {
&self.hash self.hash.as_ref()
} }
pub fn admin(&self) -> bool { pub fn admin(&self) -> bool {
@@ -74,6 +79,7 @@ impl UserName {
} }
/// A valid email /// A valid email
// TODO: Maybe move this somewhere else (emails are used here and for OIDC)
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display, sqlx::Type)]
#[sqlx(transparent)] #[sqlx(transparent)]
pub struct UserEmail(String); pub struct UserEmail(String);

View File

@@ -0,0 +1,62 @@
use thiserror::Error;
use crate::domain::{
oidc::models::UserInfo,
warren::models::user::{UserEmail, UserName},
};
/// An admin request to create a new OIDC user or update an existing one if the `sub` already exists
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CreateOrUpdateUserOidcRequest {
sub: String,
name: UserName,
email: UserEmail,
admin: bool,
}
impl CreateOrUpdateUserOidcRequest {
pub fn new(sub: String, name: UserName, email: UserEmail, admin: bool) -> Self {
Self {
sub,
name,
email,
admin,
}
}
pub fn sub(&self) -> &String {
&self.sub
}
pub fn name(&self) -> &UserName {
&self.name
}
pub fn email(&self) -> &UserEmail {
&self.email
}
pub fn admin(&self) -> bool {
self.admin
}
}
impl From<&UserInfo> for CreateOrUpdateUserOidcRequest {
fn from(value: &UserInfo) -> Self {
let name = value.preferred_username().unwrap_or(value.name()).clone();
Self::new(
value.sub().clone(),
name,
value.email().clone(),
value.warren_admin().unwrap_or(false),
)
}
}
#[derive(Debug, Error)]
pub enum CreateOrUpdateUserOidcError {
#[error("This email is already taken")]
EmailTaken,
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,43 @@
use thiserror::Error;
use crate::domain::oidc::requests::GetRedirectError;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetOidcRedirectRequest {}
impl GetOidcRedirectRequest {
pub fn new() -> Self {
Self {}
}
}
impl From<GetOidcRedirectRequest> for crate::domain::oidc::requests::GetRedirectRequest {
fn from(_value: GetOidcRedirectRequest) -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetOidcRedirectResponse {
url: String,
}
impl GetOidcRedirectResponse {
pub fn new(url: String) -> Self {
Self { url }
}
pub fn url(&self) -> &String {
&self.url
}
}
#[derive(Debug, Error)]
pub enum GetOidcRedirectError {
#[error("OIDC is not enabled")]
Disabled,
#[error(transparent)]
GetRedirect(#[from] GetRedirectError),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,71 @@
use thiserror::Error;
use crate::domain::{
oidc::requests::{GetUserInfoError, GetUserInfoRequest},
warren::models::{
auth_session::{AuthSession, requests::CreateAuthSessionError},
user::User,
},
};
use super::CreateOrUpdateUserOidcError;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LoginUserOidcRequest {
pub(super) code: String,
pub(super) state: Option<String>,
}
impl LoginUserOidcRequest {
pub fn new(code: String, state: Option<String>) -> Self {
Self { code, state }
}
pub fn code(&self) -> &String {
&self.code
}
pub fn state(&self) -> Option<&String> {
self.state.as_ref()
}
}
impl From<LoginUserOidcRequest> for GetUserInfoRequest {
fn from(value: LoginUserOidcRequest) -> Self {
GetUserInfoRequest::new(value.code, value.state)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LoginUserOidcResponse {
session: AuthSession,
user: User,
}
impl LoginUserOidcResponse {
pub fn new(session: AuthSession, user: User) -> Self {
Self { session, user }
}
pub fn session(&self) -> &AuthSession {
&self.session
}
pub fn user(&self) -> &User {
&self.user
}
}
#[derive(Debug, Error)]
pub enum LoginUserOidcError {
#[error("OIDC is not enabled")]
Disabled,
#[error(transparent)]
GetUserInfo(#[from] GetUserInfoError),
#[error(transparent)]
CreateOrUpdateUser(#[from] CreateOrUpdateUserOidcError),
#[error(transparent)]
CreateAuthToken(#[from] CreateAuthSessionError),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -1,17 +1,23 @@
mod create; mod create;
mod create_or_update;
mod delete; mod delete;
mod edit; mod edit;
mod get_oidc_redirect;
mod list; mod list;
mod list_all; mod list_all;
mod login; mod login;
mod login_oidc;
mod register; mod register;
mod verify_password; mod verify_password;
pub use create::*; pub use create::*;
pub use create_or_update::*;
pub use delete::*; pub use delete::*;
pub use edit::*; pub use edit::*;
pub use get_oidc_redirect::*;
pub use list::*; pub use list::*;
pub use list_all::*; pub use list_all::*;
pub use login::*; pub use login::*;
pub use login_oidc::*;
pub use register::*; pub use register::*;
pub use verify_password::*; pub use verify_password::*;

View File

@@ -35,6 +35,8 @@ impl From<LoginUserRequest> for VerifyUserPasswordRequest {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum VerifyUserPasswordError { pub enum VerifyUserPasswordError {
#[error("This user does not use password authentication")]
PasswordNotAllowed,
#[error("There is no user with this email: {0}")] #[error("There is no user with this email: {0}")]
NotFound(UserEmail), NotFound(UserEmail),
#[error("The password is incorrect")] #[error("The password is incorrect")]

View File

@@ -86,6 +86,9 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
fn record_user_login_success(&self) -> impl Future<Output = ()> + Send; fn record_user_login_success(&self) -> impl Future<Output = ()> + Send;
fn record_user_login_failure(&self) -> impl Future<Output = ()> + Send; fn record_user_login_failure(&self) -> impl Future<Output = ()> + Send;
fn record_user_login_oidc_success(&self) -> impl Future<Output = ()> + Send;
fn record_user_login_oidc_failure(&self) -> impl Future<Output = ()> + Send;
fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send; fn record_user_creation_success(&self) -> impl Future<Output = ()> + Send;
fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send; fn record_user_creation_failure(&self) -> impl Future<Output = ()> + Send;

View File

@@ -21,9 +21,11 @@ use super::models::{
}, },
user::{ user::{
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest, GetOidcRedirectResponse,
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, LoginUserError, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse,
LoginUserRequest, LoginUserResponse, RegisterUserError, RegisterUserRequest, User, ListUsersError, ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
RegisterUserRequest, User,
}, },
user_warren::{ user_warren::{
UserWarren, UserWarren,
@@ -141,6 +143,11 @@ pub trait AuthService: Clone + Send + Sync + 'static {
warren_service: &WS, warren_service: &WS,
) -> impl Future<Output = Result<Warren, AuthError<DeleteWarrenError>>> + Send; ) -> impl Future<Output = Result<Warren, AuthError<DeleteWarrenError>>> + Send;
fn get_oidc_redirect(
&self,
request: GetOidcRedirectRequest,
) -> impl Future<Output = Result<GetOidcRedirectResponse, GetOidcRedirectError>> + Send;
fn register_user( fn register_user(
&self, &self,
request: RegisterUserRequest, request: RegisterUserRequest,
@@ -149,6 +156,10 @@ pub trait AuthService: Clone + Send + Sync + 'static {
&self, &self,
request: LoginUserRequest, request: LoginUserRequest,
) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send; ) -> impl Future<Output = Result<LoginUserResponse, LoginUserError>> + Send;
fn login_user_oidc(
&self,
request: LoginUserOidcRequest,
) -> impl Future<Output = Result<LoginUserOidcResponse, LoginUserOidcError>> + Send;
/// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES) /// An action that creates a user (MUST REQUIRE ADMIN PRIVILEGES)
fn create_user( fn create_user(

View File

@@ -3,7 +3,7 @@ use uuid::Uuid;
use crate::domain::warren::models::{ use crate::domain::warren::models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, LsResponse}, file::{AbsoluteFilePath, LsResponse},
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
user_warren::UserWarren, user_warren::UserWarren,
warren::{ warren::{
Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse,
@@ -81,6 +81,10 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send; fn user_registered(&self, user: &User) -> impl Future<Output = ()> + Send;
fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send; fn user_logged_in(&self, response: &LoginUserResponse) -> impl Future<Output = ()> + Send;
fn user_logged_in_oidc(
&self,
response: &LoginUserOidcResponse,
) -> impl Future<Output = ()> + Send;
fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send; fn user_created(&self, creator: &User, created: &User) -> impl Future<Output = ()> + Send;
fn user_edited(&self, editor: &User, edited: &User) -> impl Future<Output = ()> + Send; fn user_edited(&self, editor: &User, edited: &User) -> impl Future<Output = ()> + Send;
fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send; fn user_deleted(&self, deleter: &User, user: &User) -> impl Future<Output = ()> + Send;

View File

@@ -12,10 +12,10 @@ use crate::domain::warren::models::{
SaveRequest, SaveResponse, TouchError, TouchRequest, SaveRequest, SaveResponse, TouchError, TouchRequest,
}, },
user::{ user::{
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, EditUserRequest,
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse,
VerifyUserPasswordError, VerifyUserPasswordRequest, ListUsersError, ListUsersRequest, User, VerifyUserPasswordError, VerifyUserPasswordRequest,
}, },
user_warren::{ user_warren::{
UserWarren, UserWarren,
@@ -81,6 +81,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
} }
pub trait AuthRepository: Clone + Send + Sync + 'static { pub trait AuthRepository: Clone + Send + Sync + 'static {
fn create_or_update_user_oidc(
&self,
request: CreateOrUpdateUserOidcRequest,
) -> impl Future<Output = Result<User, CreateOrUpdateUserOidcError>> + Send;
fn create_user( fn create_user(
&self, &self,
request: CreateUserRequest, request: CreateUserRequest,

View File

@@ -1,6 +1,8 @@
use crate::{ use crate::{
config::Config, config::Config,
domain::warren::{ domain::{
oidc::ports::OidcService,
warren::{
models::{ models::{
auth_session::{ auth_session::{
AuthError, AuthRequest, AuthSession, AuthError, AuthRequest, AuthSession,
@@ -12,10 +14,12 @@ use crate::{
file::FileStream, file::FileStream,
user::{ user::{
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
EditUserError, EditUserRequest, ListAllUsersAndWarrensError, EditUserError, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest,
GetOidcRedirectResponse, ListAllUsersAndWarrensError,
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError, ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
ListUsersRequest, LoginUserError, LoginUserRequest, LoginUserResponse, ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
RegisterUserError, RegisterUserRequest, User, LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
RegisterUserRequest, User,
}, },
user_warren::{ user_warren::{
UserWarren, UserWarren,
@@ -31,13 +35,14 @@ use crate::{
FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError, FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError,
WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest, WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest,
WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse, WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse,
WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError, WarrenRmRequest, WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError,
WarrenRmResponse, WarrenSaveError, WarrenSaveRequest, WarrenSaveResponse, WarrenRmRequest, WarrenRmResponse, WarrenSaveError, WarrenSaveRequest,
WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse, WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse,
}, },
}, },
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService}, ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
}, },
},
}; };
const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION"; const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
@@ -45,7 +50,7 @@ const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
/// The authentication service configuration /// The authentication service configuration
/// ///
/// * `session_lifetime`: The amount of milliseconds a client session is valid /// * `session_lifetime`: The amount of milliseconds a client session is valid
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthConfig { pub struct AuthConfig {
session_lifetime: SessionExpirationTime, session_lifetime: SessionExpirationTime,
} }
@@ -69,39 +74,50 @@ impl AuthConfig {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Service<R, M, N> pub struct Service<R, M, N, OIDC>
where where
R: AuthRepository, R: AuthRepository,
M: AuthMetrics, M: AuthMetrics,
N: AuthNotifier, N: AuthNotifier,
OIDC: OidcService,
{ {
repository: R, repository: R,
metrics: M, metrics: M,
notifier: N, notifier: N,
oidc: Option<OIDC>,
config: AuthConfig, config: AuthConfig,
} }
impl<R, M, N> Service<R, M, N> impl<R, M, N, OIDC> Service<R, M, N, OIDC>
where where
R: AuthRepository, R: AuthRepository,
M: AuthMetrics, M: AuthMetrics,
N: AuthNotifier, N: AuthNotifier,
OIDC: OidcService,
{ {
pub fn new(repository: R, metrics: M, notifier: N, config: AuthConfig) -> Self { pub fn new(
repository: R,
metrics: M,
notifier: N,
config: AuthConfig,
oidc: Option<OIDC>,
) -> Self {
Self { Self {
repository, repository,
metrics, metrics,
notifier, notifier,
config, config,
oidc,
} }
} }
} }
impl<R, M, N> AuthService for Service<R, M, N> impl<R, M, N, OIDC> AuthService for Service<R, M, N, OIDC>
where where
R: AuthRepository, R: AuthRepository,
M: AuthMetrics, M: AuthMetrics,
N: AuthNotifier, N: AuthNotifier,
OIDC: OidcService,
{ {
async fn create_warren<WS: WarrenService>( async fn create_warren<WS: WarrenService>(
&self, &self,
@@ -197,6 +213,18 @@ where
result result
} }
async fn get_oidc_redirect(
&self,
request: GetOidcRedirectRequest,
) -> Result<GetOidcRedirectResponse, GetOidcRedirectError> {
let oidc = self.oidc.as_ref().ok_or(GetOidcRedirectError::Disabled)?;
oidc.get_redirect(request.into())
.await
.map(|response| GetOidcRedirectResponse::new(response.url().clone()))
.map_err(Into::into)
}
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> { async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
let result = self.repository.create_user(request.into()).await; let result = self.repository.create_user(request.into()).await;
@@ -238,6 +266,37 @@ where
result.map_err(Into::into) result.map_err(Into::into)
} }
async fn login_user_oidc(
&self,
request: LoginUserOidcRequest,
) -> Result<LoginUserOidcResponse, LoginUserOidcError> {
let oidc = self.oidc.as_ref().ok_or(LoginUserOidcError::Disabled)?;
let user_info = oidc.get_user_info(request.into()).await?;
let user = self
.repository
.create_or_update_user_oidc(user_info.info().into())
.await?;
let result = self
.create_auth_session(CreateAuthSessionRequest::new(
user.clone(),
self.config.session_lifetime(),
))
.await
.map(|session| LoginUserOidcResponse::new(session, user));
if let Ok(response) = result.as_ref() {
self.metrics.record_user_login_oidc_success().await;
self.notifier.user_logged_in_oidc(response).await;
} else {
self.metrics.record_user_login_oidc_failure().await;
}
result.map_err(Into::into)
}
async fn create_user( async fn create_user(
&self, &self,
request: AuthRequest<CreateUserRequest>, request: AuthRequest<CreateUserRequest>,

View File

@@ -2,7 +2,10 @@ use crate::{
domain::warren::models::{ domain::warren::models::{
auth_session::{AuthError, requests::FetchAuthSessionError}, auth_session::{AuthError, requests::FetchAuthSessionError},
file::{LsError, MkdirError, RmError}, file::{LsError, MkdirError, RmError},
user::{CreateUserError, LoginUserError, RegisterUserError, VerifyUserPasswordError}, user::{
CreateUserError, GetOidcRedirectError, LoginUserError, LoginUserOidcError,
RegisterUserError, VerifyUserPasswordError,
},
user_warren::requests::FetchUserWarrenError, user_warren::requests::FetchUserWarrenError,
warren::{ warren::{
FetchWarrenError, FetchWarrensError, WarrenLsError, WarrenMkdirError, WarrenMvError, FetchWarrenError, FetchWarrensError, WarrenLsError, WarrenMkdirError, WarrenMvError,
@@ -117,6 +120,9 @@ impl From<LoginUserError> for ApiError {
fn from(value: LoginUserError) -> Self { fn from(value: LoginUserError) -> Self {
match value { match value {
LoginUserError::VerifyUser(e) => match e { LoginUserError::VerifyUser(e) => match e {
VerifyUserPasswordError::PasswordNotAllowed => {
Self::NotFound("This user does not use password authentication".to_string())
}
VerifyUserPasswordError::NotFound(_) => { VerifyUserPasswordError::NotFound(_) => {
Self::NotFound("Could not find a user with that email".to_string()) Self::NotFound("Could not find a user with that email".to_string())
} }
@@ -131,6 +137,18 @@ impl From<LoginUserError> for ApiError {
} }
} }
impl From<LoginUserOidcError> for ApiError {
fn from(value: LoginUserOidcError) -> Self {
match value {
LoginUserOidcError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
LoginUserOidcError::CreateOrUpdateUser(e) => Self::InternalServerError(e.to_string()),
LoginUserOidcError::GetUserInfo(e) => Self::InternalServerError(e.to_string()),
LoginUserOidcError::CreateAuthToken(e) => Self::InternalServerError(e.to_string()),
LoginUserOidcError::Unknown(e) => Self::InternalServerError(e.to_string()),
}
}
}
impl From<FetchAuthSessionError> for ApiError { impl From<FetchAuthSessionError> for ApiError {
fn from(value: FetchAuthSessionError) -> Self { fn from(value: FetchAuthSessionError) -> Self {
match value { match value {
@@ -177,3 +195,17 @@ impl From<CreateUserError> for ApiError {
} }
} }
} }
impl From<GetOidcRedirectError> for ApiError {
fn from(value: GetOidcRedirectError) -> Self {
match value {
GetOidcRedirectError::GetRedirect(e) => match e {
crate::domain::oidc::requests::GetRedirectError::Unknown(e) => {
Self::InternalServerError(e.to_string())
}
},
GetOidcRedirectError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
GetOidcRedirectError::Unknown(e) => Self::InternalServerError(e.to_string()),
}
}
}

View File

@@ -68,6 +68,7 @@ impl LoginUserHttpRequestBody {
} }
#[derive(Debug, Clone, PartialEq, Serialize)] #[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoginResponseBody { pub struct LoginResponseBody {
token: String, token: String,
user: UserData, user: UserData,

View File

@@ -1,8 +1,13 @@
mod fetch_session; mod fetch_session;
mod login; mod login;
mod oidc_login;
mod oidc_redirect;
mod register; mod register;
use fetch_session::fetch_session; use fetch_session::fetch_session;
use login::login; use login::login;
use oidc_login::oidc_login;
use oidc_redirect::oidc_redirect;
use register::register; use register::register;
use axum::{ use axum::{
@@ -20,4 +25,6 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
.route("/register", post(register)) .route("/register", post(register))
.route("/login", post(login)) .route("/login", post(login))
.route("/session", get(fetch_session)) .route("/session", get(fetch_session))
.route("/oidc", get(oidc_redirect))
.route("/oidc/login", get(oidc_login))
} }

View File

@@ -0,0 +1,62 @@
use axum::{
extract::{Query, State},
http::StatusCode,
};
use serde::{Deserialize, Serialize};
use crate::{
domain::warren::{
models::user::{LoginUserOidcRequest, LoginUserOidcResponse},
ports::{AuthService, WarrenService},
},
inbound::http::{
AppState,
handlers::UserData,
responses::{ApiError, ApiSuccess},
},
};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OidcLoginRequestBody {
code: String,
state: Option<String>,
}
impl OidcLoginRequestBody {
pub fn into_domain(self) -> LoginUserOidcRequest {
LoginUserOidcRequest::new(self.code, self.state)
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct OidcLoginResponseBody {
token: String,
user: UserData,
expires_at: i64,
}
impl From<LoginUserOidcResponse> for OidcLoginResponseBody {
fn from(value: LoginUserOidcResponse) -> Self {
Self {
token: value.session().session_id().to_string(),
user: value.user().to_owned().into(),
expires_at: value.session().expires_at().and_utc().timestamp_millis(),
}
}
}
pub async fn oidc_login<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
Query(request): Query<OidcLoginRequestBody>,
) -> Result<ApiSuccess<OidcLoginResponseBody>, ApiError> {
let domain_request = request.into_domain();
state
.auth_service
.login_user_oidc(domain_request)
.await
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
.map_err(ApiError::from)
}

View File

@@ -0,0 +1,23 @@
use axum::{extract::State, http::StatusCode};
use crate::{
domain::warren::{
models::user::GetOidcRedirectRequest,
ports::{AuthService, WarrenService},
},
inbound::http::{
AppState,
responses::{ApiError, ApiSuccess},
},
};
pub async fn oidc_redirect<WS: WarrenService, AS: AuthService>(
State(state): State<AppState<WS, AS>>,
) -> Result<ApiSuccess<String>, ApiError> {
state
.auth_service
.get_oidc_redirect(GetOidcRedirectRequest::new())
.await
.map(|response| ApiSuccess::new(StatusCode::FOUND, response.url().clone()))
.map_err(ApiError::from)
}

View File

@@ -1,4 +1,7 @@
use crate::domain::warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics}; use crate::domain::{
oidc::ports::OidcMetrics,
warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics},
};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct MetricsDebugLogger; pub struct MetricsDebugLogger;
@@ -175,17 +178,17 @@ impl AuthMetrics for MetricsDebugLogger {
tracing::debug!("[Metrics] Warren creation by admin failed"); tracing::debug!("[Metrics] Warren creation by admin failed");
} }
async fn record_auth_warren_edit_success(&self) -> () { async fn record_auth_warren_edit_success(&self) {
tracing::debug!("[Metrics] Warren edit by admin succeeded"); tracing::debug!("[Metrics] Warren edit by admin succeeded");
} }
async fn record_auth_warren_edit_failure(&self) -> () { async fn record_auth_warren_edit_failure(&self) {
tracing::debug!("[Metrics] Warren edit by admin failed"); tracing::debug!("[Metrics] Warren edit by admin failed");
} }
async fn record_auth_warren_deletion_success(&self) -> () { async fn record_auth_warren_deletion_success(&self) {
tracing::debug!("[Metrics] Warren deletion by admin succeeded"); tracing::debug!("[Metrics] Warren deletion by admin succeeded");
} }
async fn record_auth_warren_deletion_failure(&self) -> () { async fn record_auth_warren_deletion_failure(&self) {
tracing::debug!("[Metrics] Warren deletion by admin failed"); tracing::debug!("[Metrics] Warren deletion by admin failed");
} }
@@ -203,6 +206,13 @@ impl AuthMetrics for MetricsDebugLogger {
tracing::debug!("[Metrics] User login failed"); tracing::debug!("[Metrics] User login failed");
} }
async fn record_user_login_oidc_success(&self) {
tracing::debug!("[Metrics] User login succeeded");
}
async fn record_user_login_oidc_failure(&self) {
tracing::debug!("[Metrics] User login failed");
}
async fn record_user_creation_success(&self) { async fn record_user_creation_success(&self) {
tracing::debug!("[Metrics] User creation succeeded"); tracing::debug!("[Metrics] User creation succeeded");
} }
@@ -350,3 +360,19 @@ impl AuthMetrics for MetricsDebugLogger {
tracing::debug!("[Metrics] Auth warren cp failed"); tracing::debug!("[Metrics] Auth warren cp failed");
} }
} }
impl OidcMetrics for MetricsDebugLogger {
async fn record_get_redirect_success(&self) {
tracing::debug!("[Metrics] OIDC get redirect succeeded");
}
async fn record_get_redirect_failure(&self) {
tracing::debug!("[Metrics] OIDC get redirect failed");
}
async fn record_get_user_info_success(&self) {
tracing::debug!("[Metrics] OIDC get user info succeeded");
}
async fn record_get_user_info_failure(&self) {
tracing::debug!("[Metrics] OIDC get user info failed");
}
}

View File

@@ -1,4 +1,5 @@
pub mod file_system; pub mod file_system;
pub mod metrics_debug_logger; pub mod metrics_debug_logger;
pub mod notifier_debug_logger; pub mod notifier_debug_logger;
pub mod oidc;
pub mod postgres; pub mod postgres;

View File

@@ -1,10 +1,17 @@
use uuid::Uuid; use uuid::Uuid;
use crate::domain::warren::{ use crate::domain::{
oidc::{
ports::OidcNotifier,
requests::{GetRedirectResponse, GetUserInfoResponse},
},
warren::{
models::{ models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, LsResponse}, file::{AbsoluteFilePath, LsResponse},
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User}, user::{
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
},
user_warren::UserWarren, user_warren::UserWarren,
warren::{ warren::{
Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse, Warren, WarrenCpResponse, WarrenLsResponse, WarrenMkdirResponse, WarrenMvResponse,
@@ -12,6 +19,7 @@ use crate::domain::warren::{
}, },
}, },
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier}, ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
},
}; };
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -220,6 +228,13 @@ impl AuthNotifier for NotifierDebugLogger {
tracing::debug!("[Notifier] Logged in user {}", response.user().name()); tracing::debug!("[Notifier] Logged in user {}", response.user().name());
} }
async fn user_logged_in_oidc(&self, response: &LoginUserOidcResponse) {
tracing::debug!(
"[Notifier] Logged in user {} with OIDC",
response.user().name()
);
}
async fn auth_session_created(&self, user_id: &Uuid) { async fn auth_session_created(&self, user_id: &Uuid) {
tracing::debug!("[Notifier] Created auth session for user {}", user_id); tracing::debug!("[Notifier] Created auth session for user {}", user_id);
} }
@@ -354,3 +369,19 @@ impl AuthNotifier for NotifierDebugLogger {
) )
} }
} }
impl OidcNotifier for NotifierDebugLogger {
async fn get_redirect(&self, response: &GetRedirectResponse) {
tracing::debug!("[Notifier] Got OIDC redirect: {}", response.url());
}
async fn get_user_info(&self, response: &GetUserInfoResponse) {
tracing::debug!(
"[Notifier] Got OIDC user info: {} ({})",
response
.info()
.preferred_username()
.unwrap_or(response.info().name()),
response.info().sub(),
);
}
}

View File

@@ -0,0 +1,187 @@
use std::{str::FromStr as _, sync::Arc};
use anyhow::Context as _;
use openid::{Client, CompactJson, CustomClaims, Discovered, Options, StandardClaims};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
config::Config,
domain::{
oidc::{
models::UserInfo,
ports::OidcRepository,
requests::{
GetRedirectError, GetRedirectRequest, GetRedirectResponse, GetUserInfoError,
GetUserInfoRequest, GetUserInfoResponse,
},
},
warren::models::user::{UserEmail, UserName},
},
};
type OidcClient = Client<Discovered, Claims>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OidcConfig {
issuer_url: String,
client_id: String,
client_secret: String,
redirect_url: String,
origin_url: String,
}
impl OidcConfig {
pub fn from_env() -> anyhow::Result<Self> {
let issuer_url = Config::load_env("OIDC_ISSUER_URL")?;
let client_id = Config::load_env("OIDC_CLIENT_ID")?;
let client_secret = Config::load_env("OIDC_CLIENT_SECRET")?;
let origin = Config::load_env("OIDC_ORIGIN_URL")?;
let redirect_url = Config::load_env("OIDC_REDIRECT_URL")?;
Ok(Self::new(
issuer_url,
client_id,
client_secret,
redirect_url,
origin,
))
}
pub fn new(
issuer_url: String,
client_id: String,
client_secret: String,
redirect_url: String,
origin_url: String,
) -> Self {
Self {
issuer_url,
client_id,
client_secret,
redirect_url,
origin_url,
}
}
}
#[derive(Debug, Clone)]
pub struct Oidc {
client: OidcClient,
auth_options: Arc<openid::Options>,
}
impl Oidc {
pub async fn new(config: &OidcConfig) -> anyhow::Result<Self> {
let client = OidcClient::discover(
config.client_id.clone(),
config.client_secret.clone(),
config.redirect_url.clone(),
Url::from_str(&config.issuer_url)?,
)
.await?;
let auth_options = Options {
scope: Some("openid profile email".into()),
state: Some(config.origin_url.clone()),
..Default::default()
};
Ok(Self {
client,
auth_options: Arc::new(auth_options),
})
}
}
#[derive(Debug, Deserialize, Serialize)]
struct Claims {
warren_admin: Option<bool>,
#[serde(flatten)]
standard_claims: StandardClaims,
}
impl CustomClaims for Claims {
fn standard_claims(&self) -> &StandardClaims {
&self.standard_claims
}
}
impl CompactJson for Claims {}
impl OidcRepository for Oidc {
async fn get_redirect(
&self,
_request: GetRedirectRequest,
) -> Result<GetRedirectResponse, GetRedirectError> {
let url = self.client.auth_url(&self.auth_options);
Ok(GetRedirectResponse::new(url.into()))
}
async fn get_user_info(
&self,
request: GetUserInfoRequest,
) -> Result<GetUserInfoResponse, GetUserInfoError> {
let mut token: openid::Token<Claims> = self
.client
.request_token(request.code())
.await
.context("Failed to request token")?
.into();
let id_token = token
.id_token
.as_mut()
.context("Token didn't have id_token field")?;
self.client
.decode_token(id_token)
.context("Failed to decode token")?;
id_token
.payload_mut()
.context("Failed to get id_token payload")?
.standard_claims
.azp = Some(self.client.client_id.clone());
self.client
.validate_token(id_token, None, None)
.context("Failed to validate token")?;
let user_info = try_get_user_info(
id_token
.payload()
.context("Failed to get id token payload")?,
)?;
Ok(GetUserInfoResponse::new(user_info))
}
}
fn try_get_user_info(claims: &Claims) -> anyhow::Result<UserInfo> {
let sub = claims.standard_claims.sub.clone();
let raw = &claims.standard_claims.userinfo;
let name = UserName::new(raw.name.as_ref().context("Missing name")?)?;
let email = UserEmail::new(raw.email.as_ref().context("Missing email")?)?;
let preferred_username = if let Some(preferred_username) = raw.preferred_username.as_ref() {
Some(UserName::new(preferred_username)?)
} else {
None
};
let user_info = UserInfo::new(
sub,
name,
email,
preferred_username,
raw.picture.as_ref().map(|url| url.clone().into()),
raw.locale.clone(),
raw.updated_at,
claims.warren_admin,
);
Ok(user_info)
}

View File

@@ -1,6 +1,6 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{Context as _, anyhow}; use anyhow::{Context as _, anyhow, bail};
use argon2::{ use argon2::{
Argon2, PasswordHash, PasswordVerifier as _, Argon2, PasswordHash, PasswordVerifier as _,
password_hash::{ password_hash::{
@@ -23,8 +23,9 @@ use crate::domain::warren::{
}, },
}, },
user::{ user::{
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
EditUserRequest, ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError, EditUserRequest,
ListAllUsersAndWarrensError, ListAllUsersAndWarrensRequest,
ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, UserEmail, ListAllUsersAndWarrensResponse, ListUsersError, ListUsersRequest, User, UserEmail,
UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest, UserName, UserPassword, VerifyUserPasswordError, VerifyUserPasswordRequest,
}, },
@@ -66,6 +67,30 @@ impl AuthRepository for Postgres {
Ok(user) Ok(user)
} }
async fn create_or_update_user_oidc(
&self,
request: CreateOrUpdateUserOidcRequest,
) -> Result<User, CreateOrUpdateUserOidcError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
let user = self
.create_or_update_user(
&mut connection,
request.sub(),
request.name(),
request.email(),
request.admin(),
)
.await
.context(format!("Failed to create or update user"))?;
Ok(user)
}
async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> { async fn edit_user(&self, request: EditUserRequest) -> Result<User, EditUserError> {
let mut connection = self let mut connection = self
.pool .pool
@@ -127,7 +152,11 @@ impl AuthRepository for Postgres {
} }
})?; })?;
self.check_user_password_against_hash(request.password(), user.password_hash())?; self.check_user_password_against_hash(
request.password(),
user.password_hash()
.ok_or(VerifyUserPasswordError::PasswordNotAllowed)?,
)?;
Ok(user) Ok(user)
} }
@@ -423,6 +452,121 @@ impl Postgres {
Ok(user) Ok(user)
} }
async fn create_or_update_user(
&self,
connection: &mut PgConnection,
sub: &String,
name: &UserName,
email: &UserEmail,
is_admin: bool,
) -> anyhow::Result<User> {
let mut tx = connection.begin().await?;
let existing_user: Option<User> = sqlx::query_as(
"
SELECT
*
FROM
users
WHERE
oidc_sub = $1 OR
email = $2
",
)
.bind(sub)
.bind(email)
.fetch_optional(&mut *tx)
.await?;
let user: User = match existing_user {
Some(existing_user) => {
if let Some(existing_oidc_sub) = existing_user.oidc_sub() {
if existing_oidc_sub != sub {
// TODO: Return a proper error
bail!("The user is already linked to another OIDC subject");
}
sqlx::query_as(
"
UPDATE
users
SET
name = $3,
email = $4,
admin = $5
WHERE
id = $1 AND
oidc_sub = $2
RETURNING
*
",
)
.bind(existing_user.id())
.bind(sub)
.bind(name)
.bind(email)
.bind(is_admin)
.fetch_one(&mut *tx)
.await?
} else {
sqlx::query_as(
"
UPDATE
users
SET
oidc_sub = $2,
name = $3,
email = $4,
admin = $5
WHERE
id = $1 AND
oidc_sub IS NULL
RETURNING
*
",
)
.bind(existing_user.id())
.bind(sub)
.bind(name)
.bind(email)
.bind(is_admin)
.fetch_one(&mut *tx)
.await?
}
}
None => {
sqlx::query_as(
"
INSERT INTO users (
oidc_sub,
name,
email,
admin
)
VALUES (
$1,
$2,
$3,
$4
)
RETURNING
*
",
)
.bind(sub)
.bind(name)
.bind(email)
.bind(is_admin)
.fetch_one(&mut *tx)
.await?
}
};
tx.commit().await?;
Ok(user)
}
async fn edit_user( async fn edit_user(
&self, &self,
connection: &mut PgConnection, connection: &mut PgConnection,

View File

@@ -1,6 +1,7 @@
export async function logout() { export async function logout() {
useAuthSession().value = null; useAuthSession().value = null;
useAdminStore().$reset(); useAdminStore().$reset();
useWarrenStore().$reset();
await navigateTo({ await navigateTo({
path: '/login', path: '/login',
}); });

View File

@@ -0,0 +1,74 @@
import type { ApiResponse } from '~/shared/types/api';
import { getApiHeaders } from '..';
import { toast } from 'vue-sonner';
import type { AuthUser } from '~/shared/types/auth';
export async function getRedirectUrl(): Promise<
{ success: true; url: string } | { success: false }
> {
const { data, error } = await useFetch<ApiResponse<string>>(
getApiUrl('auth/oidc'),
{
method: 'GET',
headers: getApiHeaders(false),
responseType: 'json',
}
);
if (data.value == null) {
toast.error('OpenID Connect', {
description: error.value?.data ?? 'Failed to get OIDC redirect',
});
return {
success: false,
};
}
return {
success: true,
url: data.value.data,
};
}
export async function oidcLoginUser(
code: string,
state: string
): Promise<{ success: boolean }> {
const { data, error } = await useFetch<
ApiResponse<{ token: string; user: AuthUser; expiresAt: number }>
>(getApiUrl(`auth/oidc/login?code=${code}&state=${state}`), {
method: 'GET',
headers: getApiHeaders(false),
responseType: 'json',
retry: false,
});
if (data.value == null) {
toast.error('OpenID Connect', {
description: error.value?.data ?? 'Failed to login with OIDC',
});
return {
success: false,
};
}
const token = data.value.data.token;
const { user, expiresAt } = data.value.data;
useAuthSession().value = {
type: 'WarrenAuth',
id: token,
user,
expiresAt,
};
toast.success('OpenID Connect', {
description: `Successfully logged in`,
});
return {
success: true,
};
}

View File

@@ -10,6 +10,7 @@ import {
import { toTypedSchema } from '@vee-validate/yup'; import { toTypedSchema } from '@vee-validate/yup';
import { useForm } from 'vee-validate'; import { useForm } from 'vee-validate';
import { loginUser } from '~/lib/api/auth/login'; import { loginUser } from '~/lib/api/auth/login';
import { getRedirectUrl, oidcLoginUser } from '~/lib/api/auth/oidc';
import { loginSchema } from '~/lib/schemas/auth'; import { loginSchema } from '~/lib/schemas/auth';
definePageMeta({ definePageMeta({
@@ -20,10 +21,31 @@ useHead({
title: 'Login - Warren', title: 'Login - Warren',
}); });
const route = useRoute();
// TODO: Get this from the backend // TODO: Get this from the backend
const OPEN_ID = false; const OPEN_ID = true;
const loggingIn = ref(false); const loggingIn = ref(false);
if (
route.query.code &&
typeof route.query.code === 'string' &&
route.query.state &&
typeof route.query.state === 'string'
) {
console.log('SEND');
loggingIn.value = true;
const { success } = await oidcLoginUser(
route.query.code,
route.query.state
);
loggingIn.value = false;
if (success) {
await navigateTo({ path: '/' });
}
}
const form = useForm({ const form = useForm({
validationSchema: toTypedSchema(loginSchema), validationSchema: toTypedSchema(loginSchema),
}); });
@@ -43,6 +65,24 @@ const onSubmit = form.handleSubmit(async (values) => {
loggingIn.value = false; loggingIn.value = false;
}); });
async function oidcClicked() {
if (loggingIn.value) {
return;
}
loggingIn.value = true;
const result = await getRedirectUrl();
if (result.success) {
await navigateTo(result.url, {
external: true,
});
} else {
loggingIn.value = false;
}
}
</script> </script>
<template> <template>
@@ -97,7 +137,11 @@ const onSubmit = form.handleSubmit(async (values) => {
:disabled="loggingIn" :disabled="loggingIn"
>Log in</Button >Log in</Button
> >
<Button class="w-full" variant="outline" :disabled="!OPEN_ID" <Button
class="w-full"
variant="outline"
:disabled="!OPEN_ID || loggingIn"
@click="oidcClicked"
>OpenID Connect</Button >OpenID Connect</Button
> >
<NuxtLink to="/register" class="w-full"> <NuxtLink to="/register" class="w-full">