diff --git a/CHANGELOG.md b/CHANGELOG.md index be829ea..50a517b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [13.0.0] - 2021-04-08 + +### Added +- Two Factor Auth + +### Updated +- Updated README + ## [12.0.2] - 2021-04-08 ### Updated diff --git a/Cargo.lock b/Cargo.lock index 4a1b0d5..6e71f8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,26 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +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.3.2" @@ -344,12 +365,32 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide 0.4.4", + "object", + "rustc-demangle", +] + [[package]] name = "base-x" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" +[[package]] +name = "base32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" + [[package]] name = "base64" version = "0.12.3" @@ -480,11 +521,12 @@ dependencies = [ [[package]] name = "broker" -version = "12.0.2" +version = "13.0.0" dependencies = [ "anyhow", "async-std", "base64 0.13.0", + "chbs", "driftwood", "futures", "go-flag", @@ -503,6 +545,7 @@ dependencies = [ "tide", "tide-acme", "tide-rustls", + "totp-rs", "uuid", "zxcvbn", ] @@ -523,6 +566,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "bytemuck" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed57e2090563b83ba8f83366628ce535a7584c9afa4c9fc0612a03925c6df58" + [[package]] name = "byteorder" version = "1.4.3" @@ -565,6 +614,23 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chbs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3553a1f278b0090017b37d0f2e4878ee1de65439626bd51030ef2bd97ebdf7f7" +dependencies = [ + "derive_builder", + "failure", + "rand 0.8.3", +] + +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "chrono" version = "0.4.19" @@ -616,6 +682,12 @@ dependencies = [ "vec_map", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colored" version = "2.0.0" @@ -686,6 +758,49 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crc32fast" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca26ee1f8d361640700bde38b2c37d8c22b3ce2d360e1fc1c74ea4b0aa7d775" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94af6efb46fef72616855b036a624cf27ba656ffc9be1b9a3c931cfc7749a9a9" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2584f639eb95fea8c798496315b297cf81b9b58b6d30ab066a75455333cf4b12" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-queue" version = "0.3.1" @@ -803,6 +918,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" +[[package]] +name = "deflate" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +dependencies = [ + "adler32", + "byteorder", +] + [[package]] name = "der-oid-macro" version = "0.4.0" @@ -905,6 +1030,28 @@ version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "fancy-regex" version = "0.4.1" @@ -1120,6 +1267,22 @@ dependencies = [ "polyval", ] +[[package]] +name = "gif" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a668f699973d0f573d15749b7002a9ac9e1f9c6b220e7b165601334c173d8de" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "glob" version = "0.3.0" @@ -1254,6 +1417,25 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "infer" version = "0.2.3" @@ -1293,6 +1475,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.50" @@ -1415,6 +1606,34 @@ version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +[[package]] +name = "memoffset" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83fb6581e8ed1f85fd45c116db8405483899489e38406156c25eb743554361d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + [[package]] name = "nb-connect" version = "1.1.0" @@ -1494,6 +1713,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -1513,6 +1754,12 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" + [[package]] name = "oid-registry" version = "0.1.1" @@ -1601,6 +1848,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "png" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +dependencies = [ + "bitflags", + "crc32fast", + "deflate", + "miniz_oxide 0.3.7", +] + [[package]] name = "polling" version = "2.0.3" @@ -1652,6 +1911,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1760,6 +2029,31 @@ dependencies = [ "rand_core 0.6.2", ] +[[package]] +name = "rayon" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b0d8e0819fadc20c74ea8373106ead0600e3a67ef1fe8da56e39b9ae7275674" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab346ac5921dc62ffa9f89b7a773907511cdfa5490c572ae9be1be33e8afa4a" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "lazy_static", + "num_cpus", +] + [[package]] name = "rcgen" version = "0.8.9" @@ -1853,6 +2147,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1926,6 +2226,18 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.0" @@ -2006,6 +2318,19 @@ dependencies = [ "serde", ] +[[package]] +name = "sha-1" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfebf75d25bd900fd1e7d11501efab59bc846dbc76196839663e6637bba9f25f" +dependencies = [ + "block-buffer", + "cfg-if 1.0.0", + "cpuid-bool 0.1.2", + "digest", + "opaque-debug", +] + [[package]] name = "sha1" version = "0.6.0" @@ -2206,6 +2531,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "synstructure" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "tap" version = "1.0.1" @@ -2299,6 +2636,17 @@ dependencies = [ "tide", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.1.44" @@ -2363,6 +2711,22 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "totp-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71aa19b1e6a6bdf8c212498da1a8e54c50ec7c22da9862dea576910616d1137e" +dependencies = [ + "base32", + "base64 0.13.0", + "byteorder", + "hmac 0.8.1", + "image", + "qrcode", + "sha-1", + "sha2", +] + [[package]] name = "typenum" version = "1.13.0" @@ -2581,6 +2945,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a32b378380f4e9869b22f0b5177c68a5519f03b3454fde0b291455ddbae266c" + [[package]] name = "wepoll-sys" version = "3.0.1" diff --git a/Cargo.toml b/Cargo.toml index cd83b48..92e9f9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broker" -version = "12.0.2" +version = "13.0.0" authors = ["Bevan Hunt "] edition = "2018" license = "MIT" @@ -12,7 +12,7 @@ readme = "README.md" [dependencies] serde_json = "1" -tide = "0.16.0" +tide = "0.16" async-std = { version = "1.9", features = ["attributes"] } serde = { version = "1", features = ["derive"] } serde_derive = "1" @@ -34,3 +34,5 @@ tide-acme = "0.1.0" base64 = "0.13" mailchecker = "4" zxcvbn = "2" +totp-rs = { version = "0.6", features = ["qr"] } +chbs = "0.1" diff --git a/README.md b/README.md index edb1cb8..7cc3be0 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,14 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD * Issues JWTs for authentication (username) and authorization (scopes) for external services * Verify endpoint for external services using Broker user system like [portal](https://crates.io/crates/portal) * Secure password storage with Argon2 encoding -* Uses Global NTP servers and doesn't rely on your local server time for JWT expiry timing +* Uses Global NTP servers and doesn't rely on your local server time for JWT expiry timing and Two Factor timing * Sync latest events on SSE client connection * Auto-provision and renews SSL cert via LetsEncrypt or use your own SSL cert * User Management API endpoints (create, revoke, unrevoke, list, get, update) * User Email Address Validation (regex and blacklist check against throwaway emails) using [mailchecker](https://crates.io/crates/mailchecker) * Password Strength Checker using [zxcvbn](https://crates.io/crates/zxcvbn) +* Two Factor Authenication with QR code generation for Google Authenticator, Authy, etc. +* Secure user password resets with a TOTP with a configurable time duration ### How it works @@ -73,6 +75,7 @@ POST /create_user "admin_token": "letmein", "tenant_name": "tenant_1", "email": "bob@hotmail.com", + "two_factor": true, "scopes": ["news:get", "news:post"], "data": { "name": "Robert Wieland", @@ -81,7 +84,7 @@ POST /create_user } ``` - `admin_token` is required and can be set in the command args - it is for not allowing everyone to add a user - the default is `letmein` -- `email`, `scopes`, and `data` are optional fields +- `email`, `scopes`, `two_factor`, and `data` are optional fields will return `200` or `500` or `400` @@ -94,9 +97,11 @@ POST /login ```json { "username": "bob", - "password": "password1" + "password": "password1", + "totp": "123456", } ``` +- `totp` is an optional field for two factor authentication will return ```json @@ -195,6 +200,7 @@ will return: `200` or `500` or `400` or `401` { "id": "69123c04-fa42-4193-a6c5-ab2fc27658b1", "password": "***", + "totp": "***", "revoked": false, "tenant_name": "tenant_1", "username": "bob", @@ -207,7 +213,7 @@ will return: `200` or `500` or `400` or `401` } ] ``` -- note: `email`, `scopes`, and `data` can be `null` +- note: `email`, `scopes`, `two_factor`, and `data` can be `null` #### Optional - get user @@ -229,6 +235,7 @@ will return: `200` or `500` or `400` or `401` { "id": "69123c04-fa42-4193-a6c5-ab2fc27658b1", "password": "***", + "totp": "***", "revoked": false, "tenant_name": "tenant_1", "username": "bob", @@ -240,7 +247,7 @@ will return: `200` or `500` or `400` or `401` } } ``` -- note: `email`, `scopes`, and `data` can be `null` +- note: `email`, `scopes`, `two_factor`, and `data` can be `null` #### Optional - update user @@ -275,6 +282,69 @@ GET or HEAD / will return: `200` +#### Optional - generate two factor QR Code + +```html +POST /create_qr +``` +- public endpoint +```json +{ + "issuer": "Broker", + "admin_token": "letmein", + "username": "bob" +} +``` +- note: put the name of your application in the issuer field +- note: the ID of the QR will be the user's username and your issuer field + +will return: `200` or `500` or `400` or `401` + +200 - will return the qr code in PNG format in base64 +```json +{ + "qr": "dGhpc2lzYXN0cmluZw==" +} +``` + +#### Optional - create totp + +```html +POST /create_totp +``` +- public endpoint +```json +{ + "admin_token": "letmein", + "username": "bob", +} +``` +will return: `200` or `500` or `400` or `401` + +200 - will return the totp +```json +{ + "totp": "622346" +} +``` +- note: these TOTPs can only be used with the password reset endpoint + +#### Optional - user password reset + +```html +POST /password_reset +``` +- public endpoint +```json +{ + "totp": "622346", + "username": "bob", + "password": "password1" +} +``` + +will return: `200` or `500` or `400` or `401` + ### Install ``` cargo install broker ``` @@ -292,6 +362,7 @@ will return: `200` - the `domain` flag is the domain name (e.g. api.broker.com) of the domain you want to register with LetsEncrypt - must be fully resolvable - the `admin_token` flag is the password for the admin to add users - default `letmein` - the `password_checker` flag enables zxcvbn password checking - default `false` +- the `totp_duration` flag is the duration of the TOTP for user generated password reset - default 300 seconds (5 min) - production example: `./broker --secure="true" --admin_token"23ce4234@123$" --jwt_secret="xTJEX234$##$" --domain="api.broker.com" --password_checker="true"` ### Service diff --git a/src/main.rs b/src/main.rs index adb0396..f389063 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, iter::Iterator}; +use std::{collections::HashMap, convert::TryInto, iter::Iterator}; use serde_derive::{Deserialize, Serialize}; use serde_json::json; use uuid::Uuid; @@ -16,6 +16,8 @@ use futures::StreamExt; use tide_acme::{AcmeConfig, TideRustlsExt}; use mailchecker::is_valid; use zxcvbn::zxcvbn; +use chbs::{config::BasicConfig, prelude::*}; +use totp_rs::{Algorithm, TOTP}; lazy_static! { static ref DB : Arc = { @@ -47,6 +49,7 @@ pub struct EnvVarConfig { pub key_path: String, pub cert_path: String, pub password_checker: bool, + pub totp_duration: u64, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -59,6 +62,8 @@ pub struct User { pub tenant_name: String, pub data: Option, pub scopes: Option>, + pub totp: String, + pub two_factor: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -70,6 +75,7 @@ pub struct UserForm { pub email: Option, pub data: Option, pub scopes: Option>, + pub two_factor: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -81,12 +87,32 @@ pub struct UpdateUserForm { pub password: Option, pub data: Option, pub scopes: Option>, + pub two_factor: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AdminTokenForm { pub admin_token: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateQRForm { + pub issuer: String, + pub admin_token: String, + pub username: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CreateTOTPForm { + pub admin_token: String, + pub username: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PasswordResetForm { + pub totp: String, + pub password: String, + pub username: String, +} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RevokeUserForm { @@ -98,6 +124,7 @@ pub struct RevokeUserForm { pub struct LoginForm { pub username: String, pub password: String, + pub totp: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -174,6 +201,7 @@ fn modify_user(update_user_form: UpdateUserForm) -> Result> { user.data = update_user_form.data; user.scopes = update_user_form.scopes; + user.two_factor = update_user_form.two_factor; match update_user_form.password { Some(password) => { @@ -323,6 +351,12 @@ fn user_create(user_form: UserForm) -> Result> { } } + let mut config = BasicConfig::default(); + config.words = 12; + config.separator = "-".into(); + let scheme = config.to_scheme(); + let totp = scheme.generate(); + let uuid = Uuid::new_v4(); let config = Argon2Config::default(); let uuid_string = Uuid::new_v4().to_string(); @@ -338,6 +372,8 @@ fn user_create(user_form: UserForm) -> Result> { email: user_form.clone().email, data: user_form.clone().data, scopes: user_form.clone().scopes, + totp, + two_factor: user_form.clone().two_factor, }; puts_user(new_user).unwrap(); @@ -356,20 +392,74 @@ async fn create_jwt(login: LoginForm) -> Result> { if !user.revoked { let verified = argon2::verify_encoded(&user.password, login.password.as_bytes())?; if verified { - let app = env_var_config(); - let iat = nippy::get_unix_ntp_time().await?; - let exp = iat + app.jwt_expiry; - let iss = "Broker".to_string(); - let aud: String; - match user.scopes.clone() { - Some(scopes) => { - aud = scopes.join(","); + match user.two_factor { + Some(two_factor) => { + if two_factor { + match login.totp { + Some(token) => { + let totp = TOTP::new( + Algorithm::SHA512, + 6, + 1, + 30, + user.clone().totp, + ); + let time = nippy::get_unix_ntp_time().await?; + if totp.check(&token, time.try_into()?) { + let app = env_var_config(); + let iat = nippy::get_unix_ntp_time().await?; + let exp = iat + app.jwt_expiry; + let iss = "Broker".to_string(); + let aud: String; + match user.scopes.clone() { + Some(scopes) => { + aud = scopes.join(","); + }, + None => { aud = "".to_string() } + } + let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud}; + let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?; + Ok(Some(token)) + } else { + Ok(None) + } + }, + None => { Ok(None) } + } + } else { + let app = env_var_config(); + let iat = nippy::get_unix_ntp_time().await?; + let exp = iat + app.jwt_expiry; + let iss = "Broker".to_string(); + let aud: String; + match user.scopes.clone() { + Some(scopes) => { + aud = scopes.join(","); + }, + None => { aud = "".to_string() } + } + let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud}; + let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?; + Ok(Some(token)) + } }, - None => { aud = "".to_string() } + None => { + let app = env_var_config(); + let iat = nippy::get_unix_ntp_time().await?; + let exp = iat + app.jwt_expiry; + let iss = "Broker".to_string(); + let aud: String; + match user.scopes.clone() { + Some(scopes) => { + aud = scopes.join(","); + }, + None => { aud = "".to_string() } + } + let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud}; + let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?; + Ok(Some(token)) + } } - let my_claims = Claims{sub: user.clone().username, exp, iat, iss, aud}; - let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref()))?; - Ok(Some(token)) } else { Ok(None) } @@ -396,6 +486,7 @@ fn env_var_config() -> EnvVarConfig { let mut key_path = "certs/private_key.pem".to_string(); let mut cert_path = "certs/chain.pem".to_string(); let mut password_checker = false; + let mut totp_duration: u64 = 300; let _ : Vec = go_flag::parse(|flags| { flags.add_flag("port", &mut port); flags.add_flag("origin", &mut origin); @@ -410,9 +501,10 @@ fn env_var_config() -> EnvVarConfig { flags.add_flag("key_path", &mut key_path); flags.add_flag("cert_path", &mut cert_path); flags.add_flag("password_checker", &mut password_checker); + flags.add_flag("totp_duration", &mut totp_duration); }); - EnvVarConfig{port, origin, jwt_expiry, jwt_secret, secure, domain, certs, db, admin_token, auto_cert, key_path, cert_path, password_checker} + EnvVarConfig{port, origin, jwt_expiry, jwt_secret, secure, domain, certs, db, admin_token, auto_cert, key_path, cert_path, password_checker, totp_duration} } async fn jwt_verify(token: String) -> Result>> { @@ -575,6 +667,7 @@ async fn get_user(mut req: Request<()>) -> tide::Result { let users: Vec<_> = users.iter().map(|user| { let mut u = user.to_owned(); u.password = "***".to_string(); + u.totp = "***".to_string(); u }).collect(); Ok(tide::Response::builder(200).body(json!(users)).header("content-type", "application/json").build()) @@ -592,6 +685,7 @@ async fn list_users(mut req: Request<()>) -> tide::Result { let users: Vec<_> = users.iter().map(|user| { let mut u = user.to_owned(); u.password = "***".to_string(); + u.totp = "***".to_string(); u }).collect(); Ok(tide::Response::builder(200).body(json!(users)).header("content-type", "application/json").build()) @@ -647,6 +741,110 @@ async fn health(_: Request<()>) -> tide::Result { Ok(tide::Response::builder(200).header("content-type", "application/json").build()) } +async fn create_qr(mut req: Request<()>) -> tide::Result { + let r = req.body_string().await?; + let create_qr_form : CreateQRForm = serde_json::from_str(&r)?; + + let configure = env_var_config(); + + if create_qr_form.admin_token == configure.admin_token { + match get_user_by_username(create_qr_form.username)? { + Some(user) => { + let totp = TOTP::new( + Algorithm::SHA512, + 6, + 1, + 30, + user.totp, + ); + let code = totp.get_qr(&user.username, &create_qr_form.issuer).unwrap(); + let j = json!({"qr": code}); + + Ok(tide::Response::builder(200).body(j).header("content-type", "application/json").build()) + }, + None => { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } + } + } else { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } +} + +async fn create_totp(mut req: Request<()>) -> tide::Result { + let r = req.body_string().await?; + let create_totp_form : CreateTOTPForm = serde_json::from_str(&r)?; + + let configure = env_var_config(); + + if create_totp_form.admin_token == configure.admin_token { + match get_user_by_username(create_totp_form.username)? { + Some(user) => { + let totp = TOTP::new( + Algorithm::SHA512, + 6, + 1, + configure.totp_duration, + user.totp, + ); + + let time = nippy::get_unix_ntp_time().await?; + let token = totp.generate(time.try_into()?); + let j = json!({"totp": token}); + + Ok(tide::Response::builder(200).body(j).header("content-type", "application/json").build()) + }, + None => { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } + } + } else { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } +} + +async fn password_reset(mut req: Request<()>) -> tide::Result { + let r = req.body_string().await?; + let password_reset_form : PasswordResetForm = serde_json::from_str(&r)?; + + let configure = env_var_config(); + + match get_user_by_username(password_reset_form.username)? { + Some(user) => { + let totp = TOTP::new( + Algorithm::SHA512, + 6, + 1, + configure.totp_duration, + user.totp, + ); + + let time = nippy::get_unix_ntp_time().await?; + let check = totp.check(&password_reset_form.totp, time.try_into()?); + + if check { + let update_user_form = UpdateUserForm{ + username: user.username, + password: Some(password_reset_form.password), + tenant_name: Some(user.tenant_name), + admin_token: configure.admin_token, + email: user.email, + data: user.data, + scopes: user.scopes, + two_factor: user.two_factor, + }; + modify_user(update_user_form)?; + Ok(tide::Response::builder(200).header("content-type", "application/json").build()) + } else { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } + }, + None => { + Ok(tide::Response::builder(401).header("content-type", "application/json").build()) + } + } +} + #[async_std::main] async fn main() -> tide::Result<()> { @@ -672,6 +870,9 @@ async fn main() -> tide::Result<()> { app.at("/get_user").post(get_user); app.at("/unrevoke_user").post(unrevoke_user); app.at("/update_user").post(update_user); + app.at("/create_qr").post(create_qr); + app.at("/create_totp").post(create_totp); + app.at("/password_reset").post(password_reset); app.at("/sse").get(tide::sse::endpoint(|req: Request<()>, sender| async move {