diff --git a/.env b/.env new file mode 100644 index 0000000..a048662 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +LISTEN=127.0.0.1:3000 +SECRET=secret diff --git a/.gitignore b/.gitignore index 9a3a8d8..a0bc7f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,4 @@ -# ---> Go -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - +# ---> Rust +/target +Cargo.lock +**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0166836 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "backend-auth" +version = "0.1.0" +authors = ["UnicodingUnicorn <7555ic@gmail.com>"] +edition = "2018" + +[dependencies] +dotenv = "0.13.0" +lazy_static = "1.2.0" +iron = "0.6.0" +router = "0.6.0" +jsonwebtoken = "5" +serde = { version = "1.0", features = [ "derive" ] } +serde_json = "1.0" diff --git a/README.md b/README.md index 6a5b31d..f08950d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,14 @@ # backend-auth -Beep backend auth proxy \ No newline at end of file +Beep backend auth proxy. At long last, something done properly in Rust. My ancestors are smiling at me, Imperial, can you say the same? + +Is basically tailored just for traefik's Forward Authentication system. It takes a `GET`, `POST`, `PUT`, `PATCH` or `DELETE` request, reads a Bearer Auth JWT token if available. If it is not available or invalid, request fails with 4XX and traefik rejects the request. Otherwise, a success response is returned with a `X-User-Claim` header containing serialised user information. `OPTIONS` requests are allowed to pass through wholesale. + +## Contents of `X-User-Claim` + +```json +{ + "userid": "", + "clientid": "" +} +``` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ff98395 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,71 @@ +#[macro_use] +extern crate serde; +extern crate serde_json; +#[macro_use] +extern crate lazy_static; +extern crate dotenv; +extern crate iron; +extern crate router; +extern crate jsonwebtoken as jwt; + +use dotenv::dotenv; +use iron::prelude::*; +use iron::headers::{ Authorization, Bearer }; +use iron::status::Status; +use router::Router; +use std::env; + +#[derive(Debug, Serialize, Deserialize)] +struct UserClaims { + userid: String, + clientid: String, +} + +fn main() { + dotenv().ok(); + + let listen = env::var("LISTEN").unwrap(); + + lazy_static! { + static ref secret:String = env::var("SECRET").unwrap(); + } + + fn verify_jwt(req:&mut Request) -> IronResult { + if let Some(authorisation_header) = req.headers.get::>() { + match jwt::decode::(&authorisation_header.token, secret.as_ref(), &jwt::Validation{ validate_exp:false, ..Default::default()}) { // Don't validate expiry + // match jwt::decode::(&authorisation_header.token, secret.as_ref(), &jwt::Validation::default()) { // Production version + Ok(decoded) => { + let user_string = match serde_json::to_string(&decoded.claims) { + Ok(s) => s, + Err(_) => return Ok(Response::with((Status::Unauthorized, "401 Unauthorised"))), + }; + let mut res = Response::with((Status::Ok, "200 Ok")); + res.headers.set_raw("X-User-Claim", vec![user_string.as_bytes().to_vec()]); + Ok(res) + }, + Err(_) => Ok(Response::with((Status::Unauthorized, "401 Unauthorised"))) + } + } else { + Ok(Response::with((Status::BadRequest, "400 Bad Request"))) + } + } + + let mut router = Router::new(); + // Let OPTIONS through + router.options("/auth", success, "auth_options"); + // Auth GET, POST, PUT, PATCH, DELETE + router.get("/auth", verify_jwt, "auth_get"); + router.post("/auth", verify_jwt, "auth_post"); + router.put("/auth", verify_jwt, "auth_put"); + router.patch("/auth", verify_jwt, "auth_patch"); + router.delete("/auth", verify_jwt, "auth_delete"); + + match Iron::new(router).http(&listen) { + Ok(_) => println!("Listening on {}", &listen), + Err(e) => println!("Error: {:?}", e), + }; +} + +fn success(_:&mut Request) -> IronResult { + Ok(Response::with((Status::Ok, ""))) +}