Merge pull request 'Two factor authentication : TOTP' (#5) from twofactors into master
Reviewed-on: #5
This commit is contained in:
		
							
								
								
									
										384
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										384
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -237,6 +237,12 @@ version = "1.0.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" | ||||
|  | ||||
| [[package]] | ||||
| name = "adler32" | ||||
| version = "1.2.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" | ||||
|  | ||||
| [[package]] | ||||
| name = "aead" | ||||
| version = "0.4.3" | ||||
| @@ -393,6 +399,12 @@ version = "0.1.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" | ||||
|  | ||||
| [[package]] | ||||
| name = "base32" | ||||
| version = "0.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" | ||||
|  | ||||
| [[package]] | ||||
| name = "base64" | ||||
| version = "0.13.0" | ||||
| @@ -413,6 +425,7 @@ dependencies = [ | ||||
|  "actix-identity", | ||||
|  "actix-web", | ||||
|  "askama", | ||||
|  "base32", | ||||
|  "base64", | ||||
|  "bcrypt", | ||||
|  "clap", | ||||
| @@ -424,11 +437,13 @@ dependencies = [ | ||||
|  "lazy-regex", | ||||
|  "log", | ||||
|  "mime_guess", | ||||
|  "qrcode-generator", | ||||
|  "rand", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "serde_yaml", | ||||
|  "sha2 0.10.2", | ||||
|  "totp_rfc6238", | ||||
|  "urlencoding", | ||||
|  "uuid", | ||||
| ] | ||||
| @@ -444,6 +459,12 @@ dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "bit_field" | ||||
| version = "0.10.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4" | ||||
|  | ||||
| [[package]] | ||||
| name = "bitflags" | ||||
| version = "1.3.2" | ||||
| @@ -505,6 +526,12 @@ version = "3.9.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" | ||||
|  | ||||
| [[package]] | ||||
| name = "bytemuck" | ||||
| version = "1.9.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" | ||||
|  | ||||
| [[package]] | ||||
| name = "byteorder" | ||||
| version = "1.4.3" | ||||
| @@ -611,6 +638,12 @@ dependencies = [ | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "color_quant" | ||||
| version = "1.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" | ||||
|  | ||||
| [[package]] | ||||
| name = "const-oid" | ||||
| version = "0.6.2" | ||||
| @@ -675,6 +708,31 @@ dependencies = [ | ||||
|  "crossbeam-utils", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "crossbeam-deque" | ||||
| version = "0.8.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "crossbeam-epoch", | ||||
|  "crossbeam-utils", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "crossbeam-epoch" | ||||
| version = "0.9.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" | ||||
| dependencies = [ | ||||
|  "autocfg 1.1.0", | ||||
|  "cfg-if", | ||||
|  "crossbeam-utils", | ||||
|  "lazy_static", | ||||
|  "memoffset", | ||||
|  "scopeguard", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "crossbeam-utils" | ||||
| version = "0.8.8" | ||||
| @@ -743,6 +801,15 @@ dependencies = [ | ||||
|  "cipher 0.3.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "deflate" | ||||
| version = "1.0.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" | ||||
| dependencies = [ | ||||
|  "adler32", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "der" | ||||
| version = "0.4.5" | ||||
| @@ -818,6 +885,12 @@ dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "either" | ||||
| version = "1.6.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" | ||||
|  | ||||
| [[package]] | ||||
| name = "elliptic-curve" | ||||
| version = "0.11.12" | ||||
| @@ -859,6 +932,22 @@ dependencies = [ | ||||
|  "termcolor", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "exr" | ||||
| version = "1.4.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "14cc0e06fb5f67e5d6beadf3a382fec9baca1aa751c6d5368fdeee7e5932c215" | ||||
| dependencies = [ | ||||
|  "bit_field", | ||||
|  "deflate", | ||||
|  "flume", | ||||
|  "half", | ||||
|  "inflate", | ||||
|  "lebe", | ||||
|  "smallvec", | ||||
|  "threadpool", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ff" | ||||
| version = "0.11.0" | ||||
| @@ -884,7 +973,20 @@ dependencies = [ | ||||
|  "cfg-if", | ||||
|  "crc32fast", | ||||
|  "libc", | ||||
|  "miniz_oxide", | ||||
|  "miniz_oxide 0.4.4", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "flume" | ||||
| version = "0.10.12" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" | ||||
| dependencies = [ | ||||
|  "futures-core", | ||||
|  "futures-sink", | ||||
|  "nanorand", | ||||
|  "pin-project", | ||||
|  "spin 0.9.3", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -963,8 +1065,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "js-sys", | ||||
|  "libc", | ||||
|  "wasi 0.10.2+wasi-snapshot-preview1", | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -977,6 +1081,16 @@ dependencies = [ | ||||
|  "polyval", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "gif" | ||||
| version = "0.11.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b" | ||||
| dependencies = [ | ||||
|  "color_quant", | ||||
|  "weezl", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "group" | ||||
| version = "0.11.0" | ||||
| @@ -1007,6 +1121,12 @@ dependencies = [ | ||||
|  "tracing", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "half" | ||||
| version = "1.8.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" | ||||
|  | ||||
| [[package]] | ||||
| name = "hashbrown" | ||||
| version = "0.11.2" | ||||
| @@ -1080,6 +1200,15 @@ dependencies = [ | ||||
|  "digest 0.9.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "html-escape" | ||||
| version = "0.2.11" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c" | ||||
| dependencies = [ | ||||
|  "utf8-width", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "http" | ||||
| version = "0.2.6" | ||||
| @@ -1126,6 +1255,26 @@ dependencies = [ | ||||
|  "unicode-normalization", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "image" | ||||
| version = "0.24.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "db207d030ae38f1eb6f240d5a1c1c88ff422aa005d10f8c6c6fc5e75286ab30e" | ||||
| dependencies = [ | ||||
|  "bytemuck", | ||||
|  "byteorder", | ||||
|  "color_quant", | ||||
|  "exr", | ||||
|  "gif", | ||||
|  "jpeg-decoder", | ||||
|  "num-iter", | ||||
|  "num-rational", | ||||
|  "num-traits", | ||||
|  "png", | ||||
|  "scoped_threadpool", | ||||
|  "tiff", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "include_dir" | ||||
| version = "0.7.2" | ||||
| @@ -1155,6 +1304,15 @@ dependencies = [ | ||||
|  "hashbrown", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "inflate" | ||||
| version = "0.4.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" | ||||
| dependencies = [ | ||||
|  "adler32", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "inout" | ||||
| version = "0.1.2" | ||||
| @@ -1179,6 +1337,24 @@ dependencies = [ | ||||
|  "libc", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "jpeg-decoder" | ||||
| version = "0.2.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "744c24117572563a98a7e9168a5ac1ee4a1ca7f702211258797bbe0ed0346c3c" | ||||
| dependencies = [ | ||||
|  "rayon", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "js-sys" | ||||
| version = "0.3.57" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" | ||||
| dependencies = [ | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "jwt-simple" | ||||
| version = "0.10.9" | ||||
| @@ -1250,9 +1426,15 @@ version = "1.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" | ||||
| dependencies = [ | ||||
|  "spin", | ||||
|  "spin 0.5.2", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "lebe" | ||||
| version = "0.5.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7efd1d698db0759e6ef11a7cd44407407399a910c774dd804c64c032da7826ff" | ||||
|  | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.121" | ||||
| @@ -1319,6 +1501,15 @@ version = "2.4.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" | ||||
|  | ||||
| [[package]] | ||||
| name = "memoffset" | ||||
| version = "0.6.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" | ||||
| dependencies = [ | ||||
|  "autocfg 1.1.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "mime" | ||||
| version = "0.3.16" | ||||
| @@ -1351,6 +1542,15 @@ dependencies = [ | ||||
|  "autocfg 1.1.0", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "miniz_oxide" | ||||
| version = "0.5.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" | ||||
| dependencies = [ | ||||
|  "adler", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "mio" | ||||
| version = "0.8.2" | ||||
| @@ -1374,6 +1574,15 @@ dependencies = [ | ||||
|  "winapi", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "nanorand" | ||||
| version = "0.7.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" | ||||
| dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "nom" | ||||
| version = "7.1.1" | ||||
| @@ -1432,6 +1641,17 @@ dependencies = [ | ||||
|  "num-traits", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "num-rational" | ||||
| version = "0.4.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" | ||||
| dependencies = [ | ||||
|  "autocfg 1.1.0", | ||||
|  "num-integer", | ||||
|  "num-traits", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "num-traits" | ||||
| version = "0.2.14" | ||||
| @@ -1544,6 +1764,26 @@ version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" | ||||
|  | ||||
| [[package]] | ||||
| name = "pin-project" | ||||
| version = "1.0.10" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" | ||||
| dependencies = [ | ||||
|  "pin-project-internal", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "pin-project-internal" | ||||
| version = "1.0.10" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "pin-project-lite" | ||||
| version = "0.2.8" | ||||
| @@ -1591,6 +1831,18 @@ dependencies = [ | ||||
|  "zeroize", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "png" | ||||
| version = "0.17.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" | ||||
| dependencies = [ | ||||
|  "bitflags", | ||||
|  "crc32fast", | ||||
|  "deflate", | ||||
|  "miniz_oxide 0.5.1", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "polyval" | ||||
| version = "0.5.3" | ||||
| @@ -1642,6 +1894,23 @@ dependencies = [ | ||||
|  "unicode-xid", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "qrcode-generator" | ||||
| version = "4.1.4" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3b1c0d1ea2ed9730a4037bf2dbc12b4b5c15679171adca65792657d1bd65ef6f" | ||||
| dependencies = [ | ||||
|  "html-escape", | ||||
|  "image", | ||||
|  "qrcodegen", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "qrcodegen" | ||||
| version = "1.8.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" | ||||
|  | ||||
| [[package]] | ||||
| name = "quote" | ||||
| version = "1.0.17" | ||||
| @@ -1681,6 +1950,30 @@ dependencies = [ | ||||
|  "getrandom", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rayon" | ||||
| version = "1.5.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221" | ||||
| dependencies = [ | ||||
|  "autocfg 1.1.0", | ||||
|  "crossbeam-deque", | ||||
|  "either", | ||||
|  "rayon-core", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rayon-core" | ||||
| version = "1.9.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4" | ||||
| dependencies = [ | ||||
|  "crossbeam-channel", | ||||
|  "crossbeam-deque", | ||||
|  "crossbeam-utils", | ||||
|  "num_cpus", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "redox_syscall" | ||||
| version = "0.2.12" | ||||
| @@ -1718,6 +2011,21 @@ dependencies = [ | ||||
|  "zeroize", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "ring" | ||||
| version = "0.16.20" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" | ||||
| dependencies = [ | ||||
|  "cc", | ||||
|  "libc", | ||||
|  "once_cell", | ||||
|  "spin 0.5.2", | ||||
|  "untrusted", | ||||
|  "web-sys", | ||||
|  "winapi", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rsa" | ||||
| version = "0.5.0" | ||||
| @@ -1753,6 +2061,12 @@ version = "1.0.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" | ||||
|  | ||||
| [[package]] | ||||
| name = "scoped_threadpool" | ||||
| version = "0.1.9" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" | ||||
|  | ||||
| [[package]] | ||||
| name = "scopeguard" | ||||
| version = "1.1.0" | ||||
| @@ -1915,6 +2229,15 @@ version = "0.5.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" | ||||
|  | ||||
| [[package]] | ||||
| name = "spin" | ||||
| version = "0.9.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" | ||||
| dependencies = [ | ||||
|  "lock_api", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "spki" | ||||
| version = "0.4.1" | ||||
| @@ -2004,6 +2327,26 @@ dependencies = [ | ||||
|  "syn", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "threadpool" | ||||
| version = "1.8.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" | ||||
| dependencies = [ | ||||
|  "num_cpus", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "tiff" | ||||
| version = "0.7.2" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7cfada0986f446a770eca461e8c6566cb879682f7d687c8348aa0c857bd52286" | ||||
| dependencies = [ | ||||
|  "flate2", | ||||
|  "jpeg-decoder", | ||||
|  "weezl", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "time" | ||||
| version = "0.3.9" | ||||
| @@ -2092,6 +2435,15 @@ dependencies = [ | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "totp_rfc6238" | ||||
| version = "0.5.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3e7d63d8bc3098dd14e5f1a107979a38e06b3263f1230a3cd717615fab4e615e" | ||||
| dependencies = [ | ||||
|  "ring", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "tracing" | ||||
| version = "0.1.32" | ||||
| @@ -2171,6 +2523,12 @@ dependencies = [ | ||||
|  "subtle", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "untrusted" | ||||
| version = "0.7.1" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" | ||||
|  | ||||
| [[package]] | ||||
| name = "url" | ||||
| version = "2.2.2" | ||||
| @@ -2189,6 +2547,12 @@ version = "2.1.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "68b90931029ab9b034b300b797048cf23723400aa757e8a2bfb9d748102f9821" | ||||
|  | ||||
| [[package]] | ||||
| name = "utf8-width" | ||||
| version = "0.1.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" | ||||
|  | ||||
| [[package]] | ||||
| name = "uuid" | ||||
| version = "0.8.2" | ||||
| @@ -2270,6 +2634,22 @@ version = "0.2.80" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" | ||||
|  | ||||
| [[package]] | ||||
| name = "web-sys" | ||||
| version = "0.3.57" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" | ||||
| dependencies = [ | ||||
|  "js-sys", | ||||
|  "wasm-bindgen", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "weezl" | ||||
| version = "0.1.5" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" | ||||
|  | ||||
| [[package]] | ||||
| name = "winapi" | ||||
| version = "0.3.9" | ||||
|   | ||||
| @@ -27,4 +27,7 @@ base64 = "0.13.0" | ||||
| jwt-simple = "0.10.9" | ||||
| digest = "0.10.3" | ||||
| sha2 = "0.10.2" | ||||
| lazy-regex = "2.3.0" | ||||
| lazy-regex = "2.3.0" | ||||
| totp_rfc6238 = "0.5.0" | ||||
| base32 = "0.4.0" | ||||
| qrcode-generator = "4.1.4" | ||||
| @@ -25,7 +25,7 @@ Features : | ||||
| * [x] Client authentication using secrets  | ||||
| * [x] Bruteforce protection  | ||||
| * [ ] 2 factors authentication | ||||
|   * [ ] TOTP | ||||
|   * [x] TOTP (authenticator app) | ||||
|   * [ ] Using a security key | ||||
| * [ ] Fully responsive webui | ||||
|  | ||||
|   | ||||
| @@ -11,4 +11,5 @@ body { | ||||
|  | ||||
| .page_body { | ||||
|     padding: 3rem; | ||||
|     overflow-y: scroll; | ||||
| } | ||||
							
								
								
									
										12
									
								
								assets/js/clipboard_utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								assets/js/clipboard_utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| function copyToClipboard(str) { | ||||
| 	const input = document.createElement("input"); | ||||
| 	input.value = str; | ||||
|  | ||||
| 	document.body.appendChild(input); | ||||
|  | ||||
| 	input.select(); | ||||
| 	input.setSelectionRange(0, str.length); | ||||
| 	document.execCommand("copy"); | ||||
|  | ||||
| 	input.remove(); | ||||
| } | ||||
| @@ -46,7 +46,7 @@ impl Session { | ||||
|                                                 jwt_signer: &JWTSigner) -> Res { | ||||
|         let access_token = AccessToken { | ||||
|             issuer: app_config.website_origin.to_string(), | ||||
|             subject_identifier: self.user.clone(), | ||||
|             subject_identifier: self.user.clone().0, | ||||
|             issued_at: time(), | ||||
|             exp_time: time() + OPEN_ID_ACCESS_TOKEN_TIMEOUT, | ||||
|             rand_val: rand_str(OPEN_ID_ACCESS_TOKEN_LEN), | ||||
|   | ||||
| @@ -18,7 +18,7 @@ struct FindUserResult { | ||||
| pub async fn find_username(req: web::Form<FindUserNameReq>, users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     let res = users.send(FindUserByUsername(req.0.username)).await.unwrap(); | ||||
|     HttpResponse::Ok().json(FindUserResult { | ||||
|         user_id: res.0.map(|r| r.uid) | ||||
|         user_id: res.0.map(|r| r.uid.0) | ||||
|     }) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -60,6 +60,7 @@ pub struct UpdateUserQuery { | ||||
|     admin: Option<String>, | ||||
|     grant_type: String, | ||||
|     granted_clients: String, | ||||
|     two_factor: String, | ||||
| } | ||||
|  | ||||
| pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>, update_query: Option<web::Form<UpdateUserQuery>>) -> impl Responder { | ||||
| @@ -80,6 +81,10 @@ pub async fn users_route(user: CurrentUser, users: web::Data<Addr<UsersActor>>, | ||||
|         user.enabled = update.0.enabled.is_some(); | ||||
|         user.admin = update.0.admin.is_some(); | ||||
|  | ||||
|         let factors_to_keep = update.0.two_factor.split(';').collect::<Vec<_>>(); | ||||
|         user.two_factor.retain(|f| factors_to_keep.contains(&f.id.0.as_str())); | ||||
|  | ||||
|  | ||||
|         user.authorized_clients = match update.0.grant_type.as_str() { | ||||
|             "all_clients" => None, | ||||
|             "custom_clients" => Some(update.0.granted_clients.split(',') | ||||
|   | ||||
| @@ -7,32 +7,49 @@ use crate::actors::{bruteforce_actor, users_actor}; | ||||
| use crate::actors::bruteforce_actor::BruteForceActor; | ||||
| use crate::actors::users_actor::{ChangePasswordResult, LoginResult, UsersActor}; | ||||
| use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; | ||||
| use crate::controllers::base_controller::{FatalErrorPage, redirect_user}; | ||||
| use crate::controllers::base_controller::{FatalErrorPage, redirect_user, redirect_user_for_login}; | ||||
| use crate::data::login_redirect::LoginRedirect; | ||||
| use crate::data::remote_ip::RemoteIP; | ||||
| use crate::data::session_identity::{SessionIdentity, SessionStatus}; | ||||
| use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; | ||||
|  | ||||
| struct BaseLoginPage { | ||||
|     danger: String, | ||||
|     success: String, | ||||
| struct BaseLoginPage<'a> { | ||||
|     danger: Option<String>, | ||||
|     success: Option<String>, | ||||
|     page_title: &'static str, | ||||
|     app_name: &'static str, | ||||
|     redirect_uri: String, | ||||
|     redirect_uri: &'a LoginRedirect, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "login/login.html")] | ||||
| struct LoginTemplate { | ||||
|     _p: BaseLoginPage, | ||||
| struct LoginTemplate<'a> { | ||||
|     _p: BaseLoginPage<'a>, | ||||
|     login: String, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "login/password_reset.html")] | ||||
| struct PasswordResetTemplate { | ||||
|     _p: BaseLoginPage, | ||||
| struct PasswordResetTemplate<'a> { | ||||
|     _p: BaseLoginPage<'a>, | ||||
|     min_pass_len: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "login/choose_second_factor.html")] | ||||
| struct ChooseSecondFactorTemplate<'a> { | ||||
|     _p: BaseLoginPage<'a>, | ||||
|     factors: &'a [TwoFactor], | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "login/opt_input.html")] | ||||
| struct LoginWithOTPTemplate<'a> { | ||||
|     _p: BaseLoginPage<'a>, | ||||
|     factor: &'a TwoFactor, | ||||
| } | ||||
|  | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct LoginRequestBody { | ||||
|     login: String, | ||||
| @@ -42,7 +59,8 @@ pub struct LoginRequestBody { | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct LoginRequestQuery { | ||||
|     logout: Option<bool>, | ||||
|     redirect: Option<String>, | ||||
|     #[serde(default)] | ||||
|     redirect: LoginRedirect, | ||||
| } | ||||
|  | ||||
| /// Authenticate user | ||||
| @@ -54,11 +72,10 @@ pub async fn login_route( | ||||
|     req: Option<web::Form<LoginRequestBody>>, | ||||
|     id: Identity, | ||||
| ) -> impl Responder { | ||||
|     let mut danger = String::new(); | ||||
|     let mut success = String::new(); | ||||
|     let mut danger = None; | ||||
|     let mut success = None; | ||||
|     let mut login = String::new(); | ||||
|  | ||||
|  | ||||
|     let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }) | ||||
|         .await.unwrap(); | ||||
|  | ||||
| @@ -70,49 +87,29 @@ pub async fn login_route( | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     let redirect_uri = match query.redirect.as_deref() { | ||||
|         None => "/", | ||||
|         Some(s) => match s.starts_with('/') && !s.starts_with("//") { | ||||
|             true => s, | ||||
|             false => "/", | ||||
|         }, | ||||
|     }; | ||||
|  | ||||
|     // Check if user session must be closed | ||||
|     if let Some(true) = query.logout { | ||||
|         id.forget(); | ||||
|         success = "Goodbye!".to_string(); | ||||
|         success = Some("Goodbye!".to_string()); | ||||
|     } | ||||
|  | ||||
|     // Check if user is already authenticated | ||||
|     if SessionIdentity(&id).is_authenticated() { | ||||
|         return redirect_user(redirect_uri); | ||||
|         return redirect_user(query.redirect.get()); | ||||
|     } | ||||
|  | ||||
|     // Check if user is setting a new  password | ||||
|     if let (Some(req), true) = (&req, SessionIdentity(&id).need_new_password()) { | ||||
|         if req.password.len() < MIN_PASS_LEN { | ||||
|             danger = "Password is too short!".to_string(); | ||||
|         } else { | ||||
|             let res: ChangePasswordResult = users | ||||
|                 .send(users_actor::ChangePasswordRequest { | ||||
|                     user_id: SessionIdentity(&id).user_id(), | ||||
|                     new_password: req.password.clone(), | ||||
|                     temporary: false, | ||||
|                 }) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|  | ||||
|             if !res.0 { | ||||
|                 danger = "Failed to change password!".to_string(); | ||||
|             } else { | ||||
|                 SessionIdentity(&id).set_status(SessionStatus::SignedIn); | ||||
|                 return redirect_user(redirect_uri); | ||||
|             } | ||||
|         } | ||||
|     // Check if the password of the user has to be changed | ||||
|     if SessionIdentity(&id).need_new_password() { | ||||
|         return redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())); | ||||
|     } | ||||
|  | ||||
|     // Check if the user has to valide a second factor | ||||
|     if SessionIdentity(&id).need_2fa_auth() { | ||||
|         return redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded())); | ||||
|     } | ||||
|  | ||||
|     // Try to authenticate user | ||||
|     else if let Some(req) = &req { | ||||
|     if let Some(req) = &req { | ||||
|         login = req.login.clone(); | ||||
|         let response: LoginResult = users | ||||
|             .send(users_actor::LoginRequest { | ||||
| @@ -126,45 +123,31 @@ pub async fn login_route( | ||||
|             LoginResult::Success(user) => { | ||||
|                 SessionIdentity(&id).set_user(&user); | ||||
|  | ||||
|                 if user.need_reset_password { | ||||
|                 return if user.need_reset_password { | ||||
|                     SessionIdentity(&id).set_status(SessionStatus::NeedNewPassword); | ||||
|                     redirect_user(&format!("/reset_password?redirect={}", query.redirect.get_encoded())) | ||||
|                 } else if user.has_two_factor() { | ||||
|                     SessionIdentity(&id).set_status(SessionStatus::Need2FA); | ||||
|                     redirect_user(&format!("/2fa_auth?redirect={}", query.redirect.get_encoded())) | ||||
|                 } else { | ||||
|                     return redirect_user(redirect_uri); | ||||
|                 } | ||||
|                     redirect_user(query.redirect.get()) | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             LoginResult::AccountDisabled => { | ||||
|                 log::warn!("Failed login for username {} : account is disabled", login); | ||||
|                 danger = "Your account is disabled!".to_string(); | ||||
|                 danger = Some("Your account is disabled!".to_string()); | ||||
|             } | ||||
|  | ||||
|             c => { | ||||
|                 log::warn!("Failed login for ip {:?} /  username {}: {:?}", remote_ip, login, c); | ||||
|                 danger = "Login failed.".to_string(); | ||||
|                 danger = Some("Login failed.".to_string()); | ||||
|  | ||||
|                 bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Display password reset form if it is appropriate | ||||
|     if SessionIdentity(&id).need_new_password() { | ||||
|         return HttpResponse::Ok().content_type("text/html").body( | ||||
|             PasswordResetTemplate { | ||||
|                 _p: BaseLoginPage { | ||||
|                     page_title: "Password reset", | ||||
|                     danger, | ||||
|                     success, | ||||
|                     app_name: APP_NAME, | ||||
|                     redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||
|                 }, | ||||
|                 min_pass_len: MIN_PASS_LEN, | ||||
|             } | ||||
|                 .render() | ||||
|                 .unwrap(), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     HttpResponse::Ok().content_type("text/html").body( | ||||
|         LoginTemplate { | ||||
|             _p: BaseLoginPage { | ||||
| @@ -172,7 +155,7 @@ pub async fn login_route( | ||||
|                 danger, | ||||
|                 success, | ||||
|                 app_name: APP_NAME, | ||||
|                 redirect_uri: urlencoding::encode(redirect_uri).to_string(), | ||||
|                 redirect_uri: &query.redirect, | ||||
|             }, | ||||
|             login, | ||||
|         } | ||||
| @@ -185,3 +168,156 @@ pub async fn login_route( | ||||
| pub async fn logout_route() -> impl Responder { | ||||
|     redirect_user("/login?logout=true") | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct ChangePasswordRequestBody { | ||||
|     password: String, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct PasswordResetQuery { | ||||
|     #[serde(default)] | ||||
|     redirect: LoginRedirect, | ||||
| } | ||||
|  | ||||
| /// Reset user password route | ||||
| pub async fn reset_password_route(id: Identity, query: web::Query<PasswordResetQuery>, | ||||
|                                   req: Option<web::Form<ChangePasswordRequestBody>>, | ||||
|                                   users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     let mut danger = None; | ||||
|  | ||||
|     if !SessionIdentity(&id).need_new_password() { | ||||
|         return redirect_user_for_login(query.redirect.get()); | ||||
|     } | ||||
|  | ||||
|     // Check if user is setting a new  password | ||||
|     if let Some(req) = &req { | ||||
|         if req.password.len() < MIN_PASS_LEN { | ||||
|             danger = Some("Password is too short!".to_string()); | ||||
|         } else { | ||||
|             let res: ChangePasswordResult = users | ||||
|                 .send(users_actor::ChangePasswordRequest { | ||||
|                     user_id: SessionIdentity(&id).user_id(), | ||||
|                     new_password: req.password.clone(), | ||||
|                     temporary: false, | ||||
|                 }) | ||||
|                 .await | ||||
|                 .unwrap(); | ||||
|  | ||||
|             if !res.0 { | ||||
|                 danger = Some("Failed to change password!".to_string()); | ||||
|             } else { | ||||
|                 SessionIdentity(&id).set_status(SessionStatus::SignedIn); | ||||
|                 return redirect_user(query.redirect.get()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     HttpResponse::Ok().content_type("text/html").body( | ||||
|         PasswordResetTemplate { | ||||
|             _p: BaseLoginPage { | ||||
|                 page_title: "Password reset", | ||||
|                 danger, | ||||
|                 success: None, | ||||
|                 app_name: APP_NAME, | ||||
|                 redirect_uri: &query.redirect, | ||||
|             }, | ||||
|             min_pass_len: MIN_PASS_LEN, | ||||
|         } | ||||
|             .render() | ||||
|             .unwrap(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct ChooseSecondFactorQuery { | ||||
|     #[serde(default)] | ||||
|     redirect: LoginRedirect, | ||||
| } | ||||
|  | ||||
| /// Let the user select the factor to use to authenticate | ||||
| pub async fn choose_2fa_method(id: Identity, query: web::Query<ChooseSecondFactorQuery>, | ||||
|                                users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     if !SessionIdentity(&id).need_2fa_auth() { | ||||
|         return redirect_user_for_login(query.redirect.get()); | ||||
|     } | ||||
|  | ||||
|     let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) | ||||
|         .await.unwrap().0.expect("Could not find user!"); | ||||
|  | ||||
|     HttpResponse::Ok().content_type("text/html").body( | ||||
|         ChooseSecondFactorTemplate { | ||||
|             _p: BaseLoginPage { | ||||
|                 page_title: "Two factor authentication", | ||||
|                 danger: None, | ||||
|                 success: None, | ||||
|                 app_name: APP_NAME, | ||||
|                 redirect_uri: &query.redirect, | ||||
|             }, | ||||
|             factors: &user.two_factor, | ||||
|         } | ||||
|             .render() | ||||
|             .unwrap(), | ||||
|     ) | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct LoginWithOTPQuery { | ||||
|     #[serde(default)] | ||||
|     redirect: LoginRedirect, | ||||
|     id: FactorID, | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct LoginWithOTPForm { | ||||
|     code: String, | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Login with OTP | ||||
| pub async fn login_with_otp(id: Identity, query: web::Query<LoginWithOTPQuery>, | ||||
|                             form: Option<web::Form<LoginWithOTPForm>>, | ||||
|                             users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     let mut danger = None; | ||||
|  | ||||
|     if !SessionIdentity(&id).need_2fa_auth() { | ||||
|         return redirect_user_for_login(query.redirect.get()); | ||||
|     } | ||||
|  | ||||
|     let user: User = users.send(users_actor::GetUserRequest(SessionIdentity(&id).user_id())) | ||||
|         .await.unwrap().0.expect("Could not find user!"); | ||||
|  | ||||
|     let factor = match user.find_factor(&query.id) { | ||||
|         Some(f) => f, | ||||
|         None => return HttpResponse::Ok() | ||||
|             .body(FatalErrorPage { message: "Factor not found!" }.render().unwrap()) | ||||
|     }; | ||||
|  | ||||
|     let key = match &factor.kind { | ||||
|         TwoFactorType::TOTP(key) => key, | ||||
|         _ => { | ||||
|             return HttpResponse::Ok() | ||||
|                 .body(FatalErrorPage { message: "Factor is not a TOTP key!" }.render().unwrap()); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if let Some(form) = form { | ||||
|         if !key.check_code(&form.code).unwrap_or(false) { | ||||
|             danger = Some("Specified code is invalid!".to_string()); | ||||
|         } else { | ||||
|             SessionIdentity(&id).set_status(SessionStatus::SignedIn); | ||||
|             return redirect_user(query.redirect.get()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     HttpResponse::Ok().body(LoginWithOTPTemplate { | ||||
|         _p: BaseLoginPage { | ||||
|             danger, | ||||
|             success: None, | ||||
|             page_title: "Two-Factor Auth", | ||||
|             app_name: APP_NAME, | ||||
|             redirect_uri: &query.redirect, | ||||
|         }, | ||||
|         factor, | ||||
|     }.render().unwrap()) | ||||
| } | ||||
| @@ -4,4 +4,6 @@ pub mod login_controller; | ||||
| pub mod settings_controller; | ||||
| pub mod admin_controller; | ||||
| pub mod admin_api; | ||||
| pub mod openid_controller; | ||||
| pub mod openid_controller; | ||||
| pub mod two_factors_controller; | ||||
| pub mod two_factor_api; | ||||
| @@ -341,7 +341,7 @@ pub async fn token(req: HttpRequest, | ||||
|             // Generate id token | ||||
|             let id_token = IdToken { | ||||
|                 issuer: app_config.website_origin.to_string(), | ||||
|                 subject_identifier: session.user, | ||||
|                 subject_identifier: session.user.0, | ||||
|                 audience: session.client.0.to_string(), | ||||
|                 expiration_time: session.access_token_expire_at, | ||||
|                 issued_at: time(), | ||||
| @@ -499,7 +499,7 @@ async fn user_info(req: HttpRequest, token: Option<String>, | ||||
|     HttpResponse::Ok() | ||||
|         .json(OpenIDUserInfo { | ||||
|             name: user.full_name(), | ||||
|             sub: user.uid, | ||||
|             sub: user.uid.0, | ||||
|             given_name: user.first_name, | ||||
|             family_name: user.last_name, | ||||
|             preferred_username: user.username, | ||||
|   | ||||
| @@ -108,10 +108,9 @@ pub async fn change_password_route(user: CurrentUser, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     HttpResponse::Ok() | ||||
|         .body(ChangePasswordPage { | ||||
|             _p: BaseSettingsPage::get("Change password", &user, danger, success), | ||||
|             min_pwd_len: MIN_PASS_LEN, | ||||
|         }.render().unwrap()) | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										65
									
								
								src/controllers/two_factor_api.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/controllers/two_factor_api.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| use actix::Addr; | ||||
| use actix_web::{HttpResponse, Responder, web}; | ||||
| use uuid::Uuid; | ||||
|  | ||||
| use crate::actors::users_actor; | ||||
| use crate::actors::users_actor::UsersActor; | ||||
| use crate::data::current_user::CurrentUser; | ||||
| use crate::data::totp_key::TotpKey; | ||||
| use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User}; | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct AddTOTPRequest { | ||||
|     factor_name: String, | ||||
|     secret: String, | ||||
|     first_code: String, | ||||
| } | ||||
|  | ||||
| pub async fn save_totp_factor(user: CurrentUser, form: web::Json<AddTOTPRequest>, | ||||
|                               users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     let key = TotpKey::from_encoded_secret(&form.secret); | ||||
|  | ||||
|     if !key.check_code(&form.first_code).unwrap_or(false) { | ||||
|         return HttpResponse::BadRequest() | ||||
|             .body(format!("Given code is invalid (expected {} or {})!", | ||||
|                           key.current_code().unwrap_or_default(), | ||||
|                           key.previous_code().unwrap_or_default())); | ||||
|     } | ||||
|  | ||||
|     if form.factor_name.is_empty() { | ||||
|         return HttpResponse::BadRequest().body("Please give a name to the factor!"); | ||||
|     } | ||||
|  | ||||
|     let mut user = User::from(user); | ||||
|     user.add_factor(TwoFactor { | ||||
|         id: FactorID(Uuid::new_v4().to_string()), | ||||
|         name: form.0.factor_name, | ||||
|         kind: TwoFactorType::TOTP(key), | ||||
|     }); | ||||
|     let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; | ||||
|  | ||||
|     if !res { | ||||
|         HttpResponse::InternalServerError().body("Failed to update user information!") | ||||
|     } else { | ||||
|         HttpResponse::Ok().body("Added new factor!") | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(serde::Deserialize)] | ||||
| pub struct DeleteFactorRequest { | ||||
|     id: FactorID, | ||||
| } | ||||
|  | ||||
| pub async fn delete_factor(user: CurrentUser, form: web::Json<DeleteFactorRequest>, | ||||
|                            users: web::Data<Addr<UsersActor>>) -> impl Responder { | ||||
|     let mut user = User::from(user); | ||||
|     user.remove_factor(form.0.id); | ||||
|  | ||||
|     let res = users.send(users_actor::UpdateUserRequest(user)).await.unwrap().0; | ||||
|  | ||||
|     if !res { | ||||
|         HttpResponse::InternalServerError().body("Failed to update user information!") | ||||
|     } else { | ||||
|         HttpResponse::Ok().body("Removed factor!") | ||||
|     } | ||||
| } | ||||
							
								
								
									
										72
									
								
								src/controllers/two_factors_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/controllers/two_factors_controller.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| use std::ops::Deref; | ||||
|  | ||||
| use actix_web::{HttpResponse, Responder, web}; | ||||
| use askama::Template; | ||||
| use qrcode_generator::QrCodeEcc; | ||||
|  | ||||
| use crate::controllers::settings_controller::BaseSettingsPage; | ||||
| use crate::data::app_config::AppConfig; | ||||
| use crate::data::current_user::CurrentUser; | ||||
| use crate::data::totp_key::TotpKey; | ||||
| use crate::data::user::User; | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "settings/two_factors_page.html")] | ||||
| struct TwoFactorsPage<'a> { | ||||
|     _p: BaseSettingsPage, | ||||
|     user: &'a User, | ||||
| } | ||||
|  | ||||
| #[derive(Template)] | ||||
| #[template(path = "settings/add_2fa_totp_page.html")] | ||||
| struct AddTotpPage { | ||||
|     _p: BaseSettingsPage, | ||||
|     qr_code: String, | ||||
|     account_name: String, | ||||
|     secret_key: String, | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Manage two factors authentication methods route | ||||
| pub async fn two_factors_route(user: CurrentUser) -> impl Responder { | ||||
|     HttpResponse::Ok() | ||||
|         .body(TwoFactorsPage { | ||||
|             _p: BaseSettingsPage::get( | ||||
|                 "Two factors auth", | ||||
|                 &user, | ||||
|                 None, | ||||
|                 None), | ||||
|             user: user.deref(), | ||||
|         }.render().unwrap()) | ||||
| } | ||||
|  | ||||
|  | ||||
| /// Configure a new TOTP authentication factor | ||||
| pub async fn add_totp_factor_route(user: CurrentUser, app_conf: web::Data<AppConfig>) -> impl Responder { | ||||
|     let key = TotpKey::new_random(); | ||||
|  | ||||
|     let qr_code = qrcode_generator::to_png_to_vec( | ||||
|         key.url_for_user(&user, &app_conf), | ||||
|         QrCodeEcc::Low, | ||||
|         1024, | ||||
|     ); | ||||
|     let qr_code = match qr_code { | ||||
|         Ok(q) => q, | ||||
|         Err(e) => { | ||||
|             log::error!("Failed to generate QrCode! {:?}", e); | ||||
|             return HttpResponse::InternalServerError().body("Failed to generate QrCode!"); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     HttpResponse::Ok() | ||||
|         .body(AddTotpPage { | ||||
|             _p: BaseSettingsPage::get( | ||||
|                 "New authenticator app", | ||||
|                 &user, | ||||
|                 None, | ||||
|                 None), | ||||
|             qr_code: base64::encode(qr_code), | ||||
|             account_name: key.account_name(&user, &app_conf), | ||||
|             secret_key: key.get_secret(), | ||||
|         }.render().unwrap()) | ||||
| } | ||||
| @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; | ||||
|  | ||||
| use clap::Parser; | ||||
|  | ||||
| use crate::constants::{CLIENTS_LIST_FILE, USERS_LIST_FILE}; | ||||
| use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, USERS_LIST_FILE}; | ||||
|  | ||||
| /// Basic OIDC provider | ||||
| #[derive(Parser, Debug, Clone)] | ||||
| @@ -53,4 +53,8 @@ impl AppConfig { | ||||
|             format!("{}/{}", self.website_origin, uri) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn domain_name(&self) -> &str { | ||||
|         self.website_origin.split('/').nth(2).unwrap_or(APP_NAME) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ use actix::Addr; | ||||
| use actix_identity::Identity; | ||||
| use actix_web::{Error, FromRequest, HttpRequest, web}; | ||||
| use actix_web::dev::Payload; | ||||
| use actix_web::error::ErrorInternalServerError; | ||||
|  | ||||
| use crate::actors::users_actor; | ||||
| use crate::actors::users_actor::UsersActor; | ||||
| @@ -41,9 +42,14 @@ impl FromRequest for CurrentUser { | ||||
|  | ||||
|  | ||||
|         Box::pin(async move { | ||||
|             let user: User = user_actor.send( | ||||
|             let user = match user_actor.send( | ||||
|                 users_actor::GetUserRequest(user_id) | ||||
|             ).await.unwrap().0.unwrap(); | ||||
|             ).await.unwrap().0 { | ||||
|                 Some(u) => u, | ||||
|                 None => { | ||||
|                     return Err(ErrorInternalServerError("Could not extract user information!")); | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             Ok(CurrentUser(user)) | ||||
|         }) | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/data/login_redirect.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/data/login_redirect.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| #[derive(serde::Serialize, serde::Deserialize, Debug, Eq, PartialEq, Clone)] | ||||
| pub struct LoginRedirect(String); | ||||
|  | ||||
| impl LoginRedirect { | ||||
|     pub fn get(&self) -> &str { | ||||
|         match self.0.starts_with('/') && !self.0.starts_with("//") { | ||||
|             true => self.0.as_str(), | ||||
|             false => "/", | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_encoded(&self) -> String { | ||||
|         urlencoding::encode(self.get()).to_string() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for LoginRedirect { | ||||
|     fn default() -> Self { | ||||
|         Self("/".to_string()) | ||||
|     } | ||||
| } | ||||
| @@ -10,4 +10,6 @@ pub mod jwt_signer; | ||||
| pub mod id_token; | ||||
| pub mod code_challenge; | ||||
| pub mod open_id_user_info; | ||||
| pub mod access_token; | ||||
| pub mod access_token; | ||||
| pub mod totp_key; | ||||
| pub mod login_redirect; | ||||
| @@ -9,7 +9,7 @@ pub enum SessionStatus { | ||||
|     Invalid, | ||||
|     SignedIn, | ||||
|     NeedNewPassword, | ||||
|     NeedMFA, | ||||
|     Need2FA, | ||||
| } | ||||
|  | ||||
| impl Default for SessionStatus { | ||||
| @@ -20,7 +20,7 @@ impl Default for SessionStatus { | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize, Default)] | ||||
| pub struct SessionIdentityData { | ||||
|     pub id: UserID, | ||||
|     pub id: Option<UserID>, | ||||
|     pub is_admin: bool, | ||||
|     pub auth_time: u64, | ||||
|     pub status: SessionStatus, | ||||
| @@ -48,9 +48,7 @@ impl<'a> SessionIdentity<'a> { | ||||
|  | ||||
|         // Check if session is valid | ||||
|         if let Some(sess) = &res { | ||||
|             if sess.id.is_empty() { | ||||
|                 return None; | ||||
|             } | ||||
|             sess.id.as_ref()?; | ||||
|         } | ||||
|  | ||||
|         res | ||||
| @@ -64,7 +62,7 @@ impl<'a> SessionIdentity<'a> { | ||||
|  | ||||
|     pub fn set_user(&self, user: &User) { | ||||
|         self.set_session_data(&SessionIdentityData { | ||||
|             id: user.uid.clone(), | ||||
|             id: Some(user.uid.clone()), | ||||
|             is_admin: user.admin, | ||||
|             auth_time: time(), | ||||
|             status: SessionStatus::SignedIn, | ||||
| @@ -89,12 +87,19 @@ impl<'a> SessionIdentity<'a> { | ||||
|             .unwrap_or(false) | ||||
|     } | ||||
|  | ||||
|     pub fn need_2fa_auth(&self) -> bool { | ||||
|         self.get_session_data() | ||||
|             .map(|s| s.status == SessionStatus::Need2FA) | ||||
|             .unwrap_or(false) | ||||
|     } | ||||
|  | ||||
|     pub fn is_admin(&self) -> bool { | ||||
|         self.get_session_data().unwrap_or_default().is_admin | ||||
|     } | ||||
|  | ||||
|     pub fn user_id(&self) -> UserID { | ||||
|         self.get_session_data().unwrap_or_default().id | ||||
|             .expect("UserID should never be null here!") | ||||
|     } | ||||
|  | ||||
|     pub fn auth_time(&self) -> u64 { | ||||
|   | ||||
							
								
								
									
										97
									
								
								src/data/totp_key.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/data/totp_key.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| use std::io::ErrorKind; | ||||
|  | ||||
| use base32::Alphabet; | ||||
| use rand::Rng; | ||||
| use totp_rfc6238::{HashAlgorithm, TotpGenerator}; | ||||
|  | ||||
| use crate::data::app_config::AppConfig; | ||||
| use crate::data::user::User; | ||||
| use crate::utils::err::Res; | ||||
| use crate::utils::time::time; | ||||
|  | ||||
| const BASE32_ALPHABET: Alphabet = Alphabet::RFC4648 { padding: true }; | ||||
| const NUM_DIGITS: usize = 6; | ||||
| const PERIOD: u64 = 30; | ||||
|  | ||||
| #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] | ||||
| pub struct TotpKey { | ||||
|     encoded: String, | ||||
| } | ||||
|  | ||||
| impl TotpKey { | ||||
|     /// Generate a new TOTP key | ||||
|     pub fn new_random() -> Self { | ||||
|         let random_bytes = rand::thread_rng().gen::<[u8; 10]>(); | ||||
|         Self { | ||||
|             encoded: base32::encode(BASE32_ALPHABET, &random_bytes) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get a key from an encoded secret | ||||
|     pub fn from_encoded_secret(s: &str) -> Self { | ||||
|         Self { encoded: s.to_string() } | ||||
|     } | ||||
|  | ||||
|     /// Get QrCode URL for user | ||||
|     /// | ||||
|     /// Based on https://github.com/google/google-authenticator/wiki/Key-Uri-Format | ||||
|     pub fn url_for_user(&self, u: &User, conf: &AppConfig) -> String { | ||||
|         format!( | ||||
|             "otpauth://totp/{}:{}?secret={}&issuer={}&algorithm=SHA1&digits={}&period={}", | ||||
|             urlencoding::encode(conf.domain_name()), | ||||
|             urlencoding::encode(&u.username), | ||||
|             self.encoded, | ||||
|             urlencoding::encode(conf.domain_name()), | ||||
|             NUM_DIGITS, | ||||
|             PERIOD, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /// Get account name | ||||
|     pub fn account_name(&self, u: &User, conf: &AppConfig) -> String { | ||||
|         format!( | ||||
|             "{}:{}", | ||||
|             urlencoding::encode(conf.domain_name()), | ||||
|             urlencoding::encode(&u.username) | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /// Get current secret in base32 format | ||||
|     pub fn get_secret(&self) -> String { | ||||
|         self.encoded.to_string() | ||||
|     } | ||||
|  | ||||
|     /// Get current code | ||||
|     pub fn current_code(&self) -> Res<String> { | ||||
|         self.get_code_at(time) | ||||
|     } | ||||
|  | ||||
|     /// Get previous code | ||||
|     pub fn previous_code(&self) -> Res<String> { | ||||
|         self.get_code_at(|| time() - PERIOD) | ||||
|     } | ||||
|  | ||||
|     /// Get the code at a specific time | ||||
|     fn get_code_at<F: Fn() -> u64>(&self, get_time: F) -> Res<String> { | ||||
|         let gen = TotpGenerator::new() | ||||
|             .set_digit(NUM_DIGITS).unwrap() | ||||
|             .set_step(PERIOD).unwrap() | ||||
|             .set_hash_algorithm(HashAlgorithm::SHA1) | ||||
|             .build(); | ||||
|  | ||||
|         let key = match base32::decode(BASE32_ALPHABET, &self.encoded) { | ||||
|             None => { | ||||
|                 return Err(Box::new( | ||||
|                     std::io::Error::new(ErrorKind::Other, "Failed to decode base32 secret!"))); | ||||
|             } | ||||
|             Some(k) => k, | ||||
|         }; | ||||
|  | ||||
|         Ok(gen.get_code_with(&key, get_time)) | ||||
|     } | ||||
|  | ||||
|     /// Check a code's validity | ||||
|     pub fn check_code(&self, code: &str) -> Res<bool> { | ||||
|         Ok(self.current_code()?.eq(code) || self.previous_code()?.eq(code)) | ||||
|     } | ||||
| } | ||||
| @@ -1,8 +1,44 @@ | ||||
| use crate::data::client::ClientID; | ||||
| use crate::data::entity_manager::EntityManager; | ||||
| use crate::data::login_redirect::LoginRedirect; | ||||
| use crate::data::totp_key::TotpKey; | ||||
| use crate::utils::err::Res; | ||||
|  | ||||
| pub type UserID = String; | ||||
| #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] | ||||
| pub struct UserID(pub String); | ||||
|  | ||||
| #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] | ||||
| pub struct FactorID(pub String); | ||||
|  | ||||
| #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||
| pub enum TwoFactorType { | ||||
|     TOTP(TotpKey), | ||||
|     _OTHER, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||
| pub struct TwoFactor { | ||||
|     pub id: FactorID, | ||||
|     pub name: String, | ||||
|     pub kind: TwoFactorType, | ||||
| } | ||||
|  | ||||
| impl TwoFactor { | ||||
|     pub fn type_str(&self) -> &'static str { | ||||
|         match self.kind { | ||||
|             TwoFactorType::TOTP(_) => "Authenticator app", | ||||
|             _ => unimplemented!() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String { | ||||
|         match self.kind { | ||||
|             TwoFactorType::TOTP(_) => format!("/2fa_otp?id={}&redirect={}", | ||||
|                                               self.id.0, redirect_uri.get_encoded()), | ||||
|             _ => unimplemented!() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] | ||||
| pub struct User { | ||||
| @@ -16,6 +52,10 @@ pub struct User { | ||||
|     pub enabled: bool, | ||||
|     pub admin: bool, | ||||
|  | ||||
|     /// 2FA | ||||
|     #[serde(default)] | ||||
|     pub two_factor: Vec<TwoFactor>, | ||||
|  | ||||
|     /// None = all services | ||||
|     /// Some([]) = no service | ||||
|     pub authorized_clients: Option<Vec<ClientID>>, | ||||
| @@ -36,6 +76,22 @@ impl User { | ||||
|     pub fn verify_password<P: AsRef<[u8]>>(&self, pass: P) -> bool { | ||||
|         verify_password(pass, &self.password) | ||||
|     } | ||||
|  | ||||
|     pub fn has_two_factor(&self) -> bool { | ||||
|         !self.two_factor.is_empty() | ||||
|     } | ||||
|  | ||||
|     pub fn add_factor(&mut self, factor: TwoFactor) { | ||||
|         self.two_factor.push(factor); | ||||
|     } | ||||
|  | ||||
|     pub fn remove_factor(&mut self, factor_id: FactorID) { | ||||
|         self.two_factor.retain(|f| f.id != factor_id); | ||||
|     } | ||||
|  | ||||
|     pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> { | ||||
|         self.two_factor.iter().find(|f| f.id.eq(factor_id)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PartialEq for User { | ||||
| @@ -49,7 +105,7 @@ impl Eq for User {} | ||||
| impl Default for User { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             uid: uuid::Uuid::new_v4().to_string(), | ||||
|             uid: UserID(uuid::Uuid::new_v4().to_string()), | ||||
|             first_name: "".to_string(), | ||||
|             last_name: "".to_string(), | ||||
|             username: "".to_string(), | ||||
| @@ -58,6 +114,7 @@ impl Default for User { | ||||
|             need_reset_password: false, | ||||
|             enabled: true, | ||||
|             admin: false, | ||||
|             two_factor: vec![], | ||||
|             authorized_clients: Some(Vec::new()), | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										18
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -12,7 +12,6 @@ use basic_oidc::actors::users_actor::UsersActor; | ||||
| use basic_oidc::constants::*; | ||||
| use basic_oidc::controllers::*; | ||||
| use basic_oidc::controllers::assets_controller::assets_route; | ||||
| use basic_oidc::controllers::login_controller::{login_route, logout_route}; | ||||
| use basic_oidc::data::app_config::AppConfig; | ||||
| use basic_oidc::data::client::ClientManager; | ||||
| use basic_oidc::data::entity_manager::EntityManager; | ||||
| @@ -108,16 +107,27 @@ async fn main() -> std::io::Result<()> { | ||||
|             .route("/assets/{path:.*}", web::get().to(assets_route)) | ||||
|  | ||||
|             // Login page | ||||
|             .route("/login", web::get().to(login_route)) | ||||
|             .route("/login", web::post().to(login_route)) | ||||
|             .route("/login", web::get().to(login_controller::login_route)) | ||||
|             .route("/login", web::post().to(login_controller::login_route)) | ||||
|             .route("/reset_password", web::get().to(login_controller::reset_password_route)) | ||||
|             .route("/reset_password", web::post().to(login_controller::reset_password_route)) | ||||
|             .route("/2fa_auth", web::get().to(login_controller::choose_2fa_method)) | ||||
|             .route("/2fa_otp", web::get().to(login_controller::login_with_otp)) | ||||
|             .route("/2fa_otp", web::post().to(login_controller::login_with_otp)) | ||||
|  | ||||
|             // Logout page | ||||
|             .route("/logout", web::get().to(logout_route)) | ||||
|             .route("/logout", web::get().to(login_controller::logout_route)) | ||||
|  | ||||
|             // Settings routes | ||||
|             .route("/settings", web::get().to(settings_controller::account_settings_details_route)) | ||||
|             .route("/settings/change_password", web::get().to(settings_controller::change_password_route)) | ||||
|             .route("/settings/change_password", web::post().to(settings_controller::change_password_route)) | ||||
|             .route("/settings/two_factors", web::get().to(two_factors_controller::two_factors_route)) | ||||
|             .route("/settings/two_factors/add_totp", web::get().to(two_factors_controller::add_totp_factor_route)) | ||||
|  | ||||
|             // User API | ||||
|             .route("/settings/api/two_factor/save_totp_factor", web::post().to(two_factor_api::save_totp_factor)) | ||||
|             .route("/settings/api/two_factor/delete_factor", web::post().to(two_factor_api::delete_factor)) | ||||
|  | ||||
|             // Admin routes | ||||
|             .route("/admin", web::get() | ||||
|   | ||||
| @@ -43,13 +43,17 @@ | ||||
|  | ||||
|     <h1 class="h3 mb-3 fw-normal">{{ _p.page_title }}</h1> | ||||
|  | ||||
|     {% if let Some(danger) = _p.danger %} | ||||
|     <div class="alert alert-danger" role="alert"> | ||||
|         {{ _p.danger }} | ||||
|         {{ danger }} | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% if let Some(success) = _p.success %} | ||||
|     <div class="alert alert-success" role="alert"> | ||||
|         {{ _p.success }} | ||||
|         {{ success }} | ||||
|     </div> | ||||
|     {% endif %} | ||||
|  | ||||
|     {% block content %} | ||||
|     TO_REPLACE | ||||
|   | ||||
							
								
								
									
										23
									
								
								templates/login/choose_second_factor.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								templates/login/choose_second_factor.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
|  | ||||
| <div> | ||||
|     <p>You need to validate a second factor to validate your login.</p> | ||||
|  | ||||
|     {% for factor in factors %} | ||||
|     <p> | ||||
|         <a class="btn btn-primary btn-lg" href="{{ factor.login_url(_p.redirect_uri) }}" style="width: 100%;"> | ||||
|             {{ factor.name }} <br/> | ||||
|             <small>{{ factor.type_str() }}</small> | ||||
|         </a> | ||||
|     </p> | ||||
|     {% endfor %} | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <div style="margin-top: 10px;"> | ||||
|     <a href="/logout">Sign out</a> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| {% endblock content %} | ||||
| @@ -1,10 +1,10 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
| <form action="/login?redirect={{ _p.redirect_uri }}" method="post"> | ||||
| <form action="/login?redirect={{ _p.redirect_uri.get_encoded() }}" method="post"> | ||||
|     <div> | ||||
|         <div class="form-floating"> | ||||
|             <input name="login" type="text" required class="form-control" id="floatingName" placeholder="unsername" | ||||
|                    value="{{ login }}"> | ||||
|                    value="{{ login }}" autofocus> | ||||
|             <label for="floatingName">Email address or username</label> | ||||
|         </div> | ||||
|  | ||||
|   | ||||
							
								
								
									
										24
									
								
								templates/login/opt_input.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								templates/login/opt_input.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
|  | ||||
| <div> | ||||
|     <p>Please go to your authenticator app <i>{{ factor.name }}</i>, generate a new code and enter it here:</p> | ||||
|     <form method="post" action="{{ factor.login_url(_p.redirect_uri) }}"> | ||||
|  | ||||
|         <div class="form-group"> | ||||
|             <label for="code" class="form-label mt-4">Generated code</label> | ||||
|             <input type="text" name="code" minlength="6" maxlength="6" class="form-control" id="code" | ||||
|                    placeholder="XXXXXX" autofocus> | ||||
|         </div> | ||||
|  | ||||
|         <button class="w-100 btn btn-primary" type="submit" style="margin: 20px 0px;">Login</button> | ||||
|     </form> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <div style="margin-top: 10px;"> | ||||
|     <a href="/2fa_auth?redirect={{ _p.redirect_uri.get_encoded() }}">Sign in using another factor</a><br /> | ||||
|     <a href="/logout">Sign out</a> | ||||
| </div> | ||||
|  | ||||
| {% endblock content %} | ||||
| @@ -1,17 +1,14 @@ | ||||
| {% extends "base_login_page.html" %} | ||||
| {% block content %} | ||||
| <form action="/login?redirect={{ _p.redirect_uri }}" method="post" id="reset_password_form"> | ||||
| <form action="/reset_password?redirect={{ _p.redirect_uri.get_encoded() }}" method="post" id="reset_password_form"> | ||||
|     <div> | ||||
|         <p>You need to configure a new password:</p> | ||||
|  | ||||
|         <p style="color:red" id="err_target"></p> | ||||
|  | ||||
|         <!-- Needed for controller --> | ||||
|         <input type="hidden" name="login" value="."/> | ||||
|  | ||||
|         <div class="form-floating"> | ||||
|             <input name="password" type="password" required class="form-control" id="pass1" | ||||
|                    placeholder="Password"/> | ||||
|                    placeholder="Password" autofocus/> | ||||
|             <label for="pass1">New password</label> | ||||
|         </div> | ||||
|  | ||||
| @@ -46,8 +43,6 @@ | ||||
|         else | ||||
|             form.submit(); | ||||
|     }) | ||||
|  | ||||
|  | ||||
| </script> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     <tbody> | ||||
|     <tr> | ||||
|         <th scope="row">User ID</th> | ||||
|         <td>{{ u.uid }}</td> | ||||
|         <td>{{ u.uid.0 }}</td> | ||||
|     </tr> | ||||
|     <tr> | ||||
|         <th scope="row">First name</th> | ||||
|   | ||||
							
								
								
									
										103
									
								
								templates/settings/add_2fa_totp_page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								templates/settings/add_2fa_totp_page.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| {% extends "base_settings_page.html" %} | ||||
| {% block content %} | ||||
| <div style="max-width: 700px;"> | ||||
|  | ||||
|     <p>On this page you can configure a new Authenticator app. Please use an authenticator app to scan the QR code.</p> | ||||
|  | ||||
|     <p>Note: if you have not an authenticator app yet, you might want to use | ||||
|         <a href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp" rel="noopener" target="_blank">FreeOTP | ||||
|             Authenticator</a> for example.</p> | ||||
|  | ||||
|     <img src="data:image/png;base64,{{ qr_code }}" style="width: 150px; margin: 20px 0px;"/> | ||||
|  | ||||
|     <p>If you can't scan the QrCode, please use the following parameters instead:</p> | ||||
|     <ul> | ||||
|         <li><strong>Account name:</strong> <i>{{ account_name }}</i> <a | ||||
|                 href="javascript:copyToClipboard('{{ account_name }}')">Copy to clipboard</a></li> | ||||
|         <li><strong>Secret key:</strong> <i>{{ secret_key }}</i> <a | ||||
|                 href="javascript:copyToClipboard('{{ secret_key }}')">Copy | ||||
|             to clipboard</a></li> | ||||
|     </ul> | ||||
|  | ||||
|     <p>Once you have scanned the QrCode, please generate a code and type it below:</p> | ||||
|  | ||||
|     <form id="validateForm" method="post"> | ||||
|         <input type="hidden" name="secret" id="secretInput" value="{{ secret_key }}"/> | ||||
|  | ||||
|         <div class="form-group"> | ||||
|             <label for="inputDevName" class="form-label mt-4">Device name</label> | ||||
|             <input type="text" class="form-control" id="inputDevName" | ||||
|                    placeholder="Device / Authenticator app name" | ||||
|                    value="Authenticator app" minlength="1" required/> | ||||
|             <small class="form-text text-muted">Please give a name to your device to identity it more easily | ||||
|                 later.</small> | ||||
|             <div class="invalid-feedback">Please give a name to this authenticator app</div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="form-group"> | ||||
|             <label for="inputFirstCode" class="form-label mt-4">First code</label> | ||||
|             <input type="text" class="form-control" id="inputFirstCode" | ||||
|                    placeholder="XXXXXX" | ||||
|                    maxlength="6" minlength="6" required/> | ||||
|             <small class="form-text text-muted">Check that your authenticator app is working correctly by typing a first | ||||
|                 code.</small> | ||||
|             <div class="invalid-feedback">Please enter a first code (must have 6 digits)</div> | ||||
|         </div> | ||||
|  | ||||
|         <input type="submit" value="Register app" class="btn btn-primary"> | ||||
|     </form> | ||||
|  | ||||
|     <script src="/assets/js/clipboard_utils.js"></script> | ||||
|  | ||||
|     <script> | ||||
|         const form = document.getElementById("validateForm"); | ||||
|         form.addEventListener("submit", async (e) => { | ||||
|             e.preventDefault(); | ||||
|             const secret = document.getElementById("secretInput").value; | ||||
|             const factorNameInput = document.getElementById("inputDevName"); | ||||
|             const firstCodeInput = document.getElementById("inputFirstCode"); | ||||
|  | ||||
|             let fail = false; | ||||
|             factorNameInput.classList.remove("is-invalid"); | ||||
|             if (factorNameInput.value.length === 0) { | ||||
|                 fail = true; | ||||
|                 factorNameInput.classList.add("is-invalid"); | ||||
|             } | ||||
|  | ||||
|             firstCodeInput.classList.remove("is-invalid"); | ||||
|             if (firstCodeInput.value.length != 6) { | ||||
|                 fail = true; | ||||
|                 firstCodeInput.classList.add("is-invalid"); | ||||
|             } | ||||
|  | ||||
|  | ||||
|             if (fail) | ||||
|                 return; | ||||
|  | ||||
|             try { | ||||
|                 const res = await fetch("/settings/api/two_factor/save_totp_factor", { | ||||
|                     method: "post", | ||||
|                     headers: { | ||||
|                       'Content-Type': 'application/json', | ||||
|                     }, | ||||
|                     body: JSON.stringify({ | ||||
|                         factor_name: factorNameInput.value, | ||||
|                         secret: secret, | ||||
|                         first_code: firstCodeInput.value, | ||||
|                     }) | ||||
|                 }); | ||||
|  | ||||
|                 let text = await res.text(); | ||||
|                 alert(text); | ||||
|  | ||||
|                 if (res.status == 200) | ||||
|                     location.href = "/settings/two_factors"; | ||||
|             } catch(e) { | ||||
|                 console.error(e); | ||||
|                 alert("Failed to register authenticator app!"); | ||||
|             } | ||||
|         }); | ||||
|     </script> | ||||
| </div> | ||||
|  | ||||
| {% endblock content %} | ||||
| @@ -26,6 +26,11 @@ | ||||
|                 Change password | ||||
|             </a> | ||||
|         </li> | ||||
|         <li> | ||||
|             <a href="/settings/two_factors" class="nav-link link-dark"> | ||||
|                 Two-factor authentication | ||||
|             </a> | ||||
|         </li> | ||||
|  | ||||
|         {% if _p.is_admin %} | ||||
|         <hr/> | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     <div class="form-group"> | ||||
|         <label class="form-label mt-4" for="userID">User ID</label> | ||||
|         <input class="form-control" id="userID" type="text" readonly="" | ||||
|                name="uid" value="{{ u.uid }}"/> | ||||
|                name="uid" value="{{ u.uid.0 }}"/> | ||||
|     </div> | ||||
|  | ||||
|     <!-- User name --> | ||||
| @@ -68,6 +68,25 @@ | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Two-Factor authentication --> | ||||
|     <input type="hidden" name="two_factor" value=""/> | ||||
|     {% if u.has_two_factor() %} | ||||
|     <fieldset class="form-group"> | ||||
|         <legend class="mt-4">Two factor authentication</legend> | ||||
|         <strong>If you uncheck a factor, it will be DELETED</strong> | ||||
|         {% for f in u.two_factor %} | ||||
|         <div class="form-check"> | ||||
|             <label class="form-check-label"> | ||||
|                 <input type="checkbox" class="form-check-input two-fact-checkbox" | ||||
|                        value="{{ f.id.0 }}" | ||||
|                        checked=""/> | ||||
|                 {{ f.name }} ({{ f.type_str() }}) | ||||
|             </label> | ||||
|         </div> | ||||
|         {% endfor %} | ||||
|     </fieldset> | ||||
|     {% endif %} | ||||
|  | ||||
|     <!-- Granted clients --> | ||||
|     <fieldset class="form-group"> | ||||
|         <legend class="mt-4">Granted clients</legend> | ||||
| @@ -126,7 +145,7 @@ | ||||
|                     return; | ||||
|  | ||||
|                 const userID = await find_username(usernameEl.value); | ||||
|                 usernameEl.classList.add((userID === null || userID === "{{ u.uid }}") ? "is-valid" : "is-invalid"); | ||||
|                 usernameEl.classList.add((userID === null || userID === "{{ u.uid.0 }}") ? "is-valid" : "is-invalid"); | ||||
|  | ||||
|             } catch(e) { | ||||
|                 console.error(e); | ||||
| @@ -161,11 +180,15 @@ | ||||
|  | ||||
|         document.querySelector("input[name=granted_clients]").value = authorized_clients; | ||||
|  | ||||
|         const factors_to_keep = [...document.querySelectorAll(".two-fact-checkbox")] | ||||
|             .filter(e => e.checked) | ||||
|             .map(e => e.value) | ||||
|             .join(";") | ||||
|  | ||||
|         document.querySelector("input[name=two_factor]").value = factors_to_keep; | ||||
|  | ||||
|         form.submit(); | ||||
|     }); | ||||
|  | ||||
|  | ||||
|  | ||||
| </script> | ||||
|  | ||||
| {% endblock content %} | ||||
							
								
								
									
										64
									
								
								templates/settings/two_factors_page.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								templates/settings/two_factors_page.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| {% extends "base_settings_page.html" %} | ||||
| {% block content %} | ||||
|  | ||||
|  | ||||
| <div class="alert alert-dismissible alert-warning"> | ||||
|     <h4 class="alert-heading">Warning!</h4> | ||||
|     <p class="mb-0">Once a new factor has been added to your account, you can not access | ||||
|         your account anymore using only your password. If you remove all your second factors, | ||||
|         2 Factor Authentication is automatically disabled for your account.</p> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <p> | ||||
|     <a href="/settings/two_factors/add_totp" type="button" class="btn btn-primary">Add Authenticator App</a> | ||||
| </p> | ||||
|  | ||||
| <table class="table table-hover" style="max-width: 800px;" aria-describedby="Factors list"> | ||||
|     <thead> | ||||
|     <tr> | ||||
|         <th scope="col">Factor type</th> | ||||
|         <th scope="col">Name</th> | ||||
|         <th scope="col">Actions</th> | ||||
|     </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     {% for f in user.two_factor %} | ||||
|     <tr id="factor-{{ f.id.0 }}"> | ||||
|         <td>{{ f.type_str() }}</td> | ||||
|         <td>{{ f.name }}</td> | ||||
|         <td><a href="javascript:delete_factor('{{ f.id.0 }}');">Delete</a></td> | ||||
|     </tr> | ||||
|     {% endfor %} | ||||
|     </tbody> | ||||
| </table> | ||||
|  | ||||
| <script> | ||||
|     async function delete_factor(id) { | ||||
|         if (!confirm("Do you really want to remove this factor?")) | ||||
|             return; | ||||
|  | ||||
|         try { | ||||
|             const res = await fetch("/settings/api/two_factor/delete_factor", { | ||||
|                 method: "post", | ||||
|                 headers: { | ||||
|                   'Content-Type': 'application/json', | ||||
|                 }, | ||||
|                 body: JSON.stringify({ | ||||
|                     id: id, | ||||
|                 }) | ||||
|             }); | ||||
|  | ||||
|             let text = await res.text(); | ||||
|             alert(text); | ||||
|  | ||||
|             if (res.status == 200) | ||||
|                 document.getElementById("factor-" + id).remove(); | ||||
|         } catch(e) { | ||||
|             console.error(e); | ||||
|             alert("Failed to remove factor!"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| </script> | ||||
| {% endblock content %} | ||||
| @@ -19,7 +19,7 @@ | ||||
|     </thead> | ||||
|     <tbody> | ||||
|     {% for u in users %} | ||||
|     <tr id="row-user-{{ u.uid }}"> | ||||
|     <tr id="row-user-{{ u.uid.0 }}"> | ||||
|         <td>{{ u.username }}</td> | ||||
|         <td>{{ u.first_name }}</td> | ||||
|         <td>{{ u.last_name }}</td> | ||||
| @@ -27,8 +27,8 @@ | ||||
|         <td>{% if u.admin %}Admin{% else %}Regular user{% endif %}</td> | ||||
|         <td>{% if u.enabled %}Enabled{% else %}Disabled{% endif %}</td> | ||||
|         <td> | ||||
|             <a href="/admin/edit_user?id={{ u.uid }}">Edit</a> | ||||
|             <a href="javascript:delete_user('{{ u.uid }}', '{{ u.username }}')">Delete</a> | ||||
|             <a href="/admin/edit_user?id={{ u.uid.0 }}">Edit</a> | ||||
|             <a href="javascript:delete_user('{{ u.uid.0 }}', '{{ u.username }}')">Delete</a> | ||||
|         </td> | ||||
|     </tr> | ||||
|     {% endfor %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user