Compare commits

...

3 Commits

Author SHA1 Message Date
Bevan Hunt 5228b58043 update to 14.1.0 2021-04-13 15:29:11 -07:00
Bevan Hunt 65ed39fa44 update to 14.0.1 2021-04-13 14:07:59 -07:00
Bevan Hunt cb86940dca update to 14.0.0 2021-04-12 17:26:29 -07:00
5 changed files with 200 additions and 13 deletions

View File

@ -4,6 +4,27 @@ 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).
## [14.1.0] - 2021-04-13
### Added
- Added expiry to verify endpoint
### Updated
- Updated README
## [14.0.1] - 2021-04-13
### Changed
- Small internal refactor
## [14.0.0] - 2021-04-12
### Added
- Added biscuit for scoping
### Updated
- Updated README
## [13.0.2] - 2021-04-10
### Updated

90
Cargo.lock generated
View File

@ -437,6 +437,28 @@ dependencies = [
"which",
]
[[package]]
name = "biscuit-auth"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfb5aacf9a2bdf1c780a2f06c7c72cbc3cad2907e3fec1777006657ffcb19786"
dependencies = [
"bytes",
"chrono",
"curve25519-dalek",
"hex",
"hmac 0.10.1",
"nom 6.1.2",
"prost",
"prost-types",
"rand 0.7.3",
"rand_core 0.5.1",
"regex",
"sha2",
"thiserror",
"zeroize",
]
[[package]]
name = "bit-set"
version = "0.5.2"
@ -521,11 +543,12 @@ dependencies = [
[[package]]
name = "broker"
version = "13.0.2"
version = "14.1.0"
dependencies = [
"anyhow",
"async-std",
"base64 0.13.0",
"biscuit-auth",
"chbs",
"driftwood",
"futures",
@ -536,6 +559,7 @@ dependencies = [
"lazy_static",
"mailchecker",
"nippy",
"regex",
"rmp-serde",
"rocksdb",
"rust-argon2",
@ -578,6 +602,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]]
name = "cache-padded"
version = "1.1.1"
@ -861,6 +891,19 @@ dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f627126b946c25a4638eec0ea634fc52506dea98db118aae985118ce7c3d723f"
dependencies = [
"byteorder",
"digest",
"rand_core 0.5.1",
"subtle",
"zeroize",
]
[[package]]
name = "custom_derive"
version = "0.1.7"
@ -1320,6 +1363,12 @@ dependencies = [
"libc",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.10.0"
@ -1911,6 +1960,39 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "prost"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e6984d2f1a23009bd270b8bb56d0926810a3d483f59c987d77969e9d8e840b2"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-derive"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "169a15f3008ecb5160cba7d37bcd690a7601b6d30cfb87a117d45e59d52af5d4"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b518d7cdd93dab1d1122cf07fa9a60771836c668dde9d9e2a139f957f0d9f1bb"
dependencies = [
"bytes",
"prost",
]
[[package]]
name = "qrcode"
version = "0.12.0"
@ -3033,6 +3115,12 @@ dependencies = [
"chrono",
]
[[package]]
name = "zeroize"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a974bcdd357f0dca4d41677db03436324d45a4c9ed2d0b873a5a360ce41c36"
[[package]]
name = "zxcvbn"
version = "2.1.1"

View File

@ -1,6 +1,6 @@
[package]
name = "broker"
version = "13.0.2"
version = "14.1.0"
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
edition = "2018"
license = "MIT"
@ -36,3 +36,5 @@ mailchecker = "4"
zxcvbn = "2"
totp-rs = { version = "0.6", features = ["qr"] }
chbs = "0.1"
biscuit-auth = "1"
regex = "1"

View File

@ -28,7 +28,8 @@ Broker is a competitor to [Firebase](https://firebase.google.com/), [Parse Serve
* Supports SSL - full end-to-end encryption
* Provides user authentication with JWTs or HTTP Basic
* 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)
* Uses [biscuit](https://crates.io/crates/biscuit-auth) for user authorization scoping
* Verify endpoint for external services 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 and Two Factor timing
* Sync latest events on SSE client connection
@ -78,9 +79,11 @@ 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`, `two_factor`, and `data` are optional fields
- `scopes` are [biscuit](https://crates.io/crates/biscuit-auth) authority scopes/facts so the first part before the colon is the resource while the second part after the colon is the operation. Don't add any additional colons in the scopes.
will return `200` or `500` or `400`
#### For JWT Auth: Step 2 - login with the user
```html
@ -104,11 +107,9 @@ will return: `200` or `500` or `400` or `401`
"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc2NzQ5MTUsImlhdCI6MTYxNzU4ODUxNSwiaXNzIjoiRGlzcGF0Y2hlciIsInN1YiI6ImZvbyJ9.OwiaZJcFUC_B0CA0ffRZVTWKRf5_vQ7vt5USNJEeKRE"
}
```
- note: `iat` is the issue time, `exp` is the expiry time, `sub` is the username, `iss` is `Broker`, while `aud` is the user scopes joined with a comma like in this example `news:get,news:post`
- note: if you need to debug your JWT then visit [jwt.io](https://jwt.io)
#### Step 3 - connect to SSE
```html
@ -146,6 +147,15 @@ GET /verify
will return: `200` or `500` or `401`
200 - will return a biscuit public key, biscuit token, and JWT expiry for your microservice (use from_bytes to hydrate the key and token)
```json
{
"key": [136,133,229,196,134,20,240,80,159,158,154,20,57,35,198,7,156,160,193,224,174,209,51,150,27,86,75,122,172,24,114,66],
"token": [122,133,229,196,134,20,240,80,159,158,154,20,57,35,198,7,156,160,193,224,174,209,51,150,27,86,75,122,172,24,114,121],
"expiry: 1618352841
}
```
#### Optional - revoke user
```html
@ -348,7 +358,7 @@ will return: `200` or `500` or `400` or `401`
- the `origin` can be passed in as a flag - default `*`
- the `port` can be passed in as a flag - default `8080` - can only be set for unsecure connections
- the `jwt_expiry` for jwts can be passed in as a flag - default `86400`
- the `jwt_expiry` for jwts can be passed in as a flag in seconds - default `86400`
- the `jwt_secret` for jwts should be passed in as a flag - default `secret`
- the `secure` flag for https and can be true or false - default `false`
- the `auto_cert` flag for an autorenewing LetsEncrypt SSL cert can be true or false - requires a resolvable domain - default `true`

View File

@ -18,6 +18,9 @@ use mailchecker::is_valid;
use zxcvbn::zxcvbn;
use chbs::{config::BasicConfig, prelude::*};
use totp_rs::{Algorithm, TOTP};
extern crate biscuit_auth as biscuit;
use biscuit::{crypto::KeyPair, token::Biscuit};
use regex::Regex;
lazy_static! {
static ref DB : Arc<rocksdb::DB> = {
@ -308,6 +311,29 @@ fn puts_event(event: Event) -> Result<()> {
Ok(())
}
fn jwt_aud(scopes: Vec<String>, exp: i64) -> Result<Option<String>> {
let biscuit_root = KeyPair::new();
let biscuit_public_key = biscuit_root.public();
let public_key_bytes = biscuit_public_key.to_bytes();
let mut builder = Biscuit::builder(&biscuit_root);
for scope in scopes {
let mut parts = scope.split(":");
let first = parts.next().unwrap_or_else(|| "INTERNAL_ERROR");
let second = parts.next().unwrap_or_else(|| "INTERNAL_ERROR");
if first == "INTERNAL_ERROR" || second == "INTERNAL_ERROR" {
return Ok(None);
}
let f = format!("right(#authority, \"{}\", #{})", first, second);
let t = f.as_ref();
builder.add_authority_fact(t)?;
}
let biscuit = builder.build()?;
Ok(Some(json!({"key": public_key_bytes, "token": biscuit.to_vec()?, "expiry": exp}).to_string()))
}
fn user_create(user_form: UserForm) -> Result<Option<String>> {
let configure = env_var_config();
@ -357,6 +383,24 @@ fn user_create(user_form: UserForm) -> Result<Option<String>> {
let scheme = config.to_scheme();
let totp = scheme.generate();
let mut scope_valid = true;
match user_form.clone().scopes {
Some(scopes) => {
let re = Regex::new(r"^[^:]+:[^:]+$").unwrap();
for scope in scopes {
if !re.is_match(&scope) {
scope_valid = false;
}
}
},
None => {}
}
if !scope_valid {
let j = json!({"error": "scopes are invalid"}).to_string();
return Ok(Some(j));
}
let uuid = Uuid::new_v4();
let config = Argon2Config::default();
let uuid_string = Uuid::new_v4().to_string();
@ -413,7 +457,12 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
match jwt_aud(scopes, exp)? {
Some(a) => {
aud = a;
},
None => { aud = "".to_string() }
}
},
None => { aud = "".to_string() }
}
@ -434,12 +483,17 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
match jwt_aud(scopes, exp)? {
Some(a) => {
aud = a;
},
None => { aud = "".to_string() }
}
},
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()))?;
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(app.jwt_secret.as_ref())).unwrap();
Ok(Some(token))
}
},
@ -451,7 +505,12 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
match jwt_aud(scopes, exp)? {
Some(a) => {
aud = a;
},
None => { aud = "".to_string() }
}
},
None => { aud = "".to_string() }
}
@ -540,7 +599,12 @@ async fn jwt_verify(token: String) -> Result<Option<TokenData<Claims>>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
match jwt_aud(scopes, exp)? {
Some(a) => {
aud = a;
},
None => { aud = "".to_string() }
}
},
None => { aud = "".to_string() }
}
@ -642,9 +706,11 @@ async fn verify_user(req: Request<()>) -> tide::Result {
let jwt_value = jwt_verify(token).await?;
match jwt_value {
Some(jwt) => {
match get_user_by_username(jwt.claims.sub)? {
let username = jwt.claims.sub;
match get_user_by_username(username.clone())? {
Some(_) => {
Ok(tide::Response::builder(200).header("content-type", "application/json").build())
let aud = jwt.claims.aud;
Ok(tide::Response::builder(200).body(aud.clone()).header("content-type", "application/json").build())
},
None => {
Ok(tide::Response::builder(401).header("content-type", "application/json").build())