Add HTTP Basic authentication (#14)

* add http basic auth

* bump ver

* add tests for http basic
This commit is contained in:
Bevan Hunt 2020-04-11 20:09:23 -07:00 committed by GitHub
parent 1137c75c15
commit 8ebe1bdc57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 122 additions and 30 deletions

3
Cargo.lock generated
View File

@ -121,9 +121,10 @@ dependencies = [
[[package]]
name = "broker"
version = "4.3.4"
version = "4.4.0"
dependencies = [
"Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bcrypt 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
"broker-ntp 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"bus 2.2.3 (registry+https://github.com/rust-lang/crates.io-index)",

View File

@ -1,6 +1,6 @@
[package]
name = "broker"
version = "4.3.4"
version = "4.4.0"
authors = ["Bevan Hunt <bevan@bevanhunt.com>"]
edition = "2018"
license = "MIT"
@ -31,6 +31,7 @@ bus = "2.2"
broker-ntp = "0.0.1"
Inflector = "0.11"
json-patch = "0.2"
base64 = "0.12"
[dev-dependencies]
reqwest = { version = "0.10", features = ["json"] }

View File

@ -14,12 +14,12 @@ Broker follows an insert-only/publish/subscribe paradigm rather than a REST CRUD
### Features
* Very performant with a low memory footprint that uses about 20MB and 1 CPU thread
* About 500 lines of code
* Very performant with a low memory footprint that uses about 20MB and 2 CPU threads
* Under 1,000 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
* Supports SSL
* Provides user authentication with JWTs and Bcrypt(ed) passwords
* Supports SSL - full end-to-end encryption
* Provides user authentication with JWTs or HTTP Basic with stored Bcrypt(ed) passwords
* Handles future events via Epoch UNIX timestamp
* Uses Global NTP servers and doesn't rely on your local server time
* Stateful immutable event persistence
@ -73,7 +73,7 @@ will return
```
- where {...} is the uuid (string) of the user
#### Step 2 - login with the user
#### For JWT Auth: Step 2 - login with the user
```html
POST /login
@ -96,7 +96,7 @@ will return
```html
GET /events
```
- authenticated endpoint (Authorization: Bearer {jwt})
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
- connect your sse-client to this endpoint using [broker-client](https://www.npmjs.com/package/broker-client)
- note: broker-client uses fetch as eventsource doesn't support headers
@ -105,7 +105,7 @@ GET /events
```html
POST /insert
```
- authenticated endpoint (Authorization: Bearer {jwt})
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
- POST JSON to insert an event
```json
{"event":{...}, "collection_id":{...}, "timestamp":{...}, "data":{...}}
@ -123,7 +123,7 @@ will return
```html
GET /collections/{collection_id}
```
- authenticated endpoint (Authorization: Bearer {jwt})
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
- do a GET request where {collection_id} is the uuid of the collection you want (sorted by ascending timestamp)
will return
@ -135,7 +135,7 @@ will return
```html
GET /user_events
```
- authenticated endpoint (Authorization: Bearer {jwt})
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
- do a GET request to get the user event collections (sorted by ascending timestamp)
will return
@ -147,7 +147,7 @@ will return
```html
GET /cancel/{id}
```
- authenticated endpoint (Authorization: Bearer {jwt})
- authenticated endpoint (Authorization: Bearer {jwt}) or (Authorization: Basic {base64})
- do a GET request where id is the uuid of the event to cancel a future event
will return

View File

@ -17,6 +17,7 @@ use inflector::Inflector;
use json_patch::merge;
use std::sync::{Arc, Mutex};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use base64::{decode as base64_decode};
// init database as lazy
lazy_static! {
@ -442,21 +443,72 @@ fn config() -> Config {
Config{port: port, secret: secret, origin: origin, save_path: save_path, expiry: expiry, connection: connection, key_path: key_path, cert_path: cert_path}
}
// verify the exp and key of the JWT
// verify the exp and key of the JWT or the HTTP Basic Username/Password
fn jwt_verify(config: Config, token: String) -> JWT {
let parts = token.split(" ");
for part in parts {
if part != "Bearer" {
let _ = match decode::<Claims>(&part, &DecodingKey::from_secret(config.secret.as_ref()), &Validation::default()) {
Ok(c) => {
return JWT{check: true, claims: c.claims}
},
Err(_e) => {
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}}
}
};
}
let mut parts = token.split(" ");
let auth_type = parts.next().unwrap();
if auth_type == "Bearer" {
let token = parts.next().unwrap();
let _ = match decode::<Claims>(&token, &DecodingKey::from_secret(config.secret.as_ref()), &Validation::default()) {
Ok(c) => {
return JWT{check: true, claims: c.claims};
},
Err(_) => {
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
}
};
} else if auth_type == "Basic" {
let token = parts.next().unwrap();
let _ = match &base64_decode(token) {
Ok(c) => {
let _ = match std::str::from_utf8(&c) {
Ok(d) => {
let mut username_password = d.split(":");
let username = username_password.next().unwrap();
let password = username_password.next().unwrap();
let tree = TREE.get(&"tree".to_owned()).unwrap();
let records : HashMap<String, String> = tree.iter().into_iter().filter(|x| {
let p = x.as_ref().unwrap();
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
if k.contains(&"_u_") {
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
let user : User = serde_json::from_str(&v).unwrap();
if user.username == username {
return true
} else {
return false
}
} else {
return false
}
}).map(|x| {
let p = x.unwrap();
let k = std::str::from_utf8(&p.0).unwrap().to_owned();
let v = std::str::from_utf8(&p.1).unwrap().to_owned();
(k, v)
}).collect();
for (_k, v) in records {
let user : User = serde_json::from_str(&v).unwrap();
let verified = verify(password, &user.password).unwrap();
if verified {
return JWT{check: true, claims: Claims{company: "".to_owned(), exp: 0, sub: user.id.to_string()}};
} else {
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
}
}
},
Err(_) => {
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
}
};
},
Err(_) => {
return JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}};
}
};
}
JWT{check: false, claims: Claims{company: "".to_owned(), exp: 0, sub: "".to_owned()}}
}

View File

@ -1,5 +1,6 @@
extern crate broker;
use serde_json::json;
use base64::encode;
#[tokio::test]
async fn test1() {
@ -14,6 +15,9 @@ async fn test1() {
let client = reqwest::Client::new();
let basic_token = encode("rust22:rust");
let basic = format!("Basic {}", basic_token);
// create user 1 - want success
let res = client.post("http://localhost:8080/users")
.json(&user1)
@ -66,7 +70,7 @@ async fn test1() {
.status();
assert_eq!(res, 401);
// post event - want success
// post event with JWT - want success
let res = client.post("http://localhost:8080/insert")
.header("Authorization", &bearer)
.json(&event1)
@ -75,7 +79,7 @@ async fn test1() {
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(event.event.published, false);
// post event - want success
// post event with JWT - want success
let res = client.post("http://localhost:8080/insert")
.header("Authorization", &bearer)
.json(&event2)
@ -84,6 +88,15 @@ async fn test1() {
let event2 : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(event2.event.published, false);
// post event with HTTP Basic - want success
let res = client.post("http://localhost:8080/insert")
.header("Authorization", &basic)
.json(&event1)
.send().await.unwrap();
assert_eq!(res.status(), 200);
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(event.event.published, false);
// try getting collection without auth - want failure
let res = client.get("http://localhost:8080/collections/123")
.send().await.unwrap()
@ -94,7 +107,7 @@ async fn test1() {
let half_second = std::time::Duration::from_millis(500);
std::thread::sleep(half_second);
// get collection - want success
// get collection with JWT - want success
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90c")
.header("Authorization", &bearer)
.send().await.unwrap();
@ -102,6 +115,14 @@ async fn test1() {
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(events.events[0].published, true);
// get collection with HTTP Basic - want success
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90c")
.header("Authorization", &basic)
.send().await.unwrap();
assert_eq!(res.status(), 200);
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(events.events[0].published, true);
// try getting user without auth - want failure
let res = client.get("http://localhost:8080/user_events")
.send().await.unwrap()
@ -121,7 +142,7 @@ async fn test1() {
.status();
assert_eq!(res, 400);
// cancel - want success
// cancel with JWT - want success
let url = format!("http://localhost:8080/cancel/{}", event2.event.id);
let res = client.get(&url)
.header("Authorization", &bearer)
@ -130,11 +151,28 @@ async fn test1() {
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(event.event.cancelled, true);
// get collection - want success
// cancel with HTTP Basic - want success
let url = format!("http://localhost:8080/cancel/{}", event2.event.id);
let res = client.get(&url)
.header("Authorization", &basic)
.send().await.unwrap();
assert_eq!(res.status(), 200);
let event : broker::Record = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(event.event.cancelled, true);
// get collection with JWT - want success
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90d")
.header("Authorization", &bearer)
.send().await.unwrap();
assert_eq!(res.status(), 200);
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(events.events[0].published, false);
// get collection with HTTP Basic - want success
let res = client.get("http://localhost:8080/collections/3ca76743-8d99-4d3f-b85c-633ea456f90d")
.header("Authorization", &basic)
.send().await.unwrap();
assert_eq!(res.status(), 200);
let events : broker::Collection = serde_json::from_str(&res.text().await.unwrap()).unwrap();
assert_eq!(events.events[0].published, false);
}