Rewrite in Go with querystring support
parent
957f51867f
commit
26834cf938
|
@ -1,4 +1,12 @@
|
||||||
# ---> Rust
|
# Binaries for programs and plugins
|
||||||
/target
|
*.exe
|
||||||
Cargo.lock
|
*.exe~
|
||||||
**/*.rs.bk
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
16
Cargo.toml
16
Cargo.toml
|
@ -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"
|
|
45
Dockerfile
45
Dockerfile
|
@ -1,40 +1,15 @@
|
||||||
# FROM rust:1.32 as build
|
FROM golang:1.11-rc-alpine as build
|
||||||
FROM alpine:3.9 AS build
|
|
||||||
|
|
||||||
RUN apk add --no-cache gcc musl-dev
|
RUN apk add --no-cache git=2.18.1-r0
|
||||||
RUN apk add --no-cache rust cargo
|
|
||||||
|
|
||||||
# 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
|
FROM scratch
|
||||||
RUN USER=root cargo new --bin app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy over Cargo.toml
|
COPY --from=build /src/auth /auth
|
||||||
COPY ./Cargo.toml ./Cargo.toml
|
COPY --from=build /src/.env /.env
|
||||||
|
|
||||||
# Change target env
|
ENTRYPOINT ["/auth"]
|
||||||
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"]
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# backend-auth
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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)
|
||||||
|
}
|
102
src/main.rs
102
src/main.rs
|
@ -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, "")))
|
|
||||||
}
|
|
Loading…
Reference in New Issue