From e488cc75e0a7bcb50ed0d9635aeee7fb84fe7f74 Mon Sep 17 00:00:00 2001 From: Bevan Hunt Date: Thu, 8 Apr 2021 16:40:00 -0700 Subject: [PATCH] update to 12.0.0 --- CHANGELOG.md | 8 ++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 ++++++++++++------ src/main.rs | 26 +++++++++++++++++++++++--- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d246f7f..0dca1d5 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). +## [12.0.0] - 2021-04-08 + +### Added +- Added JWT custom scopes on user + +### Updated +- Updated README + ## [11.2.0] - 2021-04-06 ### Added diff --git a/Cargo.lock b/Cargo.lock index 6e93d0d..4af734a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,7 +480,7 @@ dependencies = [ [[package]] name = "broker" -version = "11.2.0" +version = "12.0.0" dependencies = [ "anyhow", "async-std", diff --git a/Cargo.toml b/Cargo.toml index 5011441..e7a30f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broker" -version = "11.2.0" +version = "12.0.0" authors = ["Bevan Hunt "] edition = "2018" license = "MIT" diff --git a/README.md b/README.md index 16d55f3..17b14f2 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,19 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD * Under 1000 lines of code * Secure Real-time Event Stream via SSE - requires the use of [broker-client](https://www.npmjs.com/package/broker-client) * Supports CORS +* JSON API * Add users with admin token permission * Multi-tenant * Supports SSL - full end-to-end encryption * Provides user authentication with JWTs or HTTP Basic -* Secure passwords with Argon2 encoding -* Uses Global NTP servers and doesn't rely on your local server time -* Insert event via JSON POST request +* 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 * Sync latest events on SSE client connection * Auto-provision and renews SSL cert via LetsEncrypt or use your own SSL cert -* Verify endpoint for external services using Broker user system like [portal](https://crates.io/crates/portal) -* User Management API endpoints (revoke, unrevoke, list, get, update) -* User Email Address Validation (regex and blacklist check against throwaway emails) +* 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) ### How it works @@ -72,6 +73,7 @@ POST /create_user "admin_token": "letmein", "tenant_name": "tenant_1", "email": "bob@hotmail.com", + "scopes": ["news:get", "news:post"], "data": { "name": "Robert Wieland", "image": "https://img.com/bucket/123/123.jpg" @@ -102,6 +104,7 @@ will return "jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTc2NzQ5MTUsImlhdCI6MTYxNzU4ODUxNSwiaXNzIjoiRGlzcGF0Y2hlciIsInN1YiI6ImZvbyJ9.OwiaZJcFUC_B0CA0ffRZVTWKRf5_vQ7vt5USNJEeKRE" } ``` +- note: `iss` is `Broker` while `aud` is the user scopes joined with a comma like in this example `news:get,news:post` #### Step 3 - connect to SSE @@ -195,6 +198,7 @@ will return: `200` or `500` or `400` or `401` "tenant_name": "tenant_1", "username": "bob", "email": "bob@hotmail.com", + "scopes": ["news:get", "news:post"], "data": { "name": "Robert Wieland", "image": "https://img.com/bucket/123/123.jpg" @@ -228,6 +232,7 @@ will return: `200` or `500` or `400` or `401` "tenant_name": "tenant_1", "username": "bob", "email": "bob@hotmail.com", + "scopes": ["news:get", "news:post"], "data": { "name": "Robert Wieland", "image": "https://img.com/bucket/123/123.jpg" @@ -249,6 +254,7 @@ POST /update_user "tenant_name": "tenant_2", "password": "new_password", "email": "bober@hotmail.com", + "scopes": ["news:get", "news:post"], "data": { "name": "Robert Falcon", "image": "https://img.com/bucket/123/1234.jpg" diff --git a/src/main.rs b/src/main.rs index 3126f46..adb0396 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,6 +58,7 @@ pub struct User { pub password: String, pub tenant_name: String, pub data: Option, + pub scopes: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -68,6 +69,7 @@ pub struct UserForm { pub admin_token: String, pub email: Option, pub data: Option, + pub scopes: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -78,6 +80,7 @@ pub struct UpdateUserForm { pub email: Option, pub password: Option, pub data: Option, + pub scopes: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -103,6 +106,7 @@ pub struct Claims { pub iat: i64, pub iss: String, pub sub: String, + pub aud: String, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -169,6 +173,7 @@ fn modify_user(update_user_form: UpdateUserForm) -> Result> { } user.data = update_user_form.data; + user.scopes = update_user_form.scopes; match update_user_form.password { Some(password) => { @@ -332,6 +337,7 @@ fn user_create(user_form: UserForm) -> Result> { revoked: false, email: user_form.clone().email, data: user_form.clone().data, + scopes: user_form.clone().scopes, }; puts_user(new_user).unwrap(); @@ -353,8 +359,15 @@ async fn create_jwt(login: LoginForm) -> Result> { let app = env_var_config(); let iat = nippy::get_unix_ntp_time().await?; let exp = iat + app.jwt_expiry; - let iss = "Dispatcher".to_string(); - let my_claims = Claims{sub: user.clone().username, exp, iat, iss}; + 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 { @@ -432,7 +445,14 @@ async fn jwt_verify(token: String) -> Result>> { let iat = nippy::get_unix_ntp_time().await?; let exp = iat + app.jwt_expiry; let iss = "Broker".to_string(); - let my_claims = Claims{sub: user.clone().username, exp, iat, iss}; + 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 my_token = TokenData{ header: Header::default(), claims: my_claims,