diff --git a/.traefik.yml b/.traefik.yml new file mode 100644 index 0000000..0b42266 --- /dev/null +++ b/.traefik.yml @@ -0,0 +1,7 @@ +displayName: JWT Validation Middleware +type: middleware +import: github.com/legege/jwt-validation-middleware +summary: 'Verify JWT Token in Auth header, Cookie or Query param, and injects decoded payload in header' + +testData: + secret: ThisIsMyVerySecret diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9066c3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 23 degrees GmbH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bb3e61 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# JWT Validation Middleware + +JWT Validation Middleware is a middleware plugin for [Traefik](https://github.com/containous/traefik) which verifies a JWT token in Auth header, Cookie or Query param, and adds the payload as injected header to the request. + +## Configuration + +Start with command +```yaml +command: + - "--experimental.plugins.jwt-middleware.modulename=github.com/legege/jwt-middleware" + - "--experimental.plugins.jwt-middleware.version=v0.1.0" +``` + +Activate plugin in your config + +```yaml +http: + middlewares: + my-jwt-middleware: + plugin: + jwt-middleware: + secret: SECRET + payloadHeader: X-Jwt-Payload + authQueryParam: authToken + authCookieName: authToken +``` + +Use as docker-compose label +```yaml + labels: + - "traefik.http.routers.my-service.middlewares=my-jwt-middleware@file" +``` + +Forked from https://github.com/23deg/jwt-middleware \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12a1b5b --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/legege/jwt-validation-middleware + +go 1.19 \ No newline at end of file diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..85dbade --- /dev/null +++ b/jwt.go @@ -0,0 +1,181 @@ +package jwt_middleware + +import ( + "context" + "fmt" + "net/http" + "strings" + + "crypto/hmac" + "crypto/sha256" + "encoding/base64" +) + +type Config struct { + Secret string `json:"secret,omitempty"` + PayloadHeader string `json:"payloadHeader,omitempty"` + AuthHeader string `json:"authHeader,omitempty"` + AuthHeaderPrefix string `json:"authHeaderPrefix,omitempty"` + AuthQueryParam string `json:"authQueryParam,omitempty"` + AuthCookieName string `json:"authCookieName,omitempty"` +} + +func CreateConfig() *Config { + return &Config{} +} + +type JWT struct { + next http.Handler + name string + secret string + payloadHeader string + authQueryParam string + authCookieName string +} + +func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) { + + if len(config.Secret) == 0 { + config.Secret = "SECRET" + } + if len(config.PayloadHeader) == 0 { + config.PayloadHeader = "X-Jwt-Payload" + } + if len(config.AuthQueryParam) == 0 { + config.AuthQueryParam = "authToken" + } + if len(config.AuthCookieName) == 0 { + config.AuthCookieName = "authToken" + } + + return &JWT{ + next: next, + name: name, + secret: config.Secret, + payloadHeader: config.PayloadHeader, + authQueryParam: config.AuthQueryParam, + authCookieName: config.AuthCookieName, + }, nil +} + +func (j *JWT) ServeHTTP(res http.ResponseWriter, req *http.Request) { + rawToken := j.extractTokenFromHeader(req) + if len(rawToken) == 0 && j.authQueryParam != "" { + rawToken = j.extractTokenFromQuery(req) + } + if len(rawToken) == 0 && j.authCookieName != "" { + rawToken = j.extractTokenFromCookie(req) + } + if len(rawToken) == 0 { + http.Error(res, "Token not provided", http.StatusUnauthorized) + return + } + + token, preprocessError := preprocessJWT(rawToken) + if preprocessError != nil { + http.Error(res, preprocessError.Error(), http.StatusBadRequest) + return + } + + verified, verificationError := verifyJWT(token, j.secret) + if verificationError != nil { + http.Error(res, verificationError.Error(), http.StatusUnauthorized) + return + } + + if verified { + // If true decode payload + payload, decodeErr := decodeBase64(token.payload) + if decodeErr != nil { + http.Error(res, decodeErr.Error(), http.StatusBadRequest) + return + } + + // TODO Check for outside of ASCII range characters + + // Inject header as proxypayload or configured name + req.Header.Add(j.payloadHeader, payload) + fmt.Println(req.Header) + j.next.ServeHTTP(res, req) + } else { + http.Error(res, "Not allowed", http.StatusUnauthorized) + } +} + +func (j *JWT) extractTokenFromCookie(request *http.Request) string { + cookie, err := request.Cookie(j.authCookieName) + if err != nil { + return "" + } + return cookie.Value +} + +func (j *JWT) extractTokenFromQuery(request *http.Request) string { + if request.URL.Query().Has(j.authQueryParam) { + return request.URL.Query().Get(j.authQueryParam) + } + return "" +} + +func (j *JWT) extractTokenFromHeader(request *http.Request) string { + authHeader, ok := request.Header["Authorization"] + if !ok { + return "" + } + auth := authHeader[0] + if !strings.HasPrefix(auth, "Bearer ") { + return "" + } + return auth[7:] +} + +// Token Deconstructed header token +type Token struct { + header string + payload string + verification string +} + +// verifyJWT Verifies jwt token with secret +func verifyJWT(token Token, secret string) (bool, error) { + mac := hmac.New(sha256.New, []byte(secret)) + message := token.header + "." + token.payload + mac.Write([]byte(message)) + expectedMAC := mac.Sum(nil) + + decodedVerification, errDecode := base64.RawURLEncoding.DecodeString(token.verification) + if errDecode != nil { + return false, errDecode + } + + if hmac.Equal(decodedVerification, expectedMAC) { + return true, nil + } + return false, nil + // TODO Add time check to jwt verification +} + +func preprocessJWT(rawToken string) (Token, error) { + var token Token + + tokenSplit := strings.Split(rawToken, ".") + + if len(tokenSplit) != 3 { + return token, fmt.Errorf("Invalid token") + } + + token.header = tokenSplit[0] + token.payload = tokenSplit[1] + token.verification = tokenSplit[2] + + return token, nil +} + +// decodeBase64 Decode base64 to string +func decodeBase64(baseString string) (string, error) { + byte, decodeErr := base64.RawURLEncoding.DecodeString(baseString) + if decodeErr != nil { + return baseString, fmt.Errorf("Error decoding") + } + return string(byte), nil +}