mirror of https://github.com/apibillme/broker
Compare commits
3 Commits
b413e1e859
...
5228b58043
Author | SHA1 | Date |
---|---|---|
Bevan Hunt | 5228b58043 | |
Bevan Hunt | 65ed39fa44 | |
Bevan Hunt | cb86940dca |
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
18
README.md
18
README.md
|
@ -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`
|
||||
|
|
80
src/main.rs
80
src/main.rs
|
@ -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())
|
||||
|
|
Loading…
Reference in New Issue