From 7185a9fa31d10755a8adf6156d868eade85230fa Mon Sep 17 00:00:00 2001 From: ducdetronquito Date: Wed, 1 Dec 2021 16:37:46 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20Meh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .traefik.yml | 17 ++++ README.md | 5 + go.mod | 3 + jwt.go | 211 ++++++++++++++++++++++++++++++++++++++ jwt_test.go | 278 +++++++++++++++++++++++++++++++++++++++++++++++++++ models.go | 104 +++++++++++++++++++ 6 files changed, 618 insertions(+) create mode 100644 .traefik.yml create mode 100644 README.md create mode 100644 go.mod create mode 100644 jwt.go create mode 100644 jwt_test.go create mode 100644 models.go diff --git a/.traefik.yml b/.traefik.yml new file mode 100644 index 0000000..36c04ba --- /dev/null +++ b/.traefik.yml @@ -0,0 +1,17 @@ +displayName: Magic JWT +type: middleware + +import: github.com/ZeroGachis/traefik-magic-jwt + +summary: Traefik plugin that verify JWT token +testData: + key: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv + vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc + aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy + tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 + e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb + V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9 + MwIDAQAB + -----END PUBLIC KEY----- \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f9b4d9 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Traefik Magic JWT + +Traefik plugin for verifying JWT. + +Fork of `github/APKO/jwt_rsa` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..064431e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/ZeroGachis/traefik-magic-jwt + +go 1.16 diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..58c754a --- /dev/null +++ b/jwt.go @@ -0,0 +1,211 @@ +package traefik_magic_jwt + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "log" + "net/http" + "os" + "strings" + "time" +) + +type Config struct { + Key string `json:"key"` + Alg string `json:"-"` + InjectHeader string `json:"-"` + Debug bool `json:"debug,omitempty"` + White map[string]*WhiteUrl `json:"white,omitempty"` +} + +func CreateConfig() *Config { + return &Config{} +} + +type JwtPlugin struct { + next http.Handler + rsa interface{} + alg string + injectHeader string + debug bool + white map[string]*WhiteUrl +} + +func New(context context.Context, next http.Handler, config *Config, _ string) (http.Handler, error) { + if len(config.Key) == 0 { + config.Key = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuKijNSLvTJqPV+H/MfoR\nI/EkasKIYBTujUTjN5nxrw6q7acJlyq5pzb1MMMQqe/h1ACMmoWJ9dLHJqVMFz+h\nNkx99eWkXDj2agTjnh6VetG6owdC0yYiN2nm5eFsLtj8HBPhKF+5WguLUXoeNhOc\n0zdEfI6UkyLp+xmKVzrs7wXmBVaz0nV69drIYo8RI1+AUzHKJVOuWwykpcH+wk8P\nGvxXGw7CzM2NWAF5B9OUB+InAFApXx8FLZ0jQOAvCJcPZ7So7isxIyCD5RlhbcId\n35ZmzwBuOlskdyswX78yGc46aEAWFDUkMfrXZEy+RGoj0KunXwKKufh+bHYsKmvC\nywIDAQAB\n-----END PUBLIC KEY-----" + } + if len(config.Alg) == 0 { + config.Alg = "RS256" + } + if len(config.InjectHeader) == 0 { + config.InjectHeader = "injectedPayload" + } + jwtPlugin := &JwtPlugin{ + next: next, + injectHeader: config.InjectHeader, + debug: config.Debug, + alg: config.Alg, + white: config.White, + } + if config.Alg == "RS256" { + if err := jwtPlugin.ParseKeys(config.Key); err != nil { + return nil, err + } + } else if config.Alg == "HS256" { + jwtPlugin.rsa = []byte(config.Key) + } else { + return nil, errors.New("bad alg") + } + return jwtPlugin, nil +} + +func (jwtPlugin *JwtPlugin) ServeHTTP(rw http.ResponseWriter, request *http.Request) { + ignoreExpired := false + logger := log.New(os.Stdout, "jwt: ["+request.RemoteAddr+"]", log.Ldate|log.Ltime) + if jwtPlugin.white != nil { + for _, v := range jwtPlugin.white { + if v.Type == "" { + v.Type = "full" + } + if strings.EqualFold(v.Type, "full") && strings.EqualFold(v.Method, request.Method) && strings.EqualFold(v.URL, request.URL.Path) { + log.Println("Serve White url") + jwtPlugin.next.ServeHTTP(rw, request) + return + } + if strings.EqualFold(v.Type, "refresh") && strings.EqualFold(v.Method, request.Method) && strings.EqualFold(v.URL, request.URL.Path) { + ignoreExpired = true + } + } + } + if err := jwtPlugin.CheckToken(request, ignoreExpired, logger); err != nil { + logger.Printf("Error Handle Token %+v\n", err) + httpError(rw, err.Message, err.StatusCode) + return + } + jwtPlugin.next.ServeHTTP(rw, request) +} +func (jwtPlugin *JwtPlugin) ParseKeys(certificate string) error { + if block, rest := pem.Decode([]byte(certificate)); block != nil { + if len(rest) > 0 { + return fmt.Errorf("extra data after a PEM certificate block") + } + if block.Type == "PUBLIC KEY" || block.Type == "RSA PUBLIC KEY" { + key, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse a PEM public key: %v", err) + } + jwtPlugin.rsa = key + } + } + return nil +} +func (jwtPlugin *JwtPlugin) CheckToken(request *http.Request, ignorExpired bool, log *log.Logger) *RequestError { + jwtToken, err := jwtPlugin.ExtractToken(request, log) + if err != nil { + return err + } + if jwtToken != nil { + if err = jwtPlugin.VerifyToken(jwtToken, log); err != nil { + return err + } + if !ignorExpired { + if err = handleTokenTime(jwtToken); err != nil { + return err + } + } + request.Header.Del("Authorization") + request.Header.Add(jwtPlugin.injectHeader, string(jwtToken.RawPayload)) + } + return nil +} +func handleTokenTime(jwt *JWT) *RequestError { + expiredate, err := jwt.Payload.Exp.Int64() + if err != nil { + return expiredTokenError + } + if isExpire(expiredate) { + return expiredTokenError + } + return nil +} +func (jwtPlugin *JwtPlugin) ExtractToken(request *http.Request, log *log.Logger) (*JWT, *RequestError) { + authHeader, ok := request.Header["Authorization"] + if !ok { + log.Println("Header Authorization not found") + return nil, noTokenError + } + auth := authHeader[0] + if !strings.HasPrefix(auth, "Bearer ") { + log.Printf("No Beadrer token %s\n", auth) + return nil, noTokenError + } + parts := strings.Split(auth[7:], ".") + if len(parts) != 3 { + log.Println("Invalid Token format") + return nil, noTokenError + } + header, err := base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + log.Printf("Header: %+v\n", err) + return nil, badTokenError + } + payload, err := base64.RawURLEncoding.DecodeString(parts[1]) + if err != nil { + log.Printf("Payload: %+v\n", err) + return nil, badTokenError + } + signature, err := base64.RawURLEncoding.DecodeString(parts[2]) + if err != nil { + log.Printf("Signature: %+v\n", err) + return nil, badTokenError + } + jwtToken := JWT{ + Plaintext: []byte(auth[7 : len(parts[0])+len(parts[1])+8]), + Signature: signature, + RawPayload: payload, + } + err = json.Unmarshal(header, &jwtToken.Header) + if err != nil { + log.Printf("Json Header bad format %+v\n", err) + return nil, badTokenError + } + err = json.Unmarshal(payload, &jwtToken.Payload) + if err != nil { + log.Printf("Json Payload bad format %+v\n", err) + return nil, badTokenError + } + return &jwtToken, nil +} + +func (jwtPlugin *JwtPlugin) VerifyToken(jwtToken *JWT, log *log.Logger) *RequestError { + for _, h := range jwtToken.Header.Crit { + if _, ok := supportedHeaderNames[h]; !ok { + log.Printf("unsupported header: %s\n", h) + return verifyTokenError + } + } + a, ok := tokenAlgorithms[jwtToken.Header.Alg] + if !ok { + log.Printf("unknown JWS algorithm: %s\n", jwtToken.Header.Alg) + return verifyTokenError + } + if jwtPlugin.alg != "" && jwtToken.Header.Alg != jwtPlugin.alg { + log.Printf("incorrect alg, expected %s got %s\n", jwtPlugin.alg, jwtToken.Header.Alg) + return verifyTokenError + } + if e := a.verify(jwtPlugin.rsa, a.hash, jwtToken.Plaintext, jwtToken.Signature); e != nil { + log.Printf("Verify Error %+v\n", e) + return verifyTokenError + } + return nil +} + +func isExpire(ctime int64) bool { + return ctime < (time.Now().UnixNano() / 1000000000) +} diff --git a/jwt_test.go b/jwt_test.go new file mode 100644 index 0000000..9ccfd83 --- /dev/null +++ b/jwt_test.go @@ -0,0 +1,278 @@ +// TODO: Tests are disabled for now as they depends on bou.ke/monkey which is archived and has a restrictive license +package traefik_magic_jwt_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "bou.ke/monkey" + traefik_magic_jwt "github.com/ZeroGachis/traefik-magic-jwt" +) + +func TestServiceOk(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 26, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) + if err != nil { + t.Fatal(err) + } + req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTk0NjE5MzksInVzZXJfaWQiOjEsImV4cCI6MTYxOTQ2NTUzOX0.DPWzZSEVpIlvPhWNqYxZEaeR3tN9t8heeV7YXHOOze9ECYD9uNYGn3o5QBqyMXQslVgnM62pNkHcWi2yriNe8M8Yjmk3mWhGKF6L5llxOL3jHN7Euyh7t1bnCqyetsaPoDEtiR50C0qQyV9Dm0eyrC-ZfDKWWU24Ak816AP--QOyyrDD2eBFyDYH9u1vjn94-UtPiFXL_Weu_sVcCMK47YT5mOZklGQMtHr-7x2q6nS1lKAQT27nBam78Hl8kd0RVaA5lyDxrRsSpvxemisVKljByxwWNrnrvRHNnJoJ6b1QXbdiUdzK3uUpQJkzcehrre0QVrraPJSjVw2iP9iQHg"} + jwt.ServeHTTP(recorder, req) + if nextCalled == false { + t.Fatal("next.ServeHTTP was called") + } + if recorder.Code != 200 { + t.Fatal("response expect 200") + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `` { + t.Fatal("Bad Body") + } + h := req.Header.Get(cfg.InjectHeader) + if h != `{"iat":1619461939,"user_id":1,"exp":1619465539}` { + t.Fatalf("Header Is `%s`", h) + } +} + +func TestServiceOkHS(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 26, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + cfg.Alg = "HS256" + cfg.Key = "6990ff1osITn6JaLC5EU9QI1AEMaghDTgzvpqNid" + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) + if err != nil { + t.Fatal(err) + } + req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MjQ0NDk1NDMsInVzZXJfaWQiOjEsImV4cCI6MTYyNDQ1MzE0M30.ICUuzJ9a8aq4SD1MFm3qJ1M-da3vszXAydwsYvAxbM0"} + jwt.ServeHTTP(recorder, req) + if nextCalled == false { + t.Fatal("next.ServeHTTP was called") + } + if recorder.Code != 200 { + t.Fatal("response expect 200") + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `` { + t.Fatal("Bad Body") + } + h := req.Header.Get(cfg.InjectHeader) + if h != `{"iat":1624449543,"user_id":1,"exp":1624453143}` { + t.Fatalf("Header Is `%s`", h) + } +} +func TestServiceBadToken(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 26, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) + if err != nil { + t.Fatal(err) + } + req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTk0NjE5MzksInVzZXJfaWQiOjEsImV4cCI6MTYxOTQ2NTUzOX0.DPWzZSEVpIlvPhWNqYxZEaeR3tN9t8heeV7YXHOOze9ECYD9uNYGn3o5QBqyMXQslVgnM62pNkHcWi2yriNe8M8Yjmk3mWhGKF6L5llxOL3jHN7Euyh7t1bnCqyetsaPoDEtiR50C0qQyV9Dm0eyrC-ZfDKWWU24Ak816AP--QOyyrDD2eBFyDYH9u1vjn94-UtPiFXL_Weu_sVcCMK47YT5mOZklGQMtHr-7x2q6nS1lKAQT27nBam78Hl8kd0RVaA5lyDxrRsSpzK3uUpQJkzcehrre0QVrraPJSjVw2iP9iQHg"} + jwt.ServeHTTP(recorder, req) + if nextCalled == true { + t.Fatal("next.ServeHTTP was called") + } + if recorder.Code != http.StatusBadRequest { + t.Fatalf("response expect 400 = %d", recorder.Code) + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `Invalid Token` { + t.Fatalf("Bad Body `%s`", string(body)) + } + h := req.Header.Get(cfg.InjectHeader) + if h != `` { + t.Fatalf("Header Is `%s`", h) + } +} + +func TestServiceNoToken(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 26, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) + if err != nil { + t.Fatal(err) + } + jwt.ServeHTTP(recorder, req) + if nextCalled == true { + t.Fatal("next.ServeHTTP was called") + } + if recorder.Code != http.StatusUnauthorized { + t.Fatal("response expect 401") + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `No Token Detect` { + t.Fatalf("Bad Body `%s`", string(body)) + } + h := req.Header.Get(cfg.InjectHeader) + if h != `` { + t.Fatalf("Header Is `%s`", h) + } +} + +func TestServiceIgnoreUrl(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 26, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + cfg.White = map[string]*traefik_magic_jwt.WhiteUrl{ + "login": { + URL: "/login", + Method: http.MethodPost, + }, + } + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost/login", nil) + if err != nil { + t.Fatal(err) + } + jwt.ServeHTTP(recorder, req) + if nextCalled == false { + t.Fatal("next.ServeHTTP was not called") + } + if recorder.Code != http.StatusOK { + t.Fatal("response expect 200") + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `` { + t.Fatalf("Bad Body `%s`", string(body)) + } + h := req.Header.Get(cfg.InjectHeader) + if h != `` { + t.Fatalf("Header Is `%s`", h) + } +} + +func TestServiceExpired(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 28, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil) + if err != nil { + t.Fatal(err) + } + req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTk0NjE5MzksInVzZXJfaWQiOjEsImV4cCI6MTYxOTQ2NTUzOX0.DPWzZSEVpIlvPhWNqYxZEaeR3tN9t8heeV7YXHOOze9ECYD9uNYGn3o5QBqyMXQslVgnM62pNkHcWi2yriNe8M8Yjmk3mWhGKF6L5llxOL3jHN7Euyh7t1bnCqyetsaPoDEtiR50C0qQyV9Dm0eyrC-ZfDKWWU24Ak816AP--QOyyrDD2eBFyDYH9u1vjn94-UtPiFXL_Weu_sVcCMK47YT5mOZklGQMtHr-7x2q6nS1lKAQT27nBam78Hl8kd0RVaA5lyDxrRsSpvxemisVKljByxwWNrnrvRHNnJoJ6b1QXbdiUdzK3uUpQJkzcehrre0QVrraPJSjVw2iP9iQHg"} + jwt.ServeHTTP(recorder, req) + if nextCalled == true { + t.Fatal("next.ServeHTTP was called") + } + if recorder.Code != 451 { + t.Fatalf("response expect 451 = %d", recorder.Code) + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `Expired Token` { + t.Fatalf("Bad Body `%s`", string(body)) + } + h := req.Header.Get(cfg.InjectHeader) + if h != `` { + t.Fatalf("Header Is `%s`", h) + } +} + +func TestServiceExpiredIgnore(t *testing.T) { + monkey.Patch(time.Now, func() time.Time { + return time.Date(2021, 04, 28, 18, 40, 58, 651387237, time.UTC) + }) + cfg := traefik_magic_jwt.CreateConfig() + cfg.White = map[string]*traefik_magic_jwt.WhiteUrl{ + "login": { + URL: "/login", + Method: http.MethodPut, + Type: "refresh", + }, + } + ctx := context.Background() + nextCalled := false + next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true }) + jwt, err := traefik_magic_jwt.New(ctx, next, cfg, "test-jwt-plugin") + if err != nil { + t.Fatal(err) + } + recorder := httptest.NewRecorder() + req, err := http.NewRequestWithContext(ctx, http.MethodPut, "http://localhost/login", nil) + if err != nil { + t.Fatal(err) + } + req.Header["Authorization"] = []string{"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTk0NjE5MzksInVzZXJfaWQiOjEsImV4cCI6MTYxOTQ2NTUzOX0.DPWzZSEVpIlvPhWNqYxZEaeR3tN9t8heeV7YXHOOze9ECYD9uNYGn3o5QBqyMXQslVgnM62pNkHcWi2yriNe8M8Yjmk3mWhGKF6L5llxOL3jHN7Euyh7t1bnCqyetsaPoDEtiR50C0qQyV9Dm0eyrC-ZfDKWWU24Ak816AP--QOyyrDD2eBFyDYH9u1vjn94-UtPiFXL_Weu_sVcCMK47YT5mOZklGQMtHr-7x2q6nS1lKAQT27nBam78Hl8kd0RVaA5lyDxrRsSpvxemisVKljByxwWNrnrvRHNnJoJ6b1QXbdiUdzK3uUpQJkzcehrre0QVrraPJSjVw2iP9iQHg"} + jwt.ServeHTTP(recorder, req) + if nextCalled == false { + t.Fatal("next.ServeHTTP was not called") + } + if recorder.Code != 200 { + t.Fatalf("response expect 200 = %d", recorder.Code) + } + resp := recorder.Result() + body, _ := io.ReadAll(resp.Body) + if string(body) != `` { + t.Fatalf("Bad Body `%s`", string(body)) + } + h := req.Header.Get(cfg.InjectHeader) + if h != `{"iat":1619461939,"user_id":1,"exp":1619465539}` { + t.Fatalf("Header Is `%s`", h) + } +} diff --git a/models.go b/models.go new file mode 100644 index 0000000..638c12a --- /dev/null +++ b/models.go @@ -0,0 +1,104 @@ +package traefik_magic_jwt + +import ( + "crypto" + "crypto/hmac" + "crypto/rsa" + "encoding/json" + "errors" + "fmt" + "net/http" +) + +type JwtHeader struct { + Alg string `json:"alg"` + Kid string `json:"kid"` + Typ string `json:"typ"` + Cty string `json:"cty"` + Crit []string `json:"crit"` +} +type tokenPayLoad struct { + Iat json.Number `json:"iat"` + Exp json.Number `json:"exp"` +} +type JWT struct { + Plaintext []byte + Signature []byte + Header JwtHeader + Payload tokenPayLoad + RawPayload []byte +} + +var supportedHeaderNames = map[string]struct{}{"alg": {}, "kid": {}, "typ": {}, "cty": {}, "crit": {}} + +type tokenVerifyFunction func(key interface{}, hash crypto.Hash, payload []byte, signature []byte) error +type tokenVerifyAsymmetricFunction func(key interface{}, hash crypto.Hash, digest []byte, signature []byte) error + +// jwtAlgorithm describes a JWS 'alg' value +type tokenAlgorithm struct { + hash crypto.Hash + verify tokenVerifyFunction +} + +// tokenAlgorithms is the known JWT algorithms +var ( + tokenAlgorithms = map[string]tokenAlgorithm{ + "RS256": {crypto.SHA256, verifyAsymmetric(verifyRSAPKCS)}, + "HS256": {crypto.SHA256, verifyHMAC}, + } + noTokenError = &RequestError{StatusCode: http.StatusUnauthorized, Message: "No Token Detect"} + badTokenError = &RequestError{StatusCode: http.StatusBadRequest, Message: "Invalid Token"} + verifyTokenError = &RequestError{StatusCode: http.StatusBadRequest, Message: "Verify Error"} + expiredTokenError = &RequestError{StatusCode: http.StatusUnavailableForLegalReasons, Message: "Expired Token"} +) + +func verifyHMAC(key interface{}, hash crypto.Hash, payload []byte, signature []byte) error { + macKey, ok := key.([]byte) + if !ok { + return fmt.Errorf("incorrect symmetric key type") + } + mac := hmac.New(hash.New, macKey) + if _, err := mac.Write([]byte(payload)); err != nil { + return err + } + if !hmac.Equal(signature, mac.Sum([]byte{})) { + return errors.New("signature not verified") + } + return nil +} +func verifyAsymmetric(verify tokenVerifyAsymmetricFunction) tokenVerifyFunction { + return func(key interface{}, hash crypto.Hash, payload []byte, signature []byte) error { + h := hash.New() + _, err := h.Write(payload) + if err != nil { + return err + } + return verify(key, hash, h.Sum([]byte{}), signature) + } +} + +func verifyRSAPKCS(key interface{}, hash crypto.Hash, digest []byte, signature []byte) error { + publicKeyRsa := key.(*rsa.PublicKey) + if err := rsa.VerifyPKCS1v15(publicKeyRsa, hash, digest, signature); err != nil { + return fmt.Errorf("token verification failed (RSAPKCS)") + } + return nil +} + +type WhiteUrl struct { + URL string `json:"url"` + Method string `json:"method"` + Type string `json:"type,omitempty"` +} + +type RequestError struct { + StatusCode int + Message string +} + +func httpError(w http.ResponseWriter, e string, code int) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.WriteHeader(code) + fmt.Fprint(w, e) +}