Compare commits

...

10 Commits

Author SHA1 Message Date
2fe1b4a8b2 Fetch upstream configuration
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-25 16:35:32 +02:00
16ef969e29 Fix bad comment
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-25 15:06:00 +02:00
0fa58f4d3a Generate state for authentication
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-25 15:03:56 +02:00
a0325fefbf Add providers buttons on login page
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-25 14:02:23 +02:00
92d04f3312 Fix bad comment
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 19:14:46 +02:00
abd86ff22d Can set authorized authentication providers for a given account 2023-04-24 19:13:36 +02:00
f64f01a958 Can block local login for an account
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 18:46:21 +02:00
96ffc669d7 Add logo of popular brands
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 16:07:14 +02:00
d9f659ce98 Add basic providers configuration
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 15:43:49 +02:00
e73b5b8e5b Update dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 15:14:10 +02:00
29 changed files with 1052 additions and 58 deletions

333
Cargo.lock generated
View File

@@ -317,9 +317,9 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.20" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -568,6 +568,7 @@ dependencies = [
"mime_guess", "mime_guess",
"qrcode-generator", "qrcode-generator",
"rand", "rand",
"reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@@ -664,9 +665,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.12.0" version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
@@ -737,9 +738,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.2.2" version = "4.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b802d85aaf3a1cdb02b224ba472ebdea62014fccfcb269b95a4d76443b5ee5a" checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -748,9 +749,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.2.2" version = "4.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a1a858f532119338887a4b8e1af9c60de8249cd7bafd68036a489e261e37b6" checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -858,6 +859,16 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.4" version = "0.8.4"
@@ -866,9 +877,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -1137,6 +1148,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]] [[package]]
name = "fdeflate" name = "fdeflate"
version = "0.3.0" version = "0.3.0"
@@ -1196,6 +1216,15 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures-channel"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.28" version = "0.3.28"
@@ -1283,9 +1312,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.17" version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
dependencies = [ dependencies = [
"bytes", "bytes",
"fnv", "fnv",
@@ -1401,6 +1430,17 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]] [[package]]
name = "httparse" name = "httparse"
version = "1.8.0" version = "1.8.0"
@@ -1428,6 +1468,43 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.56" version = "0.1.56"
@@ -1514,6 +1591,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.10" version = "1.0.10"
@@ -1525,6 +1611,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "ipnet"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.7" version = "0.4.7"
@@ -1641,9 +1733,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.141" version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]] [[package]]
name = "libm" name = "libm"
@@ -1662,9 +1754,9 @@ dependencies = [
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.3.1" version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
[[package]] [[package]]
name = "local-channel" name = "local-channel"
@@ -1762,6 +1854,24 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@@ -1875,9 +1985,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.50" version = "0.10.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1" checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if", "cfg-if",
@@ -1900,10 +2010,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "openssl-sys" name = "openssl-probe"
version = "0.9.85" 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 = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@@ -1953,7 +2069,7 @@ checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall 0.2.16",
"smallvec", "smallvec",
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
@@ -2134,10 +2250,19 @@ dependencies = [
] ]
[[package]] [[package]]
name = "regex" name = "redox_syscall"
version = "1.7.3" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@@ -2146,9 +2271,46 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.29" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "reqwest"
version = "0.11.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
dependencies = [
"base64 0.21.0",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]] [[package]]
name = "rfc6979" name = "rfc6979"
@@ -2217,9 +2379,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.11" version = "0.37.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@@ -2235,6 +2397,15 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -2261,6 +2432,29 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.17" version = "1.0.17"
@@ -2477,6 +2671,19 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "tempfile"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"rustix",
"windows-sys 0.45.0",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.2.0" version = "1.2.0"
@@ -2576,6 +2783,16 @@ dependencies = [
"windows-sys 0.45.0", "windows-sys 0.45.0",
] ]
[[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-util" name = "tokio-util"
version = "0.7.7" version = "0.7.7"
@@ -2599,6 +2816,12 @@ dependencies = [
"ring", "ring",
] ]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.37" version = "0.1.37"
@@ -2632,6 +2855,12 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.16.0" version = "1.16.0"
@@ -2754,6 +2983,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "want"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0"
dependencies = [
"log",
"try-lock",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" version = "0.10.0+wasi-snapshot-preview1"
@@ -2791,6 +3030,18 @@ dependencies = [
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.84" version = "0.2.84"
@@ -2920,6 +3171,21 @@ dependencies = [
"windows-targets 0.48.0", "windows-targets 0.48.0",
] ]
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"
@@ -3052,6 +3318,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 = "x509-parser" name = "x509-parser"
version = "0.13.2" version = "0.13.2"

View File

@@ -38,3 +38,4 @@ aes-gcm = { version = "0.10.1", features = ["aes"] }
bincode = "1.3.3" bincode = "1.3.3"
chrono = "0.4.24" chrono = "0.4.24"
lazy_static = "1.4.0" lazy_static = "1.4.0"
reqwest = { version = "0.11.16", features = ["json"] }

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve"><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-custom-link"/><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-general-link"/><style xmlns="" lang="en" type="text/css" id="dark-mode-custom-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-sheet"/>
<g>
<path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
<path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="705.6" height="720" viewBox="0 0 186.69 190.5" xmlns:v="https://vecta.io/nano"><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-custom-link"/><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-general-link"/><style xmlns="" lang="en" type="text/css" id="dark-mode-custom-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-sheet"/><g transform="translate(1184.583 765.171)"><path clip-path="none" mask="none" d="M-1089.333-687.239v36.888h51.262c-2.251 11.863-9.006 21.908-19.137 28.662l30.913 23.986c18.011-16.625 28.402-41.044 28.402-70.052 0-6.754-.606-13.249-1.732-19.483z" fill="#4285f4"/><path clip-path="none" mask="none" d="M-1142.714-651.791l-6.972 5.337-24.679 19.223h0c15.673 31.086 47.796 52.561 85.03 52.561 25.717 0 47.278-8.486 63.038-23.033l-30.913-23.986c-8.486 5.715-19.31 9.179-32.125 9.179-24.765 0-45.806-16.712-53.34-39.226z" fill="#34a853"/><path clip-path="none" mask="none" d="M-1174.365-712.61c-6.494 12.815-10.217 27.276-10.217 42.689s3.723 29.874 10.217 42.689c0 .086 31.693-24.592 31.693-24.592-1.905-5.715-3.031-11.776-3.031-18.098s1.126-12.383 3.031-18.098z" fill="#fbbc05"/><path d="M-1089.333-727.244c14.028 0 26.497 4.849 36.455 14.201l27.276-27.276c-16.539-15.413-38.013-24.852-63.731-24.852-37.234 0-69.359 21.388-85.032 52.561l31.692 24.592c7.533-22.514 28.575-39.226 53.34-39.226z" fill="#ea4335" clip-path="none" mask="none"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><path fill="#f35325" d="M0 0h10v10H0z"/><path fill="#81bc06" d="M11 0h10v10H11z"/><path fill="#05a6f0" d="M0 11h10v10H0z"/><path fill="#ffba08" d="M11 11h10v10H11z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@@ -1,3 +1,4 @@
pub mod bruteforce_actor; pub mod bruteforce_actor;
pub mod openid_sessions_actor; pub mod openid_sessions_actor;
pub mod providers_states_actor;
pub mod users_actor; pub mod users_actor;

View File

@@ -0,0 +1,130 @@
//! # Providers state actor
//!
//! This actor stores the content of the states
//! during authentication with upstream providers
use crate::constants::{
MAX_OIDC_PROVIDERS_STATES, OIDC_PROVIDERS_STATE_DURATION, OIDC_PROVIDERS_STATE_LEN,
OIDC_STATES_CLEANUP_INTERVAL,
};
use actix::{Actor, AsyncContext, Context, Handler, Message};
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::net::IpAddr;
use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::ProviderID;
use crate::utils::string_utils::rand_str;
use crate::utils::time::time;
#[derive(Debug, Clone)]
pub struct ProviderLoginState {
pub provider_id: ProviderID,
pub state_id: String,
pub redirect: LoginRedirect,
pub expire: u64,
}
impl ProviderLoginState {
pub fn new(prov_id: &ProviderID, redirect: LoginRedirect) -> Self {
Self {
provider_id: prov_id.clone(),
state_id: rand_str(OIDC_PROVIDERS_STATE_LEN),
redirect,
expire: time() + OIDC_PROVIDERS_STATE_DURATION,
}
}
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct RecordState {
pub ip: IpAddr,
pub state: ProviderLoginState,
}
#[derive(Message)]
#[rtype(result = "Option<ProviderLoginState>")]
pub struct ConsumeState {
pub ip: IpAddr,
pub state_id: String,
}
#[derive(Debug, Default)]
pub struct ProvidersStatesActor {
states: HashMap<IpAddr, Vec<ProviderLoginState>>,
}
impl ProvidersStatesActor {
/// Clean outdated states
fn clean_old_states(&mut self) {
#[allow(clippy::map_clone)]
let keys = self.states.keys().map(|i| *i).collect::<Vec<_>>();
for ip in keys {
// Remove old states
let states = self.states.get_mut(&ip).unwrap();
states.retain(|i| i.expire < time());
// Remove empty entry keys
if states.is_empty() {
self.states.remove(&ip);
}
}
}
/// Add a new provider login state
pub fn insert_state(&mut self, ip: IpAddr, state: ProviderLoginState) {
if let Entry::Vacant(e) = self.states.entry(ip) {
e.insert(vec![state]);
} else {
let states = self.states.get_mut(&ip).unwrap();
// We limit the number of states per IP address
if states.len() > MAX_OIDC_PROVIDERS_STATES {
states.remove(0);
}
states.push(state);
}
}
/// Get & consume a login state
pub fn consume_state(&mut self, ip: IpAddr, state_id: &str) -> Option<ProviderLoginState> {
let idx = self
.states
.get(&ip)?
.iter()
.position(|val| val.state_id.as_str() == state_id)?;
Some(self.states.get_mut(&ip)?.remove(idx))
}
}
impl Actor for ProvidersStatesActor {
type Context = Context<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// Clean up at a regular interval failed attempts
ctx.run_interval(OIDC_STATES_CLEANUP_INTERVAL, |act, _ctx| {
log::trace!("Cleaning up old states");
act.clean_old_states();
});
}
}
impl Handler<RecordState> for ProvidersStatesActor {
type Result = ();
fn handle(&mut self, req: RecordState, _ctx: &mut Self::Context) -> Self::Result {
self.insert_state(req.ip, req.state);
}
}
impl Handler<ConsumeState> for ProvidersStatesActor {
type Result = Option<ProviderLoginState>;
fn handle(&mut self, req: ConsumeState, _ctx: &mut Self::Context) -> Self::Result {
self.consume_state(req.ip, &req.state_id)
}
}

View File

@@ -1,5 +1,6 @@
use std::net::IpAddr; use std::net::IpAddr;
use crate::data::provider::ProviderID;
use actix::{Actor, Context, Handler, Message, MessageResult}; use actix::{Actor, Context, Handler, Message, MessageResult};
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID}; use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
@@ -19,6 +20,11 @@ pub trait UsersSyncBackend {
fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res; fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res;
fn clear_2fa_login_history(&mut self, id: &UserID) -> Res; fn clear_2fa_login_history(&mut self, id: &UserID) -> Res;
fn delete_account(&mut self, id: &UserID) -> Res; fn delete_account(&mut self, id: &UserID) -> Res;
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res;
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res; fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res;
} }
@@ -28,12 +34,13 @@ pub enum LoginResult {
AccountNotFound, AccountNotFound,
InvalidPassword, InvalidPassword,
AccountDisabled, AccountDisabled,
LocalAuthForbidden,
Success(Box<User>), Success(Box<User>),
} }
#[derive(Message)] #[derive(Message)]
#[rtype(LoginResult)] #[rtype(LoginResult)]
pub struct LoginRequest { pub struct LocalLoginRequest {
pub login: String, pub login: String,
pub password: String, pub password: String,
} }
@@ -88,6 +95,16 @@ pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
#[rtype(result = "bool")] #[rtype(result = "bool")]
pub struct Clear2FALoginHistory(pub UserID); pub struct Clear2FALoginHistory(pub UserID);
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct AuthorizedAuthenticationSources {
pub local: bool,
pub upstream: Vec<ProviderID>,
}
#[derive(Message)]
#[rtype(result = "bool")]
pub struct SetAuthorizedAuthenticationSources(pub UserID, pub AuthorizedAuthenticationSources);
#[derive(Message)] #[derive(Message)]
#[rtype(result = "bool")] #[rtype(result = "bool")]
pub struct SetGrantedClients(pub UserID, pub GrantedClients); pub struct SetGrantedClients(pub UserID, pub GrantedClients);
@@ -119,10 +136,10 @@ impl Actor for UsersActor {
type Context = Context<Self>; type Context = Context<Self>;
} }
impl Handler<LoginRequest> for UsersActor { impl Handler<LocalLoginRequest> for UsersActor {
type Result = MessageResult<LoginRequest>; type Result = MessageResult<LocalLoginRequest>;
fn handle(&mut self, msg: LoginRequest, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LocalLoginRequest, _ctx: &mut Self::Context) -> Self::Result {
match self.manager.find_by_username_or_email(&msg.login) { match self.manager.find_by_username_or_email(&msg.login) {
Err(e) => { Err(e) => {
log::error!("Failed to find user! {}", e); log::error!("Failed to find user! {}", e);
@@ -142,6 +159,10 @@ impl Handler<LoginRequest> for UsersActor {
return MessageResult(LoginResult::AccountDisabled); return MessageResult(LoginResult::AccountDisabled);
} }
if !user.allow_local_login {
return MessageResult(LoginResult::LocalAuthForbidden);
}
MessageResult(LoginResult::Success(Box::new(user))) MessageResult(LoginResult::Success(Box::new(user)))
} }
} }
@@ -241,6 +262,29 @@ impl Handler<Clear2FALoginHistory> for UsersActor {
} }
} }
impl Handler<SetAuthorizedAuthenticationSources> for UsersActor {
type Result = <SetAuthorizedAuthenticationSources as actix::Message>::Result;
fn handle(
&mut self,
msg: SetAuthorizedAuthenticationSources,
_ctx: &mut Self::Context,
) -> Self::Result {
match self
.manager
.set_authorized_authentication_sources(&msg.0, msg.1)
{
Ok(_) => true,
Err(e) => {
log::error!(
"Failed to set authorized authentication sources for user! {}",
e
);
false
}
}
}
}
impl Handler<SetGrantedClients> for UsersActor { impl Handler<SetGrantedClients> for UsersActor {
type Result = <SetGrantedClients as actix::Message>::Result; type Result = <SetGrantedClients as actix::Message>::Result;
fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result {

View File

@@ -6,6 +6,9 @@ pub const USERS_LIST_FILE: &str = "users.json";
/// File in storage containing clients list /// File in storage containing clients list
pub const CLIENTS_LIST_FILE: &str = "clients.yaml"; pub const CLIENTS_LIST_FILE: &str = "clients.yaml";
/// File in storage containing providers list
pub const PROVIDERS_LIST_FILE: &str = "providers.yaml";
/// Default built-in credentials /// Default built-in credentials
pub const DEFAULT_ADMIN_USERNAME: &str = "admin"; pub const DEFAULT_ADMIN_USERNAME: &str = "admin";
pub const DEFAULT_ADMIN_PASSWORD: &str = "admin"; pub const DEFAULT_ADMIN_PASSWORD: &str = "admin";
@@ -68,3 +71,12 @@ pub const OPEN_ID_REFRESH_TOKEN_TIMEOUT: u64 = 360000;
/// Webauthn constants /// Webauthn constants
pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600; pub const WEBAUTHN_REGISTER_CHALLENGE_EXPIRE: u64 = 3600;
pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600; pub const WEBAUTHN_LOGIN_CHALLENGE_EXPIRE: u64 = 3600;
/// OpenID providers login state constants
pub const OIDC_STATES_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
pub const MAX_OIDC_PROVIDERS_STATES: usize = 10;
pub const OIDC_PROVIDERS_STATE_LEN: usize = 40;
pub const OIDC_PROVIDERS_STATE_DURATION: u64 = 60 * 15;
/// OpenID providers configuration constants
pub const OIDC_PROVIDERS_LIFETIME: u64 = 3600;

View File

@@ -6,12 +6,13 @@ use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::constants::TEMPORARY_PASSWORDS_LEN; use crate::constants::TEMPORARY_PASSWORDS_LEN;
use crate::controllers::settings_controller::BaseSettingsPage; use crate::controllers::settings_controller::BaseSettingsPage;
use crate::data::action_logger::{Action, ActionLogger}; use crate::data::action_logger::{Action, ActionLogger};
use crate::data::client::{Client, ClientID, ClientManager}; use crate::data::client::{Client, ClientID, ClientManager};
use crate::data::current_user::CurrentUser; use crate::data::current_user::CurrentUser;
use crate::data::provider::{Provider, ProviderID, ProvidersManager};
use crate::data::user::{GeneralSettings, GrantedClients, User, UserID}; use crate::data::user::{GeneralSettings, GrantedClients, User, UserID};
use crate::utils::string_utils::rand_str; use crate::utils::string_utils::rand_str;
@@ -22,6 +23,13 @@ struct ClientsListTemplate {
clients: Vec<Client>, clients: Vec<Client>,
} }
#[derive(Template)]
#[template(path = "settings/providers_list.html")]
struct ProvidersListTemplate {
_p: BaseSettingsPage,
providers: Vec<Provider>,
}
#[derive(Template)] #[derive(Template)]
#[template(path = "settings/users_list.html")] #[template(path = "settings/users_list.html")]
struct UsersListTemplate { struct UsersListTemplate {
@@ -35,6 +43,7 @@ struct EditUserTemplate {
_p: BaseSettingsPage, _p: BaseSettingsPage,
u: User, u: User,
clients: Vec<Client>, clients: Vec<Client>,
providers: Vec<Provider>,
} }
pub async fn clients_route( pub async fn clients_route(
@@ -51,6 +60,20 @@ pub async fn clients_route(
) )
} }
pub async fn providers_route(
user: CurrentUser,
providers: web::Data<Arc<ProvidersManager>>,
) -> impl Responder {
HttpResponse::Ok().body(
ProvidersListTemplate {
_p: BaseSettingsPage::get("OpenID Providers list", &user, None, None),
providers: providers.cloned(),
}
.render()
.unwrap(),
)
}
#[derive(serde::Deserialize, Debug)] #[derive(serde::Deserialize, Debug)]
pub struct UpdateUserQuery { pub struct UpdateUserQuery {
uid: UserID, uid: UserID,
@@ -62,6 +85,8 @@ pub struct UpdateUserQuery {
enabled: Option<String>, enabled: Option<String>,
two_factor_exemption_after_successful_login: Option<String>, two_factor_exemption_after_successful_login: Option<String>,
admin: Option<String>, admin: Option<String>,
allow_local_login: Option<String>,
authorized_sources: String,
grant_type: String, grant_type: String,
granted_clients: String, granted_clients: String,
two_factor: String, two_factor: String,
@@ -136,6 +161,29 @@ pub async fn users_route(
} }
} }
// Update the list of authorized authentication sources
let auth_sources = AuthorizedAuthenticationSources {
local: update.0.allow_local_login.is_some(),
upstream: match update.0.authorized_sources.as_str() {
"" => vec![],
s => s.split(',').map(|s| ProviderID(s.to_string())).collect(),
},
};
if edited_user.authorized_authentication_sources() != auth_sources {
logger.log(Action::AdminSetAuthorizedAuthenticationSources(
&edited_user,
&auth_sources,
));
users
.send(users_actor::SetAuthorizedAuthenticationSources(
edited_user.uid.clone(),
auth_sources,
))
.await
.unwrap();
}
// Update list of granted clients // Update list of granted clients
let granted_clients = match update.0.grant_type.as_str() { let granted_clients = match update.0.grant_type.as_str() {
"all_clients" => GrantedClients::AllClients, "all_clients" => GrantedClients::AllClients,
@@ -240,6 +288,7 @@ pub async fn users_route(
pub async fn create_user( pub async fn create_user(
admin: CurrentUser, admin: CurrentUser,
clients: web::Data<Arc<ClientManager>>, clients: web::Data<Arc<ClientManager>>,
providers: web::Data<Arc<ProvidersManager>>,
) -> impl Responder { ) -> impl Responder {
let user = User { let user = User {
authorized_clients: Some( authorized_clients: Some(
@@ -257,6 +306,7 @@ pub async fn create_user(
_p: BaseSettingsPage::get("Create a new user", admin.deref(), None, None), _p: BaseSettingsPage::get("Create a new user", admin.deref(), None, None),
u: user, u: user,
clients: clients.cloned(), clients: clients.cloned(),
providers: providers.cloned(),
} }
.render() .render()
.unwrap(), .unwrap(),
@@ -271,6 +321,7 @@ pub struct EditUserQuery {
pub async fn edit_user( pub async fn edit_user(
admin: CurrentUser, admin: CurrentUser,
clients: web::Data<Arc<ClientManager>>, clients: web::Data<Arc<ClientManager>>,
providers: web::Data<Arc<ProvidersManager>>,
users: web::Data<Addr<UsersActor>>, users: web::Data<Addr<UsersActor>>,
query: web::Query<EditUserQuery>, query: web::Query<EditUserQuery>,
) -> impl Responder { ) -> impl Responder {
@@ -293,6 +344,7 @@ pub async fn edit_user(
), ),
u: edited_account.unwrap_or_default(), u: edited_account.unwrap_or_default(),
clients: clients.cloned(), clients: clients.cloned(),
providers: providers.cloned(),
} }
.render() .render()
.unwrap(), .unwrap(),

View File

@@ -2,6 +2,7 @@ use actix::Addr;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpRequest, HttpResponse, Responder}; use actix_web::{web, HttpRequest, HttpResponse, Responder};
use askama::Template; use askama::Template;
use std::sync::Arc;
use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::users_actor::{LoginResult, UsersActor}; use crate::actors::users_actor::{LoginResult, UsersActor};
@@ -12,6 +13,7 @@ use crate::controllers::base_controller::{
}; };
use crate::data::action_logger::{Action, ActionLogger}; use crate::data::action_logger::{Action, ActionLogger};
use crate::data::login_redirect::LoginRedirect; use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::{Provider, ProvidersManager};
use crate::data::remote_ip::RemoteIP; use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::{SessionIdentity, SessionStatus}; use crate::data::session_identity::{SessionIdentity, SessionStatus};
use crate::data::user::User; use crate::data::user::User;
@@ -30,6 +32,7 @@ struct BaseLoginPage<'a> {
struct LoginTemplate<'a> { struct LoginTemplate<'a> {
_p: BaseLoginPage<'a>, _p: BaseLoginPage<'a>,
login: String, login: String,
providers: Vec<Provider>,
} }
#[derive(Template)] #[derive(Template)]
@@ -77,6 +80,7 @@ pub struct LoginRequestQuery {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn login_route( pub async fn login_route(
remote_ip: RemoteIP, remote_ip: RemoteIP,
providers: web::Data<Arc<ProvidersManager>>,
users: web::Data<Addr<UsersActor>>, users: web::Data<Addr<UsersActor>>,
bruteforce: web::Data<Addr<BruteForceActor>>, bruteforce: web::Data<Addr<BruteForceActor>>,
query: web::Query<LoginRequestQuery>, query: web::Query<LoginRequestQuery>,
@@ -121,7 +125,7 @@ pub async fn login_route(
query.redirect.get_encoded() query.redirect.get_encoded()
)); ));
} }
// Check if the user has to valide a second factor // Check if the user has to validate a second factor
else if SessionIdentity(id.as_ref()).need_2fa_auth() { else if SessionIdentity(id.as_ref()).need_2fa_auth() {
return redirect_user(&format!( return redirect_user(&format!(
"/2fa_auth?redirect={}", "/2fa_auth?redirect={}",
@@ -132,7 +136,7 @@ pub async fn login_route(
else if let Some(req) = &req { else if let Some(req) = &req {
login = req.login.clone(); login = req.login.clone();
let response: LoginResult = users let response: LoginResult = users
.send(users_actor::LoginRequest { .send(users_actor::LocalLoginRequest {
login: login.clone(), login: login.clone(),
password: req.password.clone(), password: req.password.clone(),
}) })
@@ -163,6 +167,12 @@ pub async fn login_route(
danger = Some("Your account is disabled!".to_string()); danger = Some("Your account is disabled!".to_string());
} }
LoginResult::LocalAuthForbidden => {
log::warn!("Failed login for username {} : attempted to use local auth, but it is forbidden", &login);
logger.log(Action::TryLocalLoginFromUnauthorizedAccount(&login));
danger = Some("You cannot login from local auth with your account!".to_string());
}
LoginResult::Error => { LoginResult::Error => {
danger = Some("An unkown error occured while trying to sign you in!".to_string()); danger = Some("An unkown error occured while trying to sign you in!".to_string());
} }
@@ -197,6 +207,7 @@ pub async fn login_route(
redirect_uri: &query.redirect, redirect_uri: &query.redirect,
}, },
login, login,
providers: providers.cloned(),
} }
.render() .render()
.unwrap(), .unwrap(),

View File

@@ -5,6 +5,7 @@ pub mod base_controller;
pub mod login_api; pub mod login_api;
pub mod login_controller; pub mod login_controller;
pub mod openid_controller; pub mod openid_controller;
pub mod providers_controller;
pub mod settings_controller; pub mod settings_controller;
pub mod two_factor_api; pub mod two_factor_api;
pub mod two_factors_controller; pub mod two_factors_controller;

View File

@@ -0,0 +1,71 @@
use std::sync::Arc;
use actix::Addr;
use actix_web::{web, HttpResponse, Responder};
use crate::actors::providers_states_actor;
use crate::actors::providers_states_actor::{ProviderLoginState, ProvidersStatesActor};
use crate::controllers::base_controller::build_fatal_error_page;
use crate::data::action_logger::{Action, ActionLogger};
use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::{ProviderID, ProvidersManager};
use crate::data::provider_configuration::ProviderConfigurationHelper;
use crate::data::remote_ip::RemoteIP;
#[derive(serde::Deserialize)]
pub struct StartLoginQuery {
#[serde(default)]
redirect: LoginRedirect,
id: ProviderID,
}
/// Start user authentication using a provider
#[allow(clippy::too_many_arguments)]
pub async fn start_login(
remote_ip: RemoteIP,
providers: web::Data<Arc<ProvidersManager>>,
states: web::Data<Addr<ProvidersStatesActor>>,
query: web::Query<StartLoginQuery>,
logger: ActionLogger,
) -> impl Responder {
// Get provider information
let provider = match providers.find_by_id(&query.id) {
None => {
return HttpResponse::NotFound()
.body(build_fatal_error_page("Login provider not found!"));
}
Some(p) => p,
};
// Generate & save state
let state = ProviderLoginState::new(&provider.id, query.redirect.clone());
states
.send(providers_states_actor::RecordState {
ip: remote_ip.0,
state: state.clone(),
})
.await
.unwrap();
logger.log(Action::StartLoginAttemptWithOpenIDProvider {
provider_id: &provider.id,
state: &state.state_id,
});
// Get provider configuration
let config = match ProviderConfigurationHelper::get_configuration(&provider).await {
Ok(c) => c,
Err(e) => {
log::error!("Failed to load provider configuration! {}", e);
return HttpResponse::InternalServerError().body(build_fatal_error_page(
"Failed to load provider configuration!",
));
}
};
log::debug!("Provider configuration: {:?}", config);
HttpResponse::Ok().body(state.state_id)
// Redirect user
}

View File

@@ -8,8 +8,9 @@ use actix_web::dev::Payload;
use actix_web::{web, Error, FromRequest, HttpRequest}; use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::data::client::Client; use crate::data::client::Client;
use crate::data::provider::ProviderID;
use crate::data::remote_ip::RemoteIP; use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::SessionIdentity; use crate::data::session_identity::SessionIdentity;
use crate::data::user::{FactorID, GrantedClients, TwoFactor, User, UserID}; use crate::data::user::{FactorID, GrantedClients, TwoFactor, User, UserID};
@@ -20,22 +21,38 @@ pub enum Action<'a> {
AdminDeleteUser(&'a User), AdminDeleteUser(&'a User),
AdminResetUserPassword(&'a User), AdminResetUserPassword(&'a User),
AdminRemoveUserFactor(&'a User, &'a TwoFactor), AdminRemoveUserFactor(&'a User, &'a TwoFactor),
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients), AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
AdminClear2FAHistory(&'a User), AdminClear2FAHistory(&'a User),
LoginWebauthnAttempt { success: bool, user_id: UserID }, LoginWebauthnAttempt {
success: bool,
user_id: UserID,
},
StartLoginAttemptWithOpenIDProvider {
provider_id: &'a ProviderID,
state: &'a str,
},
Signout, Signout,
UserNeed2FAOnLogin(&'a User), UserNeed2FAOnLogin(&'a User),
UserSuccessfullyAuthenticated(&'a User), UserSuccessfullyAuthenticated(&'a User),
UserNeedNewPasswordOnLogin(&'a User), UserNeedNewPasswordOnLogin(&'a User),
TryLoginWithDisabledAccount(&'a str), TryLoginWithDisabledAccount(&'a str),
TryLocalLoginFromUnauthorizedAccount(&'a str),
FailedLoginWithBadCredentials(&'a str), FailedLoginWithBadCredentials(&'a str),
UserChangedPasswordOnLogin(&'a UserID), UserChangedPasswordOnLogin(&'a UserID),
OTPLoginAttempt { user: &'a User, success: bool }, OTPLoginAttempt {
NewOpenIDSession { client: &'a Client }, user: &'a User,
success: bool,
},
NewOpenIDSession {
client: &'a Client,
},
ChangedHisPassword, ChangedHisPassword,
ClearedHisLoginHistory, ClearedHisLoginHistory,
AddNewFactor(&'a TwoFactor), AddNewFactor(&'a TwoFactor),
Removed2FAFactor { factor_id: &'a FactorID }, Removed2FAFactor {
factor_id: &'a FactorID,
},
} }
impl<'a> Action<'a> { impl<'a> Action<'a> {
@@ -64,6 +81,11 @@ impl<'a> Action<'a> {
Action::AdminClear2FAHistory(user) => { Action::AdminClear2FAHistory(user) => {
format!("cleared 2FA history of {}", user.quick_identity()) format!("cleared 2FA history of {}", user.quick_identity())
} }
Action::AdminSetAuthorizedAuthenticationSources(user, sources) => format!(
"update authorized authentication sources ({:?}) for user ({})",
sources,
user.quick_identity()
),
Action::AdminSetNewGrantedClientsList(user, clients) => format!( Action::AdminSetNewGrantedClientsList(user, clients) => format!(
"set new granted clients list ({:?}) for user ({})", "set new granted clients list ({:?}) for user ({})",
clients, clients,
@@ -73,6 +95,9 @@ impl<'a> Action<'a> {
true => format!("successfully performed webauthn attempt for user {user_id:?}"), true => format!("successfully performed webauthn attempt for user {user_id:?}"),
false => format!("performed FAILED webauthn attempt for user {user_id:?}"), false => format!("performed FAILED webauthn attempt for user {user_id:?}"),
}, },
Action::StartLoginAttemptWithOpenIDProvider { provider_id, state } => format!(
"started new authentication attempt through an OpenID provider (prov={} / state={state})", provider_id.0
),
Action::Signout => "signed out".to_string(), Action::Signout => "signed out".to_string(),
Action::UserNeed2FAOnLogin(user) => { Action::UserNeed2FAOnLogin(user) => {
format!( format!(
@@ -90,6 +115,9 @@ impl<'a> Action<'a> {
Action::TryLoginWithDisabledAccount(login) => { Action::TryLoginWithDisabledAccount(login) => {
format!("successfully authenticated as {login}, but this is a DISABLED ACCOUNT") format!("successfully authenticated as {login}, but this is a DISABLED ACCOUNT")
} }
Action::TryLocalLoginFromUnauthorizedAccount(login) => {
format!("successfully locally authenticated as {login}, but this is a FORBIDDEN for this account!")
}
Action::FailedLoginWithBadCredentials(login) => { Action::FailedLoginWithBadCredentials(login) => {
format!("attempted to authenticate as {login} but with a WRONG PASSWORD") format!("attempted to authenticate as {login} but with a WRONG PASSWORD")
} }

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use clap::Parser; use clap::Parser;
use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, USERS_LIST_FILE}; use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, PROVIDERS_LIST_FILE, USERS_LIST_FILE};
/// Basic OIDC provider /// Basic OIDC provider
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
@@ -72,6 +72,10 @@ impl AppConfig {
self.storage_path().join(CLIENTS_LIST_FILE) self.storage_path().join(CLIENTS_LIST_FILE)
} }
pub fn providers_file(&self) -> PathBuf {
self.storage_path().join(PROVIDERS_LIST_FILE)
}
pub fn full_url(&self, uri: &str) -> String { pub fn full_url(&self, uri: &str) -> String {
if uri.starts_with('/') { if uri.starts_with('/') {
format!("{}{}", self.website_origin, uri) format!("{}{}", self.website_origin, uri)

View File

@@ -11,8 +11,10 @@ use base64::Engine as _;
use crate::utils::err::Res; use crate::utils::err::Res;
use crate::utils::string_utils::rand_str; use crate::utils::string_utils::rand_str;
const JWK_USE_SIGN: &str = "sig";
/// Json Web Key <https://datatracker.ietf.org/doc/html/rfc7517> /// Json Web Key <https://datatracker.ietf.org/doc/html/rfc7517>
#[derive(serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct JsonWebKey { pub struct JsonWebKey {
#[serde(rename = "alg")] #[serde(rename = "alg")]
algorithm: String, algorithm: String,
@@ -24,6 +26,8 @@ pub struct JsonWebKey {
modulus: String, modulus: String,
#[serde(rename = "e")] #[serde(rename = "e")]
public_exponent: String, public_exponent: String,
#[serde(rename = "use", skip_serializing_if = "Option::is_none")]
usage: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -44,6 +48,7 @@ impl JWTSigner {
key_id: self.0.key_id().as_ref().unwrap().to_string(), key_id: self.0.key_id().as_ref().unwrap().to_string(),
public_exponent: BASE64_URL_URL_SAFE.encode(components.e), public_exponent: BASE64_URL_URL_SAFE.encode(components.e),
modulus: BASE64_URL_SAFE_NO_PAD.encode(components.n), modulus: BASE64_URL_SAFE_NO_PAD.encode(components.n),
usage: Some(JWK_USE_SIGN.to_string()),
} }
} }

View File

@@ -11,6 +11,8 @@ pub mod jwt_signer;
pub mod login_redirect; pub mod login_redirect;
pub mod open_id_user_info; pub mod open_id_user_info;
pub mod openid_config; pub mod openid_config;
pub mod provider;
pub mod provider_configuration;
pub mod remote_ip; pub mod remote_ip;
pub mod session_identity; pub mod session_identity;
pub mod totp_key; pub mod totp_key;

89
src/data/provider.rs Normal file
View File

@@ -0,0 +1,89 @@
use crate::data::entity_manager::EntityManager;
use crate::data::login_redirect::LoginRedirect;
use crate::utils::string_utils::apply_env_vars;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
pub struct ProviderID(pub String);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Provider {
/// The ID of the provider
pub id: ProviderID,
/// The human-readable name of the client
pub name: String,
/// A logo presented to the users of the provider
pub logo: String,
/// The registration id of BasicOIDC on the provider
pub client_id: String,
/// The registration secret of BasicOIDC on the provider
pub client_secret: String,
/// Specify the URL of the OpenID configuration URL
///
/// (.well-known/openid-configuration endpoint)
pub configuration_url: String,
}
impl Provider {
/// Get URL-encoded provider id
pub fn id_encoded(&self) -> String {
urlencoding::encode(&self.id.0).to_string()
}
/// Get the URL where the logo can be located
pub fn logo_url(&self) -> &str {
match self.logo.as_str() {
"gitea" => "/assets/img/brands/gitea.svg",
"gitlab" => "/assets/img/brands/gitlab.svg",
"github" => "/assets/img/brands/github.svg",
"microsoft" => "/assets/img/brands/microsoft.svg",
"google" => "/assets/img/brands/google.svg",
s => s,
}
}
/// Get the URL to use to login with the provider
pub fn login_url(&self, redirect_url: &LoginRedirect) -> String {
format!(
"/login_with_prov?id={}&redirect_url={}",
self.id_encoded(),
redirect_url.get_encoded()
)
}
}
impl PartialEq for Provider {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
impl Eq for Provider {}
pub type ProvidersManager = EntityManager<Provider>;
impl EntityManager<Provider> {
pub fn find_by_id(&self, u: &ProviderID) -> Option<Provider> {
for entry in self.iter() {
if entry.id.eq(u) {
return Some(entry.clone());
}
}
None
}
pub fn apply_environment_variables(&mut self) {
for c in self.iter_mut() {
c.id = ProviderID(apply_env_vars(&c.id.0));
c.name = apply_env_vars(&c.name);
c.logo = apply_env_vars(&c.logo);
c.client_id = apply_env_vars(&c.client_id);
c.client_secret = apply_env_vars(&c.client_secret);
c.configuration_url = apply_env_vars(&c.configuration_url);
}
}
}

View File

@@ -0,0 +1,75 @@
use std::cell::RefCell;
use std::collections::HashMap;
use crate::constants::OIDC_PROVIDERS_LIFETIME;
use crate::data::jwt_signer::JsonWebKey;
use crate::data::provider::Provider;
use crate::utils::err::Res;
use crate::utils::time::time;
#[derive(Debug, Clone, serde::Deserialize)]
pub struct ProviderDiscovery {
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: Option<String>,
pub jwks_uri: String,
pub claims_supported: Option<Vec<String>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProviderJWKs {
pub keys: Vec<JsonWebKey>,
}
/// Provider configuration
#[derive(Debug, Clone)]
pub struct ProviderConfiguration {
pub discovery: ProviderDiscovery,
pub keys: ProviderJWKs,
pub expire: u64,
}
thread_local! {
static THREAD_CACHE: RefCell<HashMap<String, ProviderConfiguration>> = RefCell::new(Default::default());
}
pub struct ProviderConfigurationHelper {}
impl ProviderConfigurationHelper {
/// Get or refresh the configuration for a provider
pub async fn get_configuration(provider: &Provider) -> Res<ProviderConfiguration> {
let config = THREAD_CACHE.with(|i| i.borrow().get(&provider.configuration_url).cloned());
// Refresh config cache if needed
if config.is_none() || config.as_ref().unwrap().expire < time() {
let conf = Self::fetch_configuration(provider).await?;
THREAD_CACHE.with(|i| {
i.borrow_mut()
.insert(provider.configuration_url.clone(), conf.clone())
});
return Ok(conf);
}
// We can return immediately previously extracted value
Ok(config.unwrap())
}
/// Get fresh configuration from provider
async fn fetch_configuration(provider: &Provider) -> Res<ProviderConfiguration> {
let discovery: ProviderDiscovery = reqwest::get(&provider.configuration_url)
.await?
.json()
.await?;
let keys: ProviderJWKs = reqwest::get(&discovery.jwks_uri).await?.json().await?;
Ok(ProviderConfiguration {
discovery,
keys,
expire: time() + OIDC_PROVIDERS_LIFETIME,
})
}
}

View File

@@ -1,9 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
use crate::actors::users_actor::AuthorizedAuthenticationSources;
use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN; use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN;
use crate::data::client::{Client, ClientID}; use crate::data::client::{Client, ClientID};
use crate::data::login_redirect::LoginRedirect; use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::{Provider, ProviderID};
use crate::data::totp_key::TotpKey; use crate::data::totp_key::TotpKey;
use crate::data::webauthn_manager::WebauthnPubKey; use crate::data::webauthn_manager::WebauthnPubKey;
use crate::utils::time::{fmt_time, time}; use crate::utils::time::{fmt_time, time};
@@ -114,6 +116,10 @@ impl Successful2FALogin {
} }
} }
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct User { pub struct User {
pub uid: UserID, pub uid: UserID,
@@ -142,6 +148,14 @@ pub struct User {
/// None = all services /// None = all services
/// Some([]) = no service /// Some([]) = no service
pub authorized_clients: Option<Vec<ClientID>>, pub authorized_clients: Option<Vec<ClientID>>,
/// Authorize connection through local login
#[serde(default = "default_true")]
pub allow_local_login: bool,
/// Allowed third party providers
#[serde(default)]
pub allow_login_from_providers: Vec<ProviderID>,
} }
impl User { impl User {
@@ -162,6 +176,19 @@ impl User {
) )
} }
/// Get the list of sources from which a user can authenticate from
pub fn authorized_authentication_sources(&self) -> AuthorizedAuthenticationSources {
AuthorizedAuthenticationSources {
local: self.allow_local_login,
upstream: self.allow_login_from_providers.clone(),
}
}
/// Check if a user can authenticate using a givne provider or not
pub fn can_login_from_provider(&self, provider: &Provider) -> bool {
self.allow_login_from_providers.contains(&provider.id)
}
pub fn granted_clients(&self) -> GrantedClients { pub fn granted_clients(&self) -> GrantedClients {
match self.authorized_clients.as_deref() { match self.authorized_clients.as_deref() {
None => GrantedClients::AllClients, None => GrantedClients::AllClients,
@@ -296,6 +323,8 @@ impl Default for User {
two_factor_exemption_after_successful_login: false, two_factor_exemption_after_successful_login: false,
last_successful_2fa: Default::default(), last_successful_2fa: Default::default(),
authorized_clients: Some(Vec::new()), authorized_clients: Some(Vec::new()),
allow_local_login: true,
allow_login_from_providers: vec![],
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use std::net::IpAddr; use std::net::IpAddr;
use crate::actors::users_actor::UsersSyncBackend; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersSyncBackend};
use crate::data::entity_manager::EntityManager; use crate::data::entity_manager::EntityManager;
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID}; use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
use crate::utils::err::{new_error, Res}; use crate::utils::err::{new_error, Res};
@@ -143,6 +143,18 @@ impl UsersSyncBackend for EntityManager<User> {
self.remove(&user) self.remove(&user)
} }
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res {
self.update_user(id, |mut user| {
user.allow_local_login = sources.local;
user.allow_login_from_providers = sources.upstream;
user
})
}
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res { fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res {
self.update_user(id, |mut user| { self.update_user(id, |mut user| {
user.authorized_clients = clients.to_user(); user.authorized_clients = clients.to_user();

View File

@@ -12,6 +12,7 @@ use actix_web::{get, middleware, web, App, HttpResponse, HttpServer};
use basic_oidc::actors::bruteforce_actor::BruteForceActor; use basic_oidc::actors::bruteforce_actor::BruteForceActor;
use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor; use basic_oidc::actors::openid_sessions_actor::OpenIDSessionsActor;
use basic_oidc::actors::providers_states_actor::ProvidersStatesActor;
use basic_oidc::actors::users_actor::{UsersActor, UsersSyncBackend}; use basic_oidc::actors::users_actor::{UsersActor, UsersSyncBackend};
use basic_oidc::constants::*; use basic_oidc::constants::*;
use basic_oidc::controllers::assets_controller::assets_route; use basic_oidc::controllers::assets_controller::assets_route;
@@ -20,6 +21,7 @@ use basic_oidc::data::app_config::AppConfig;
use basic_oidc::data::client::ClientManager; use basic_oidc::data::client::ClientManager;
use basic_oidc::data::entity_manager::EntityManager; use basic_oidc::data::entity_manager::EntityManager;
use basic_oidc::data::jwt_signer::JWTSigner; use basic_oidc::data::jwt_signer::JWTSigner;
use basic_oidc::data::provider::ProvidersManager;
use basic_oidc::data::user::User; use basic_oidc::data::user::User;
use basic_oidc::data::webauthn_manager::WebAuthManager; use basic_oidc::data::webauthn_manager::WebAuthManager;
use basic_oidc::middlewares::auth_middleware::AuthMiddleware; use basic_oidc::middlewares::auth_middleware::AuthMiddleware;
@@ -68,6 +70,7 @@ async fn main() -> std::io::Result<()> {
let users_actor = UsersActor::new(users).start(); let users_actor = UsersActor::new(users).start();
let bruteforce_actor = BruteForceActor::default().start(); let bruteforce_actor = BruteForceActor::default().start();
let providers_states_actor = ProvidersStatesActor::default().start();
let openid_sessions_actor = OpenIDSessionsActor::default().start(); let openid_sessions_actor = OpenIDSessionsActor::default().start();
let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key"); let jwt_signer = JWTSigner::gen_from_memory().expect("Failed to generate JWKS key");
let webauthn_manager = Arc::new(WebAuthManager::init(config)); let webauthn_manager = Arc::new(WebAuthManager::init(config));
@@ -77,6 +80,11 @@ async fn main() -> std::io::Result<()> {
clients.apply_environment_variables(); clients.apply_environment_variables();
let clients = Arc::new(clients); let clients = Arc::new(clients);
let mut providers = ProvidersManager::open_or_create(config.providers_file())
.expect("Failed to load providers list!");
providers.apply_environment_variables();
let providers = Arc::new(providers);
log::info!("Server will listen on {}", config.listen_address); log::info!("Server will listen on {}", config.listen_address);
let listen_address = config.listen_address.to_string(); let listen_address = config.listen_address.to_string();
@@ -99,8 +107,10 @@ async fn main() -> std::io::Result<()> {
App::new() App::new()
.app_data(web::Data::new(users_actor.clone())) .app_data(web::Data::new(users_actor.clone()))
.app_data(web::Data::new(bruteforce_actor.clone())) .app_data(web::Data::new(bruteforce_actor.clone()))
.app_data(web::Data::new(providers_states_actor.clone()))
.app_data(web::Data::new(openid_sessions_actor.clone())) .app_data(web::Data::new(openid_sessions_actor.clone()))
.app_data(web::Data::new(clients.clone())) .app_data(web::Data::new(clients.clone()))
.app_data(web::Data::new(providers.clone()))
.app_data(web::Data::new(jwt_signer.clone())) .app_data(web::Data::new(jwt_signer.clone()))
.app_data(web::Data::new(webauthn_manager.clone())) .app_data(web::Data::new(webauthn_manager.clone()))
.wrap( .wrap(
@@ -110,7 +120,7 @@ async fn main() -> std::io::Result<()> {
.wrap(AuthMiddleware {}) .wrap(AuthMiddleware {})
.wrap(identity_middleware) .wrap(identity_middleware)
.wrap(session_mw) .wrap(session_mw)
// main route // Main route
.route( .route(
"/", "/",
web::get().to(|| async { web::get().to(|| async {
@@ -120,7 +130,7 @@ async fn main() -> std::io::Result<()> {
}), }),
) )
.route("/robots.txt", web::get().to(assets_controller::robots_txt)) .route("/robots.txt", web::get().to(assets_controller::robots_txt))
// health route // Health route
.service(health) .service(health)
// Assets serving // Assets serving
.route("/assets/{path:.*}", web::get().to(assets_route)) .route("/assets/{path:.*}", web::get().to(assets_route))
@@ -151,6 +161,11 @@ async fn main() -> std::io::Result<()> {
"/login/api/auth_webauthn", "/login/api/auth_webauthn",
web::post().to(login_api::auth_webauthn), web::post().to(login_api::auth_webauthn),
) )
// Providers controller
.route(
"/login_with_prov",
web::get().to(providers_controller::start_login),
)
// Settings routes // Settings routes
.route( .route(
"/settings", "/settings",
@@ -207,6 +222,10 @@ async fn main() -> std::io::Result<()> {
"/admin/clients", "/admin/clients",
web::get().to(admin_controller::clients_route), web::get().to(admin_controller::clients_route),
) )
.route(
"/admin/providers",
web::get().to(admin_controller::providers_route),
)
.route("/admin/users", web::get().to(admin_controller::users_route)) .route("/admin/users", web::get().to(admin_controller::users_route))
.route( .route(
"/admin/users", "/admin/users",

View File

@@ -30,8 +30,6 @@
font-size: 3.5rem; font-size: 3.5rem;
} }
} }
</style> </style>
@@ -45,7 +43,7 @@
<main class="form-signin"> <main class="form-signin">
<h1 class="h3 mb-3 fw-normal">{{ _p.page_title }}</h1> <h1 class="h3 mb-3 fw-normal" style="margin-bottom: 2rem !important;">{{ _p.page_title }}</h1>
{% if let Some(danger) = _p.danger %} {% if let Some(danger) = _p.danger %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">

View File

@@ -1,5 +1,23 @@
{% extends "base_login_page.html" %} {% extends "base_login_page.html" %}
{% block content %} {% block content %}
<style>
#providers {
margin-top: 40px;
}
.provider-button {
width: 100%;
display: flex;
margin-top: 10px;
}
.provider-button img {
margin-right: 1em;
width: 1em;
}
</style>
<form action="/login?redirect={{ _p.redirect_uri.get_encoded() }}" method="post"> <form action="/login?redirect={{ _p.redirect_uri.get_encoded() }}" method="post">
<div> <div>
<div class="form-floating"> <div class="form-floating">
@@ -19,4 +37,18 @@
</form> </form>
<!-- Upstream providers -->
{% if !providers.is_empty() %}
<div id="providers">
{% for prov in providers %}
<a class="btn btn-secondary btn-lg provider-button" href="{{ prov.login_url(_p.redirect_uri) }}">
<img src="{{ prov.logo_url() }}" alt="Provider icon"/>
<div style="text-align: left;">
Login using {{ prov.name }} <br/>
</div>
</a>
{% endfor %}
</div>
{% endif %}
{% endblock content %} {% endblock content %}

View File

@@ -15,7 +15,7 @@
<span class="fs-4">{{ _p.app_name }}</span> <span class="fs-4">{{ _p.app_name }}</span>
</a> </a>
{% if _p.is_admin %} {% if _p.is_admin %}
<span>Version {{ _p.version }}</span> <span>Version {{ _p.version }}</span>
{% endif %} {% endif %}
<hr> <hr>
<ul class="nav nav-pills flex-column mb-auto"> <ul class="nav nav-pills flex-column mb-auto">
@@ -42,6 +42,11 @@
Clients Clients
</a> </a>
</li> </li>
<li>
<a href="/admin/providers" class="nav-link link-dark">
Providers
</a>
</li>
<li> <li>
<a href="/admin/users" class="nav-link link-dark"> <a href="/admin/users" class="nav-link link-dark">
Users Users
@@ -83,6 +88,7 @@
if(el.href === location.href) el.classList.add("active"); if(el.href === location.href) el.classList.add("active");
else el.classList.remove("active") else el.classList.remove("active")
}) })
</script> </script>
{% if _p.ip_location_api.is_some() %} {% if _p.ip_location_api.is_some() %}
<script>const IP_LOCATION_API = "{{ _p.ip_location_api.unwrap() }}"</script> <script>const IP_LOCATION_API = "{{ _p.ip_location_api.unwrap() }}"</script>

View File

@@ -112,28 +112,61 @@
</div> </div>
<ul> <ul>
{% for e in u.get_formatted_2fa_successful_logins() %} {% for e in u.get_formatted_2fa_successful_logins() %}
{% if e.can_bypass_2fa %}<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li> {% if e.can_bypass_2fa %}
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %} <li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% endfor %} {% else %}
<li>{{ e.ip }} - {{ e.fmt_time() }}</li>
{% endif %}
{% endfor %}
</ul> </ul>
</fieldset> </fieldset>
{% endif %} {% endif %}
<!-- Authorized authentication sources -->
<fieldset class="form-group">
<legend class="mt-4">Authorized authentication sources</legend>
<!-- Local login -->
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_local_login" id="allow_local_login"
{% if u.allow_local_login %} checked="" {% endif %}>
<label class="form-check-label" for="allow_local_login">
Allow local login
</label>
</div>
<!-- Upstream providers -->
<input type="hidden" name="authorized_sources" id="authorized_sources"/>
{% for prov in providers %}
<div class="form-check">
<input class="form-check-input authorized_provider" type="checkbox" name="prov-{{ prov.id.0 }}"
id="prov-{{ prov.id.0 }}"
data-id="{{ prov.id.0 }}"
{% if u.can_login_from_provider(prov) %} checked="" {% endif %}>
<label class="form-check-label" for="prov-{{ prov.id.0 }}">
Allow login from {{ prov.name }}
</label>
</div>
{% endfor %}
</fieldset>
<!-- Granted clients --> <!-- Granted clients -->
<fieldset class="form-group"> <fieldset class="form-group">
<legend class="mt-4">Granted clients</legend> <legend class="mt-4">Granted clients</legend>
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="all_clients" {% if u.granted_clients() == GrantedClients::AllClients %} checked="" {% endif %}> value="all_clients" {% if u.granted_clients()== GrantedClients::AllClients %} checked="" {% endif
%}>
Grant all clients Grant all clients
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_)) %} checked="checked" {% endif %}> value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_))
%} checked="checked" {% endif %}>
Manually specify allowed clients Manually specify allowed clients
</label> </label>
</div> </div>
@@ -155,7 +188,8 @@
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="no_client" {% if u.granted_clients() == GrantedClients::NoClient %} checked="checked" {% endif %}> value="no_client" {% if u.granted_clients()== GrantedClients::NoClient %} checked="checked" {%
endif %}>
Do not grant any client Do not grant any client
</label> </label>
</div> </div>
@@ -215,6 +249,13 @@
form.addEventListener("submit", (ev) => { form.addEventListener("submit", (ev) => {
ev.preventDefault(); ev.preventDefault();
const authorized_sources = [...document.querySelectorAll(".authorized_provider")]
.filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",")
document.querySelector("input[name=authorized_sources]").value = authorized_sources;
const authorized_clients = [...document.querySelectorAll(".authorize_client_checkbox")] const authorized_clients = [...document.querySelectorAll(".authorize_client_checkbox")]
.filter(e => e.checked) .filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",") .map(e => e.getAttribute("data-id")).join(",")
@@ -231,6 +272,9 @@
form.submit(); form.submit();
}); });
</script> </script>
{% endblock content %} {% endblock content %}

View File

@@ -0,0 +1,37 @@
{% extends "base_settings_page.html" %}
{% block content %}
<style>
#providers td {
vertical-align: middle;
}
</style>
<table id="providers" class="table table-hover" style="max-width: 800px;" aria-describedby="OpenID providers list">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Configuration URL</th>
<th scope="col">Client ID</th>
</tr>
</thead>
<tbody>
{% for c in providers %}
<tr>
<td>
<img src="{{ c.logo_url() }}" alt="{{ c.name }} logo" width="30px"/>
</td>
<td>{{ c.id.0 }}</td>
<td>{{ c.name }}</td>
<td><a href="{{ c.configuration_url }}" target="_blank" rel="noreferrer">
{{ c.configuration_url }}
</a></td>
<td>{{ c.client_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}