Merge pull request 'Two factor authentication : TOTP' (#5) from twofactors into master
Reviewed-on: #5
This commit is contained in:
commit
d7344feb9b
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"
|
||||
|
@ -28,3 +28,6 @@ jwt-simple = "0.10.9"
|
||||
digest = "0.10.3"
|
||||
sha2 = "0.10.2"
|
||||
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();
|
||||
// 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()));
|
||||
}
|
||||
|
||||
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 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())
|
||||
}
|
@ -5,3 +5,5 @@ pub mod settings_controller;
|
||||
pub mod admin_controller;
|
||||
pub mod admin_api;
|
||||
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,7 +108,6 @@ pub async fn change_password_route(user: CurrentUser,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
HttpResponse::Ok()
|
||||
.body(ChangePasswordPage {
|
||||
_p: BaseSettingsPage::get("Change password", &user, danger, success),
|
||||
|
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())
|
||||
}
|
||||
}
|
@ -11,3 +11,5 @@ pub mod id_token;
|
||||
pub mod code_challenge;
|
||||
pub mod open_id_user_info;
|
||||
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 %}
|
||||
|
Loading…
Reference in New Issue
Block a user