From 1fb84a3ff4280f7b4e38f88f234f30704379e77a Mon Sep 17 00:00:00 2001 From: Muyao CHEN Date: Tue, 15 Oct 2024 10:14:40 +0200 Subject: [PATCH] feat: implement session delete using redis --- .gitignore | 2 +- configs/howmuch.yml | 4 + go.mod | 3 + go.sum | 10 ++ .../howmuch/adapter/controller/session.go | 34 ++++- .../adapter/controller/session_test.go | 2 +- internal/howmuch/howmuch.go | 13 +- internal/howmuch/infra/datastore/cache.go | 66 ++++++++++ internal/howmuch/infra/router/router.go | 20 ++- internal/howmuch/registry/registry.go | 8 +- internal/howmuch/registry/session.go | 2 +- internal/pkg/core/cache.go | 34 +++++ internal/pkg/middleware/{ => authn}/authn.go | 27 +++- internal/pkg/middleware/authn/authn_test.go | 117 ++++++++++++++++++ internal/pkg/shared/const.go | 22 ++++ internal/pkg/token/token.go | 2 + 16 files changed, 350 insertions(+), 16 deletions(-) create mode 100644 internal/howmuch/infra/datastore/cache.go create mode 100644 internal/pkg/core/cache.go rename internal/pkg/middleware/{ => authn}/authn.go (76%) create mode 100644 internal/pkg/middleware/authn/authn_test.go diff --git a/.gitignore b/.gitignore index b0a6f1d..8d9e283 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,7 @@ go.work.sum # Custom /_output -/deployment/db_data +/deployment/tmp /tmp/** # Vue diff --git a/configs/howmuch.yml b/configs/howmuch.yml index 373c208..45abff2 100644 --- a/configs/howmuch.yml +++ b/configs/howmuch.yml @@ -16,6 +16,10 @@ db: # DB name database: howmuch +cache: + host: 127.0.0.1:6379 + password: "" + log: level: debug disalbe-caller: false diff --git a/go.mod b/go.mod index 99fa2d5..dd6d615 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gosuri/uitable v0.0.4 github.com/jackc/pgx/v5 v5.7.1 github.com/pkg/errors v0.9.1 + github.com/redis/go-redis/v9 v9.6.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 @@ -23,9 +24,11 @@ require ( require ( github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/color v1.14.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect diff --git a/go.sum b/go.sum index 78d74f8..afe6c84 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -11,6 +17,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -91,6 +99,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= diff --git a/internal/howmuch/adapter/controller/session.go b/internal/howmuch/adapter/controller/session.go index 7392e01..5008166 100644 --- a/internal/howmuch/adapter/controller/session.go +++ b/internal/howmuch/adapter/controller/session.go @@ -23,25 +23,31 @@ package controller import ( + "fmt" + "time" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware/authn" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/token" "github.com/gin-gonic/gin" ) type Session interface { Create(*gin.Context) + Delete(*gin.Context) } type SessionController struct { userUsecase usecase.User + cache core.Cache } -func NewSessionController(u usecase.User) Session { - return &SessionController{u} +func NewSessionController(u usecase.User, cache core.Cache) Session { + return &SessionController{u, cache} } type Token struct { @@ -88,3 +94,27 @@ func (sc *SessionController) Create(ctx *gin.Context) { Token: tokenString, }) } + +// Delete deletes a session by putting the jwt token into the cache +func (sc *SessionController) Delete(ctx *gin.Context) { + tk, err := token.ParseRequest(ctx) + if err != nil || tk == nil { + // Unlikely + core.WriteResponse(ctx, authn.ErrTokenInvalid, nil) + return + } + + exp := time.Until(tk.Expiry) + key := fmt.Sprintf("jwt:%s", tk.Identity) + + log.DebugLog("session delete", "key", key, "exp", exp.String()) + err = sc.cache.Set(ctx, key, tk.Raw, exp) + if err != nil { + // unexpected + log.ErrorLog("error writing logged out jwt into cache", "err", err) + core.WriteResponse(ctx, errno.InternalServerErr, nil) + return + } + + core.WriteResponse(ctx, nil, "logged out") +} diff --git a/internal/howmuch/adapter/controller/session_test.go b/internal/howmuch/adapter/controller/session_test.go index 564ebca..4d14d52 100644 --- a/internal/howmuch/adapter/controller/session_test.go +++ b/internal/howmuch/adapter/controller/session_test.go @@ -59,7 +59,7 @@ func TestSessionCreate(t *testing.T) { for _, tst := range tests { t.Run(tst.Name, func(t *testing.T) { testUserUsecase := usecasemock.NewtestUserUsecase() - sessionController := NewSessionController(testUserUsecase) + sessionController := NewSessionController(testUserUsecase, nil) r := gin.New() r.POST( "/session/create", diff --git a/internal/howmuch/howmuch.go b/internal/howmuch/howmuch.go index 6376cf1..e4d6a86 100644 --- a/internal/howmuch/howmuch.go +++ b/internal/howmuch/howmuch.go @@ -39,6 +39,7 @@ import ( "git.vinchent.xyz/vinchent/howmuch/pkg/version/verflag" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5" + "github.com/redis/go-redis/v9" "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/net/context" @@ -126,15 +127,23 @@ func run() error { } defer dbConn.Close(context.Background()) + // Init Cache + cache := datastore.NewCache(&redis.Options{ + Addr: viper.GetString("cache.host"), + Password: viper.GetString("cache.password"), + DB: 0, + }) + defer cache.Close() + // Init token token.Init(viper.GetString("web.token-secret"), viper.GetDuration("web.token-expiry-time")) // Register the core service - r := registry.NewRegistry(dbConn) + r := registry.NewRegistry(dbConn, cache) engine := gin.Default() - engine = router.Routes(engine, r.NewAppController()) + engine = router.Routes(engine, r.NewAppController(), cache) server := http.Server{ Addr: viper.GetString("web.addr"), diff --git a/internal/howmuch/infra/datastore/cache.go b/internal/howmuch/infra/datastore/cache.go new file mode 100644 index 0000000..6d3c4ad --- /dev/null +++ b/internal/howmuch/infra/datastore/cache.go @@ -0,0 +1,66 @@ +// MIT License +// +// Copyright (c) 2024 vinchent +// +// 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. + +package datastore + +import ( + "context" + "time" + + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" + "github.com/redis/go-redis/v9" +) + +type RedisCache struct { + redis *redis.Client +} + +func NewCache(opt interface{}) core.Cache { + redisOpt := opt.(*redis.Options) + return &RedisCache{redis.NewClient(redisOpt)} +} + +func (c *RedisCache) Get(ctx context.Context, key string) (string, error) { + val, err := c.redis.Get(ctx, key).Result() + if err == redis.Nil { + log.DebugLog("redis key not found", "key", key) + return "", nil + } else if err != nil { + log.DebugLog("redis cache get error", "err", err) + return "", err + } + return val, nil +} + +func (c *RedisCache) Set( + ctx context.Context, + key string, + value interface{}, + expiration time.Duration, +) error { + return c.redis.Set(ctx, key, value, expiration).Err() +} + +func (c *RedisCache) Close() error { + return c.redis.Close() +} diff --git a/internal/howmuch/infra/router/router.go b/internal/howmuch/infra/router/router.go index 41ec1c5..1a3a21d 100644 --- a/internal/howmuch/infra/router/router.go +++ b/internal/howmuch/infra/router/router.go @@ -29,11 +29,19 @@ import ( "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware/authn" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) -func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine { +// Routes can take some options to init middlewares. +// - Cache +func Routes(engine *gin.Engine, c controller.AppController, opt ...interface{}) *gin.Engine { + cache, ok := opt[0].(core.Cache) + if !ok { + panic("the first option must be a cache driver") + } + // Middlewares // Cors corsCfg := cors.DefaultConfig() @@ -56,14 +64,20 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine { { userV1.POST("/create", func(ctx *gin.Context) { c.User.Create(ctx) }) - userV1.Use(middleware.Authn()) + userV1.Use(authn.Authn(cache)) userV1.GET( ":id/info", func(ctx *gin.Context) { ctx.JSON(http.StatusOK, "Hello world") }, ) } - v1.POST("/session/create", func(ctx *gin.Context) { c.Session.Create(ctx) }) + sessionV1 := v1.Group("/session") + { + sessionV1.POST("/create", func(ctx *gin.Context) { c.Session.Create(ctx) }) + sessionV1.Use(authn.Authn(cache)) + sessionV1.POST("/delete", func(ctx *gin.Context) { c.Session.Delete(ctx) }) + } + } return engine diff --git a/internal/howmuch/registry/registry.go b/internal/howmuch/registry/registry.go index 3b702da..98ed3fe 100644 --- a/internal/howmuch/registry/registry.go +++ b/internal/howmuch/registry/registry.go @@ -24,6 +24,7 @@ package registry import ( "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" "github.com/jackc/pgx/v5" ) @@ -32,7 +33,8 @@ import ( // It might holds other drivers when the projects grows. For example // the object needed to connect to Redis or Kafka. type registry struct { - db *pgx.Conn + db *pgx.Conn + cache core.Cache } // Registry returns a new app controller that will be used by main()/run() @@ -43,8 +45,8 @@ type Registry interface { } // NewRegistry returns a new Registry's implementation. -func NewRegistry(db *pgx.Conn) Registry { - return ®istry{db: db} +func NewRegistry(db *pgx.Conn, cache core.Cache) Registry { + return ®istry{db: db, cache: cache} } // NewAppController creates a new AppController with controller struct for diff --git a/internal/howmuch/registry/session.go b/internal/howmuch/registry/session.go index 4e10f2b..20717fd 100644 --- a/internal/howmuch/registry/session.go +++ b/internal/howmuch/registry/session.go @@ -31,5 +31,5 @@ import ( // NewSessionController returns a session controller's implementation func (r *registry) NewSessionController() controller.Session { u := usecase.NewUserUsecase(repo.NewUserRepository(r.db), repo.NewDBRepository(r.db)) - return controller.NewSessionController(u) + return controller.NewSessionController(u, r.cache) } diff --git a/internal/pkg/core/cache.go b/internal/pkg/core/cache.go new file mode 100644 index 0000000..c75f04b --- /dev/null +++ b/internal/pkg/core/cache.go @@ -0,0 +1,34 @@ +// MIT License +// +// Copyright (c) 2024 vinchent +// +// 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. + +package core + +import ( + "context" + "time" +) + +type Cache interface { + Get(ctx context.Context, key string) (string, error) + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error + Close() error +} diff --git a/internal/pkg/middleware/authn.go b/internal/pkg/middleware/authn/authn.go similarity index 76% rename from internal/pkg/middleware/authn.go rename to internal/pkg/middleware/authn/authn.go index 539c76a..63b9ef9 100644 --- a/internal/pkg/middleware/authn.go +++ b/internal/pkg/middleware/authn/authn.go @@ -20,13 +20,15 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -package middleware +package authn import ( + "fmt" "net/http" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/log" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/token" "github.com/gin-gonic/gin" @@ -38,8 +40,14 @@ var ErrTokenInvalid = &errno.Errno{ Message: "invalid token", } +var ErrLoggedOut = &errno.Errno{ + HTTP: http.StatusUnauthorized, + Code: errno.ErrorCode(errno.AuthFailureCode, "LoggedOut"), + Message: "logged out, please log in", +} + // Authn authenticates a user's access by validating their token. -func Authn() gin.HandlerFunc { +func Authn(cache core.Cache) gin.HandlerFunc { return func(ctx *gin.Context) { tk, err := token.ParseRequest(ctx) if err != nil || tk == nil { @@ -48,7 +56,20 @@ func Authn() gin.HandlerFunc { return } - // TODO: check if the key is on logout blacklist. + key := fmt.Sprintf("jwt:%s", tk.Identity) + + val, err := cache.Get(ctx, key) + if err != nil { + log.ErrorLog("cache get token", "err", err) + ctx.Abort() + return + } + if val != "" { + // blacklist + core.WriteResponse(ctx, ErrLoggedOut, nil) + ctx.Abort() + return + } ctx.Header(shared.XUserName, tk.Identity) ctx.Next() diff --git a/internal/pkg/middleware/authn/authn_test.go b/internal/pkg/middleware/authn/authn_test.go new file mode 100644 index 0000000..d2205d3 --- /dev/null +++ b/internal/pkg/middleware/authn/authn_test.go @@ -0,0 +1,117 @@ +// MIT License +// +// Copyright (c) 2024 vinchent +// +// 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. + +package authn + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/shared" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/test" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/token" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +var loggedOutID string + +type testCache struct{} + +func (tc *testCache) Get(ctx context.Context, key string) (string, error) { + loggedOutKey := fmt.Sprintf("jwt:%s", loggedOutID) + if key == loggedOutKey { + return "found", nil + } + return "", nil +} + +func (c *testCache) Set( + ctx context.Context, + key string, + value interface{}, + expiration time.Duration, +) error { + return nil +} + +func (c *testCache) Close() error { + return nil +} + +func TestAuthn(t *testing.T) { + token.Init("secret", 1*time.Second) + tk, _ := token.Sign("user") + + tkParsed, _ := token.Parse(tk) + loggedOutID = tkParsed.Identity + + cache := &testCache{} + + t.Run("token found in cache", func(t *testing.T) { + r := gin.New() + r.Use(Authn(cache)) + + r.GET("/example", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + res := test.PerformRequest( + t, + r, + "GET", + "/example", + nil, + test.Header{Key: shared.XUserName, Value: "user"}, + test.Header{Key: "Authorization", Value: fmt.Sprintf("Bearer %s", tk)}, + ) + assert.Equal(t, http.StatusUnauthorized, res.Result().StatusCode, res.Body) + + var err errno.Errno + json.NewDecoder(res.Result().Body).Decode(&err) + assert.Equal(t, "AuthFailure.LoggedOut", err.Code) + }) + + t.Run("token not found in cache", func(t *testing.T) { + newTk, _ := token.Sign("user2") + r := gin.New() + r.Use(Authn(cache)) + + r.GET("/example", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + res := test.PerformRequest( + t, + r, + "GET", + "/example", + nil, + test.Header{Key: shared.XUserName, Value: "user2"}, + test.Header{Key: "Authorization", Value: fmt.Sprintf("Bearer %s", newTk)}, + ) + assert.Equal(t, http.StatusOK, res.Result().StatusCode, res.Body) + }) +} diff --git a/internal/pkg/shared/const.go b/internal/pkg/shared/const.go index bdb9d3d..1147b2c 100644 --- a/internal/pkg/shared/const.go +++ b/internal/pkg/shared/const.go @@ -1,3 +1,25 @@ +// MIT License +// +// Copyright (c) 2024 vinchent +// +// 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. + package shared const ( diff --git a/internal/pkg/token/token.go b/internal/pkg/token/token.go index eb97516..e2cba2c 100644 --- a/internal/pkg/token/token.go +++ b/internal/pkg/token/token.go @@ -43,6 +43,7 @@ type Claims struct { } type TokenResp struct { + Raw string Identity string Expiry time.Time } @@ -91,6 +92,7 @@ func Parse(tokenString string) (*TokenResp, error) { if claims, ok := token.Claims.(*Claims); ok { return &TokenResp{ + tokenString, claims.IdentityKey, claims.ExpiresAt.Time, }, nil