Skip to content

Commit

Permalink
chore: move the authentication middleware into droplet framework (#1296)
Browse files Browse the repository at this point in the history
* chore: move the authentication middleware into droplet framework (#1295)

* fix: ci test

* fix: code style

* update: panic if can't get http.Request

* fix: fix misspell

* fix: fix ci

* fix: fix ci
  • Loading branch information
starsz authored Jan 15, 2021
1 parent a1f9730 commit d224367
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 55 deletions.
8 changes: 5 additions & 3 deletions api/cmd/managerapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ import (
"syscall"
"time"

"github.com/shiningrush/droplet"
"github.com/spf13/cobra"

"github.com/apisix/manager-api/internal"
"github.com/apisix/manager-api/internal/conf"
"github.com/apisix/manager-api/internal/core/storage"
"github.com/apisix/manager-api/internal/core/store"
"github.com/apisix/manager-api/internal/filter"
"github.com/apisix/manager-api/internal/handler"
"github.com/apisix/manager-api/internal/log"
"github.com/apisix/manager-api/internal/utils"
"github.com/shiningrush/droplet"
"github.com/spf13/cobra"
)

var (
Expand Down Expand Up @@ -62,7 +64,7 @@ func NewManagerAPICommand() *cobra.Command {
var newMws []droplet.Middleware
// default middleware order: resp_reshape, auto_input, traffic_log
// We should put err_transform at second to catch all error
newMws = append(newMws, mws[0], &handler.ErrorTransformMiddleware{})
newMws = append(newMws, mws[0], &handler.ErrorTransformMiddleware{}, &filter.AuthenticationMiddleware{})
newMws = append(newMws, mws[1:]...)
return newMws
}
Expand Down
4 changes: 2 additions & 2 deletions api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ require (
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/prometheus/client_golang v1.8.0 // indirect
github.com/satori/go.uuid v1.2.0
github.com/shiningrush/droplet v0.2.3
github.com/shiningrush/droplet/wrapper/gin v0.2.0
github.com/shiningrush/droplet v0.2.4
github.com/shiningrush/droplet/wrapper/gin v0.2.1
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/sony/sonyflake v1.0.0
github.com/spf13/cobra v0.0.3
Expand Down
4 changes: 4 additions & 0 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,12 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
github.com/shiningrush/droplet v0.0.0-20191118073048-00b06fe19ce4/go.mod h1:E/th13n/wtPi+Cj2f0hAAEFeT3gb5xsS6Ob4WRrdxdM=
github.com/shiningrush/droplet v0.2.3 h1:bzPDzkE0F54r94XsultGS8uAPeL3pZIRmjqM0zIlpeI=
github.com/shiningrush/droplet v0.2.3/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M=
github.com/shiningrush/droplet v0.2.4 h1:OW4Pp+dXs9O61QKTiYSRWCdQeOyzO1n9h+i2PDJ5DK0=
github.com/shiningrush/droplet v0.2.4/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M=
github.com/shiningrush/droplet/wrapper/gin v0.2.0 h1:LHkU+TbSkpePgXrTg3hJoSZlCMS03GeWMl0t+oLkd44=
github.com/shiningrush/droplet/wrapper/gin v0.2.0/go.mod h1:ZJu+sCRrVXn5Pg618c1KK3Ob2UiXGuPM1ROx5uMM9YQ=
github.com/shiningrush/droplet/wrapper/gin v0.2.1 h1:1o+5KUF2sKsdZ7SkmOC5ahAP1qaZKqnm0c5hOYFV6YQ=
github.com/shiningrush/droplet/wrapper/gin v0.2.1/go.mod h1:cx5BfLuStFDFIKuEOc1zBTpiT3B4Ezkg3MdlP6rW51I=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
Expand Down
110 changes: 63 additions & 47 deletions api/internal/filter/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,62 +17,78 @@
package filter

import (
"errors"
"net/http"
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/shiningrush/droplet"
"github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/middleware"

"github.com/apisix/manager-api/internal/conf"
"github.com/apisix/manager-api/internal/log"
)

func Authentication() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path != "/apisix/admin/user/login" && strings.HasPrefix(c.Request.URL.Path, "/apisix") {
tokenStr := c.GetHeader("Authorization")

// verify token
token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(conf.AuthConf.Secret), nil
})

errResp := gin.H{
"code": 010013,
"message": "Request Unauthorized",
}

if err != nil || token == nil || !token.Valid {
log.Warnf("token validate failed: %s", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, errResp)
return
}

claims, ok := token.Claims.(*jwt.StandardClaims)
if !ok {
log.Warnf("token validate failed: %s, %v", err, token.Valid)
c.AbortWithStatusJSON(http.StatusUnauthorized, errResp)
return
}

if err := token.Claims.Valid(); err != nil {
log.Warnf("token claims validate failed: %s", err)
c.AbortWithStatusJSON(http.StatusUnauthorized, errResp)
return
}

if claims.Subject == "" {
log.Warn("token claims subject empty")
c.AbortWithStatusJSON(http.StatusUnauthorized, errResp)
return
}

if _, ok := conf.UserList[claims.Subject]; !ok {
log.Warnf("user not exists by token claims subject %s", claims.Subject)
c.AbortWithStatusJSON(http.StatusUnauthorized, errResp)
return
}
type AuthenticationMiddleware struct {
middleware.BaseMiddleware
}

func (mw *AuthenticationMiddleware) Handle(ctx droplet.Context) error {
httpReq := ctx.Get(middleware.KeyHttpRequest)
if httpReq == nil {
err := errors.New("input middleware cannot get http request")

// Wrong usage, just panic here and let recoverHandler to deal with
panic(err)
}

req := httpReq.(*http.Request)

if req.URL.Path != "/apisix/admin/user/login" && strings.HasPrefix(req.URL.Path, "/apisix") {
tokenStr := req.Header.Get("Authorization")

// verify token
token, err := jwt.ParseWithClaims(tokenStr, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(conf.AuthConf.Secret), nil
})

// TODO: design the response error code
response := data.Response{Code: 010013, Message: "request unauthorized"}

if err != nil || token == nil || !token.Valid {
log.Warnf("token validate failed: %s", err)
ctx.SetOutput(&data.SpecCodeResponse{StatusCode: http.StatusUnauthorized, Response: response})
return nil
}
c.Next()

claims, ok := token.Claims.(*jwt.StandardClaims)
if !ok {
log.Warnf("token validate failed: %s, %v", err, token.Valid)
ctx.SetOutput(&data.SpecCodeResponse{StatusCode: http.StatusUnauthorized, Response: response})
return nil
}

if err := token.Claims.Valid(); err != nil {
log.Warnf("token claims validate failed: %s", err)
ctx.SetOutput(&data.SpecCodeResponse{StatusCode: http.StatusUnauthorized, Response: response})
return nil
}

if claims.Subject == "" {
log.Warn("token claims subject empty")
ctx.SetOutput(&data.SpecCodeResponse{StatusCode: http.StatusUnauthorized, Response: response})
return nil
}

if _, ok := conf.UserList[claims.Subject]; !ok {
log.Warnf("user not exists by token claims subject %s", claims.Subject)
ctx.SetOutput(&data.SpecCodeResponse{StatusCode: http.StatusUnauthorized, Response: response})
return nil
}

return mw.BaseMiddleware.Handle(ctx)
}

return mw.BaseMiddleware.Handle(ctx)
}
116 changes: 116 additions & 0 deletions api/internal/filter/authentication_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 filter

import (
"errors"
"net/http"
"net/url"
"testing"
"time"

"github.com/dgrijalva/jwt-go"
"github.com/shiningrush/droplet"
"github.com/shiningrush/droplet/data"
"github.com/shiningrush/droplet/middleware"
"github.com/stretchr/testify/assert"

"github.com/apisix/manager-api/internal/conf"
)

func genToken(username string, issueAt, expireAt int64) string {
claims := jwt.StandardClaims{
Subject: username,
IssuedAt: issueAt,
ExpiresAt: expireAt,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, _ := token.SignedString([]byte(conf.AuthConf.Secret))

return signedToken
}

type mockMiddleware struct {
middleware.BaseMiddleware
}

func (mw *mockMiddleware) Handle(ctx droplet.Context) error {
return errors.New("next middleware")
}

func testPanic(t *testing.T, mw AuthenticationMiddleware, ctx droplet.Context) {
defer func() {
panicErr := recover()
assert.Contains(t, panicErr.(error).Error(), "input middleware cannot get http request")
}()
_ = mw.Handle(ctx)
}

func TestAuthenticationMiddleware_Handle(t *testing.T) {
ctx := droplet.NewContext()
fakeReq, _ := http.NewRequest(http.MethodGet, "", nil)
expectOutput := &data.SpecCodeResponse{
Response: data.Response{
Code: 010013,
Message: "request unauthorized",
},
StatusCode: http.StatusUnauthorized,
}

mw := AuthenticationMiddleware{}
mockMw := mockMiddleware{}
mw.SetNext(&mockMw)

// test without http.Request
testPanic(t, mw, ctx)

ctx.Set(middleware.KeyHttpRequest, fakeReq)

// test without token check
fakeReq.URL = &url.URL{Path: "/apisix/admin/user/login"}
assert.Equal(t, mw.Handle(ctx), errors.New("next middleware"))

// test without authorization header
fakeReq.URL = &url.URL{Path: "/apisix/admin/routes"}
assert.Nil(t, mw.Handle(ctx))
assert.Equal(t, expectOutput, ctx.Output().(*data.SpecCodeResponse))

// test with token expire
expireToken := genToken("admin", time.Now().Unix(), time.Now().Unix()-60*3600)
fakeReq.Header.Set("Authorization", expireToken)
assert.Nil(t, mw.Handle(ctx))
assert.Equal(t, expectOutput, ctx.Output().(*data.SpecCodeResponse))

// test with temp subject
tempSubjectToken := genToken("", time.Now().Unix(), time.Now().Unix()+60*3600)
fakeReq.Header.Set("Authorization", tempSubjectToken)
assert.Nil(t, mw.Handle(ctx))
assert.Equal(t, expectOutput, ctx.Output().(*data.SpecCodeResponse))

// test username doesn't exist
userToken := genToken("user1", time.Now().Unix(), time.Now().Unix()+60*3600)
fakeReq.Header.Set("Authorization", userToken)
assert.Nil(t, mw.Handle(ctx))
assert.Equal(t, expectOutput, ctx.Output().(*data.SpecCodeResponse))

// test auth success
adminToken := genToken("admin", time.Now().Unix(), time.Now().Unix()+60*3600)
fakeReq.Header.Set("Authorization", adminToken)
ctx.SetOutput("test data")
assert.Equal(t, mw.Handle(ctx), errors.New("next middleware"))
assert.Equal(t, "test data", ctx.Output().(string))
}
2 changes: 1 addition & 1 deletion api/internal/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func SetUpRouter() *gin.Engine {
logger := log.GetLogger(log.AccessLog)
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("session", store))
r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.Authentication(), filter.RecoverHandler())
r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.RecoverHandler())
r.Use(static.Serve("/", static.LocalFile(filepath.Join(conf.WorkDir, conf.WebDir), false)))
r.NoRoute(func(c *gin.Context) {
c.File(fmt.Sprintf("%s/index.html", filepath.Join(conf.WorkDir, conf.WebDir)))
Expand Down
4 changes: 2 additions & 2 deletions api/test/e2e/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ func TestAuthentication_token(t *testing.T) {
Path: "/apisix/admin/routes",
Headers: map[string]string{"Authorization": "Not-A-Valid-Token"},
ExpectStatus: http.StatusUnauthorized,
ExpectBody: "\"message\":\"Request Unauthorized\"",
ExpectBody: "\"message\":\"request unauthorized\"",
},
{
Desc: "Access without authentication token",
Object: ManagerApiExpect(t),
Method: http.MethodGet,
Path: "/apisix/admin/routes",
ExpectStatus: http.StatusUnauthorized,
ExpectBody: "\"message\":\"Request Unauthorized\"",
ExpectBody: "\"message\":\"request unauthorized\"",
},
}

Expand Down

0 comments on commit d224367

Please sign in to comment.