5
0
Fork 0

Rewrite in Go with querystring support

master
UnicodingUnicorn 2019-03-24 08:22:22 +08:00
parent 957f51867f
commit 26834cf938
8 changed files with 153 additions and 158 deletions

16
.gitignore vendored
View File

@ -1,4 +1,12 @@
# ---> Rust
/target
Cargo.lock
**/*.rs.bk
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

View File

@ -1,16 +0,0 @@
[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"
serde_qs = "0.4.5"
urlencoded = "0.6.0"

View File

@ -1,40 +1,15 @@
# FROM rust:1.32 as build
FROM alpine:3.9 AS build
FROM golang:1.11-rc-alpine as build
RUN apk add --no-cache gcc musl-dev
RUN apk add --no-cache rust cargo
RUN apk add --no-cache git=2.18.1-r0
# RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /src
COPY go.mod go.sum .env *.go ./
RUN go get -d -v ./...
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
# Create new empty shell project
RUN USER=root cargo new --bin app
WORKDIR /app
FROM scratch
# Copy over Cargo.toml
COPY ./Cargo.toml ./Cargo.toml
COPY --from=build /src/auth /auth
COPY --from=build /src/.env /.env
# Change target env
ENV RUSTFLAGS="-C target-cpu=native"
# ENV RUSTFLAGS="-C target-cpu=x86_64_alpine-linux-musl"
# Run build step to cache dependencies
RUN cargo build --release
RUN rm src/*.rs
# Copy over src files
COPY ./src/main.rs ./src/main.rs
# Build for release
RUN rm ./target/release/deps/backend_auth*
RUN cargo build --release
# Copy over .env
COPY ./.env ./.env
FROM alpine:3.9
RUN apk add --no-cache gcc
COPY --from=build /app/target/release .
COPY --from=build /app/.env .env
ENTRYPOINT ["./backend-auth"]
ENTRYPOINT ["/auth"]

View File

@ -1,6 +1,6 @@
# backend-auth
Beep backend auth proxy. At long last, something done properly in Rust. My ancestors are smiling at me, Imperial, can you say the same?
Beep backend auth proxy.
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. Alternatively, the token can be supplied in the querystring as `token`. Tokens in the Authorization header override tokens in the querystring. 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.

7
go.mod Normal file
View File

@ -0,0 +1,7 @@
module auth
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/joho/godotenv v1.3.0
github.com/julienschmidt/httprouter v1.2.0
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=

117
main.go Normal file
View File

@ -0,0 +1,117 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"github.com/joho/godotenv"
"github.com/julienschmidt/httprouter"
"github.com/dgrijalva/jwt-go"
)
var listen string
var secret []byte
func main() {
// Load .env
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
listen = os.Getenv("LISTEN")
secret = []byte(os.Getenv("SECRET"))
// Routes
router := httprouter.New()
router.OPTIONS("/auth", PassThrough)
router.GET("/auth", Auth)
router.POST("/auth", Auth)
router.PUT("/auth", Auth)
router.DELETE("/auth", Auth)
router.PATCH("/auth", Auth)
// Start server
log.Printf("starting server on %s", listen)
log.Fatal(http.ListenAndServe(listen, router))
}
func PassThrough(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
w.WriteHeader(http.StatusOK)
}
type UserClaims struct {
UserId string `json:"userid"`
ClientId string `json:"clientid"`
}
func Auth(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
tokenString := ""
// Extract token from querystrign in X-Forwarded-Uri
uri, err := url.Parse(r.Header.Get("X-Forwarded-Uri"))
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
queries, err := url.ParseQuery(uri.RawQuery)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
if len(queries["token"]) > 0 {
tokenString = queries["token"][0]
}
// Extract token from Authorization header
reqToken := r.Header.Get("Authorization")
splitToken := strings.Split(reqToken, "Bearer ")
if len(splitToken) > 1 {
tokenString = splitToken[1]
}
// Make sure token string is not null
if tokenString == "" {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Parse token
token, err := jwt.Parse(tokenString, func (token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return secret, nil
})
if err != nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
if !token.Valid {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
userclaims := UserClaims {
UserId: claims["userid"].(string),
ClientId: claims["clientid"].(string),
}
userclaimBytes, err := json.Marshal(&userclaims)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
w.Header().Set("X-User-Claim", (string)(userclaimBytes))
w.WriteHeader(http.StatusOK)
}

View File

@ -1,102 +0,0 @@
#[macro_use]
extern crate serde;
extern crate serde_json;
extern crate serde_qs;
#[macro_use]
extern crate lazy_static;
extern crate dotenv;
extern crate iron;
extern crate router;
extern crate jsonwebtoken as jwt;
extern crate urlencoded;
use dotenv::dotenv;
use iron::prelude::*;
use iron::headers::{ Authorization, Bearer };
use iron::status::Status;
use router::Router;
use urlencoded::{ UrlDecodingError, UrlEncodedQuery };
use std::env;
#[derive(Debug, Serialize, Deserialize)]
struct UserClaims {
userid: String,
clientid: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct TokenString {
token: Option<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<Response> {
let mut token = None;
// Check token from querystring of forwarded URI
if let Some(forwarded_uri) = req.headers.get_raw("X-Forwarded-Uri") {
if !forwarded_uri.is_empty() {
let forwarded_uri = match std::str::from_utf8(&forwarded_uri[0]) {
Ok(uri) => uri,
Err(_) => return Ok(Response::with((Status::BadRequest, "400 Bad Request"))),
};
let qs:TokenString = match serde_qs::from_str(forwarded_uri) {
Ok(qs) => qs,
Err(_) => return Ok(Response::with((Status::BadRequest, "400 Bad Request"))),
};
token = qs.token;
}
}
// Check token from Authorization header
if let Some(authorisation_header) = req.headers.get::<Authorization<Bearer>>() {
token = Some(authorisation_header.token.clone());
}
// Process token
if let Some(token) = token {
match jwt::decode::<UserClaims>(&token, secret.as_ref(), &jwt::Validation{ validate_exp:false, ..Default::default()}) { // Don't validate expiry
// match jwt::decode::<UserClaims>(&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<Response> {
Ok(Response::with((Status::Ok, "")))
}