From 798b9a7695ab7ffe8396f0796b64bbd7f20fd96e Mon Sep 17 00:00:00 2001 From: Muyao CHEN Date: Sun, 13 Oct 2024 13:21:09 +0200 Subject: [PATCH] feat: Add jwt token pkg --- go.mod | 1 + go.sum | 2 + internal/pkg/middleware/authn.go | 56 +++++++++++++++ internal/pkg/token/token.go | 115 +++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 internal/pkg/middleware/authn.go create mode 100644 internal/pkg/token/token.go diff --git a/go.mod b/go.mod index ca7de21..99fa2d5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 + github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gosuri/uitable v0.0.4 github.com/jackc/pgx/v5 v5.7.1 diff --git a/go.sum b/go.sum index 71b959f..78d74f8 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= diff --git a/internal/pkg/middleware/authn.go b/internal/pkg/middleware/authn.go new file mode 100644 index 0000000..6605759 --- /dev/null +++ b/internal/pkg/middleware/authn.go @@ -0,0 +1,56 @@ +// 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 middleware + +import ( + "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/token" + "github.com/gin-gonic/gin" +) + +var ErrTokenInvalid = &errno.Errno{ + HTTP: http.StatusUnauthorized, + Code: errno.ErrorCode(errno.AuthFailureCode, "TokenInvalid"), + Message: "invalid token", +} + +const XUserName = "X-Username" + +// Authn authenticates a user's access by validating their token. +func Authn() gin.HandlerFunc { + return func(ctx *gin.Context) { + tk, err := token.ParseRequest(ctx) + if err != nil || tk == nil { + core.WriteResponse(ctx, ErrTokenInvalid, nil) + ctx.Abort() + } + + // TODO: check if the key is on logout blacklist. + + ctx.Header(XUserName, tk.Identity) + ctx.Next() + } +} diff --git a/internal/pkg/token/token.go b/internal/pkg/token/token.go new file mode 100644 index 0000000..ecc47da --- /dev/null +++ b/internal/pkg/token/token.go @@ -0,0 +1,115 @@ +// 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 token + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" +) + +type Config struct { + secretKey string + expiryTime time.Duration +} + +type Claims struct { + IdentityKey string `json:"identity_key"` + jwt.RegisteredClaims +} + +type TokenResp struct { + Identity string + Expiry time.Time +} + +var ( + once sync.Once + config *Config +) + +var ErrMissingHeader = errors.New("Authorization is needed in the header") + +func Init(secretKey string, expiryTime time.Duration) { + once.Do(func() { + config.secretKey = secretKey + config.expiryTime = expiryTime + }) +} + +func Sign(identityKey string) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims{ + identityKey, + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(config.expiryTime)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + }) + + return token.SignedString([]byte(config.secretKey)) +} + +func Parse(tokenString string) (*TokenResp, error) { + token, err := jwt.ParseWithClaims( + tokenString, + &Claims{}, + func(t *jwt.Token) (interface{}, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, jwt.ErrSignatureInvalid + } + return []byte(config.secretKey), nil + }, + ) + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*Claims); ok { + return &TokenResp{ + claims.IdentityKey, + claims.ExpiresAt.Time, + }, nil + } + + return nil, nil +} + +func ParseRequest(c *gin.Context) (*TokenResp, error) { + // NOTE: Authorization: Bearer sdkfjlsfjlskdfjlsjdflk...slkdfjlka + header := c.GetHeader("Authorization") + + if len(header) == 0 { + return nil, ErrMissingHeader + } + + // get the token + var t string + fmt.Sscanf(header, "Bearer %s", &t) + + return Parse(t) +}