From 647e6ae1c42fade1283a095ac9298919785cb6c6 Mon Sep 17 00:00:00 2001 From: Lei Cao Date: Mon, 24 Nov 2014 23:21:03 +0800 Subject: [PATCH] Added JWT plugin --- plugins/jwt/jwt.go | 135 +++++++++++++++++++++++++++++++++++ plugins/jwt/jwt_test.go | 88 +++++++++++++++++++++++ plugins/jwt/test/jwt.rsa | 15 ++++ plugins/jwt/test/jwt.rsa.pub | 6 ++ 4 files changed, 244 insertions(+) create mode 100644 plugins/jwt/jwt.go create mode 100644 plugins/jwt/jwt_test.go create mode 100644 plugins/jwt/test/jwt.rsa create mode 100644 plugins/jwt/test/jwt.rsa.pub diff --git a/plugins/jwt/jwt.go b/plugins/jwt/jwt.go new file mode 100644 index 00000000..fd064686 --- /dev/null +++ b/plugins/jwt/jwt.go @@ -0,0 +1,135 @@ +// Copyright 2014 beego Author. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package jwt provides JWT (Json Web Token) authentication +// +// Usage +// In file main.go +// +// import ( +// "github.com/astaxie/beego" +// "github.com/astaxie/beego/plugins/jwt" +// ) +// +// func main() { +// // JWT for Url matching /v1/* +// // PrivateKeyPath: The path for the private RSA key used by JWT +// // PublicKeyPath: The path for the public RSA key used by JWT +// // The list of Urls should be excluded from the JWT Auth +// beego.InsertFilter("/v1/*", beego.BeforeRouter, jwt.AuthRequest(&jwt.Options{ +// PrivateKeyPath: "conf/beeblog.rsa", +// PublicKeyPath: "conf/beeblog.rsa.pub", +// WhiteList: []string{"/v1/jwt/issue-token", "/docs"}, +// })) +// beego.Run() +// } +// +// In file routers/router.go +// +// import ( +// "github.com/astaxie/beego" +// "github.com/astaxie/beego/plugins/jwt" +// ) +// func init() { +// ns := beego.NSNamespace("/jwt", +// beego.NSInclude( +// &jwt.JwtController{}, +// ), +// ) +// beego.AddNamespace(ns) +// } +// + +package jwt + +import ( + "github.com/astaxie/beego" + "github.com/astaxie/beego/context" + "github.com/astaxie/beego/logs" + goJwt "github.com/dgrijalva/jwt-go" + "io/ioutil" + "net/http" + "time" +) + +// Options for the JWT Auth +type Options struct { + PrivateKeyPath string + PublicKeyPath string + WhiteList []string +} + +var RSAKeys struct { + PrivateKey []byte + PublicKey []byte +} + +func AuthRequest(o *Options) beego.FilterFunc { + RSAKeys.PrivateKey, _ = ioutil.ReadFile(o.PrivateKeyPath) + RSAKeys.PublicKey, _ = ioutil.ReadFile(o.PublicKeyPath) + + return func(ctx *context.Context) { + // :TODO the url patterns should be considered here. + // Shouldn't only use the string equal + for _, method := range o.WhiteList { + if method == ctx.Request.URL.Path { + return + } + } + + parsedToken, err := goJwt.ParseFromRequest(ctx.Request, func(t *goJwt.Token) (interface{}, error) { + return RSAKeys.PublicKey, nil + }) + + if err == nil && parsedToken.Valid { + ctx.Output.SetStatus(http.StatusOK) + } else { + ctx.Output.SetStatus(http.StatusUnauthorized) + } + + } +} + +// oprations for Jwt +type JwtController struct { + beego.Controller +} + +func (this *JwtController) URLMapping() { + this.Mapping("IssueToken", this.IssueToken) +} + +// @Title IssueToken +// @Description Issue a Json Web Token +// @Success 200 string +// @Failure 403 no privilege to access +// @Failure 500 server inner error +// @router /issue-token [get] +func (this *JwtController) IssueToken() { + this.Data["json"] = CreateToken() + this.ServeJson() +} + +func CreateToken() map[string]string { + log := logs.NewLogger(10000) + log.SetLogger("console", "") + + token := goJwt.New(goJwt.GetSigningMethod("RS256")) // Create a Token that will be signed with RSA 256. + token.Claims["ID"] = "This is my super fake ID" + token.Claims["exp"] = time.Now().Unix() + 36000 + // The claims object allows you to store information in the actual token. + tokenString, _ := token.SignedString(RSAKeys.PrivateKey) + // tokenString Contains the actual token you should share with your client. + return map[string]string{"token": tokenString} +} diff --git a/plugins/jwt/jwt_test.go b/plugins/jwt/jwt_test.go new file mode 100644 index 00000000..dd8cafe7 --- /dev/null +++ b/plugins/jwt/jwt_test.go @@ -0,0 +1,88 @@ +package jwt + +import ( + "github.com/astaxie/beego" + "net/http" + "net/http/httptest" + "testing" +) + +func testRequest(method, path string) (*httptest.ResponseRecorder, *http.Request) { + request, _ := http.NewRequest(method, path, nil) + recorder := httptest.NewRecorder() + + return recorder, request +} + +func Test_IssueTokenAction(t *testing.T) { + url := "/v1/jwt/issue-token" + + mux := beego.NewControllerRegister() + + mux.InsertFilter("*", beego.BeforeRouter, AuthRequest(&Options{ + PrivateKeyPath: "test/jwt.rsa", + PublicKeyPath: "test/jwt.rsa.pub", + WhiteList: []string{"/v1/jwt/issue-token", "/docs"}, + })) + + mux.Add("/v1/jwt/issue-token", &JwtController{}, "get:IssueToken") + + rw, r := testRequest("GET", url) + mux.ServeHTTP(rw, r) + + if rw.Code != http.StatusOK { + t.Errorf("Shoud return 200") + } +} + +func (tc *JwtController) Foo() { + tc.Ctx.Output.Body([]byte("ok")) +} + +func Test_AuthRequestWithAuthorizationHeader(t *testing.T) { + + url := "/foo" + + mux := beego.NewControllerRegister() + + mux.InsertFilter("*", beego.BeforeRouter, AuthRequest(&Options{ + PrivateKeyPath: "test/jwt.rsa", + PublicKeyPath: "test/jwt.rsa.pub", + WhiteList: []string{"/v1/jwt/issue-token", "/docs"}, + })) + + mux.Add("/foo", &JwtController{}, "get:Foo") + newToken := CreateToken() + + rw, r := testRequest("GET", url) + r.Header.Add("Authorization", "Bearer "+newToken["token"]) + mux.ServeHTTP(rw, r) + + if rw.Code != http.StatusOK { + t.Errorf("Shoud return 200") + } + if rw.Body.String() != "ok" { + t.Errorf("Should output ok") + } +} + +func Test_AuthRequestWithoutAuthorizationHeader(t *testing.T) { + url := "/foo" + + mux := beego.NewControllerRegister() + + mux.InsertFilter("*", beego.BeforeRouter, AuthRequest(&Options{ + PrivateKeyPath: "test/jwt.rsa", + PublicKeyPath: "test/jwt.rsa.pub", + WhiteList: []string{"/v1/jwt/issue-token", "/docs"}, + })) + + mux.Add("/foo", &JwtController{}, "get:Foo") + + rw, r := testRequest("GET", url) + mux.ServeHTTP(rw, r) + + if rw.Code != http.StatusUnauthorized { + t.Errorf("Shoud return 401") + } +} diff --git a/plugins/jwt/test/jwt.rsa b/plugins/jwt/test/jwt.rsa new file mode 100644 index 00000000..8d584fc6 --- /dev/null +++ b/plugins/jwt/test/jwt.rsa @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCdu+23Y/0J/FTQKnIPnxupoOo9/OYCv90DPXN/KLLRAMjYzgcC +DsBST2xVR5jlimI/gyfCpVB62dwpSzzr0cA3MoDhbaGWuTdQUX9zmiLoQ4I7X6h0 +dwyiihOz+CzOMlAg5+qBhiTGcKvIFlfEc1FUcn/tB3PVRG9j6B1Ibz5CnQIDAQAB +AoGAFGg+/i4ai9MwqeoD7c95Bb5C8BgrLgnir0uhCL+cOvwuABbPw01jRoLuEi58 +Mp5vzaXLXByFSA+ts03/qMbvZkDGac5g5kLli5TjHIONMxVBrdfGQ1+OApnaPayN +N+HYjZKs6xao6J5iFqfA0FqzDR9kQhUoeosdQoo1GlxDckECQQDO/0LJrFiLzYWe +qS/DxfAnFu2BlClKZjxRJ3vIkRRaON6HPl8BeJW901bFKG5+WSfO+OwQ9egWaf3X +fFm/oEHRAkEAwxMor4fOkBZbL4KPW7sen169vwnXuYusqj0t3dIeiIVrCigkOMT4 +OvX/63u4CTdXh1D5u/4Z/1HTYH92VCP7DQJAJPxbNKnE0IYSf/z++d4eQP3JxkNw +9Ug7Msz5QycZGd3bdRLh6uNe7iIa+PN2esD3afX0SDuIEqkxoBUp/CFoYQJAUmi3 +mV+/7bLkFrALK+9iwmTdt+TKk4HkEY8C32CysW3biFDo7GqZix79XFfJqWsNuQaG +WdrA1NGWgH+YV3dTyQJAIWEZGAuUXRkQB20LfjGzpsKgQFbqjTisMS0qe3JjnDwu +0JR8sYXapgeEEEisH+OtkZKIfyeFOwoUyNC83bcvgw== +-----END RSA PRIVATE KEY----- diff --git a/plugins/jwt/test/jwt.rsa.pub b/plugins/jwt/test/jwt.rsa.pub new file mode 100644 index 00000000..3fdd8ce7 --- /dev/null +++ b/plugins/jwt/test/jwt.rsa.pub @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCdu+23Y/0J/FTQKnIPnxupoOo9 +/OYCv90DPXN/KLLRAMjYzgcCDsBST2xVR5jlimI/gyfCpVB62dwpSzzr0cA3MoDh +baGWuTdQUX9zmiLoQ4I7X6h0dwyiihOz+CzOMlAg5+qBhiTGcKvIFlfEc1FUcn/t +B3PVRG9j6B1Ibz5CnQIDAQAB +-----END PUBLIC KEY-----