feat: implement session delete using redis
This commit is contained in:
		@ -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()
 | 
			
		||||
							
								
								
									
										117
									
								
								internal/pkg/middleware/authn/authn_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								internal/pkg/middleware/authn/authn_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,117 @@
 | 
			
		||||
// MIT License
 | 
			
		||||
//
 | 
			
		||||
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
 | 
			
		||||
//
 | 
			
		||||
// 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)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user