update to 14.0.0

This commit is contained in:
Bevan Hunt 2021-04-12 17:26:29 -07:00
parent b413e1e859
commit cb86940dca
5 changed files with 235 additions and 13 deletions

View File

@ -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).
## [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.0.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.0.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,14 @@ GET /verify
will return: `200` or `500` or `401`
200 - will return a biscuit public key and biscuit token for your microservice to perform authorization on the user scopes/facts both as byte arrays use the from_bytes method to rehydrate
```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]
}
```
#### Optional - revoke user
```html

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> = {
@ -357,6 +360,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 +434,30 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
let biscuit_root = KeyPair::new();
let biscuit_public_key = biscuit_root.public();
let public_key_bytes = biscuit_public_key.to_bytes();
let token_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()?;
biscuit.to_vec()?
};
aud = json!({"key": public_key_bytes, "token": token_bytes}).to_string();
},
None => { aud = "".to_string() }
}
@ -434,12 +478,35 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
let biscuit_root = KeyPair::new();
let biscuit_public_key = biscuit_root.public();
let public_key_bytes = biscuit_public_key.to_bytes();
let token_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()?;
biscuit.to_vec()?
};
aud = json!({"key": public_key_bytes, "token": token_bytes}).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 +518,30 @@ async fn create_jwt(login: LoginForm) -> Result<Option<String>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
let biscuit_root = KeyPair::new();
let biscuit_public_key = biscuit_root.public();
let public_key_bytes = biscuit_public_key.to_bytes();
let token_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()?;
biscuit.to_vec()?
};
aud = json!({"key": public_key_bytes, "token": token_bytes}).to_string();
},
None => { aud = "".to_string() }
}
@ -540,7 +630,30 @@ async fn jwt_verify(token: String) -> Result<Option<TokenData<Claims>>> {
let aud: String;
match user.scopes.clone() {
Some(scopes) => {
aud = scopes.join(",");
let biscuit_root = KeyPair::new();
let biscuit_public_key = biscuit_root.public();
let public_key_bytes = biscuit_public_key.to_bytes();
let token_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()?;
biscuit.to_vec()?
};
aud = json!({"key": public_key_bytes, "token": token_bytes}).to_string();
},
None => { aud = "".to_string() }
}
@ -598,7 +711,7 @@ async fn create_user(mut req: Request<()>) -> tide::Result {
async fn login_user(mut req: Request<()>) -> tide::Result {
let r = req.body_string().await?;
let login_form : LoginForm = serde_json::from_str(&r)?;
match create_jwt(login_form).await? {
match create_jwt(login_form).await.unwrap() {
Some(jwt) => {
let msg = json!({"jwt": jwt}).to_string();
Ok(tide::Response::builder(200).body(msg).header("content-type", "application/json").build())
@ -642,9 +755,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())