fix add auth midlleware

This commit is contained in:
Pavel 2024-03-03 13:13:13 +03:00
parent b9fb8015e3
commit aa8c326b25
14 changed files with 255 additions and 36 deletions

22
.env

@ -5,7 +5,7 @@ HTTP_PORT="8080"
# MongoDB settings
MONGO_HOST="127.0.0.1"
MONGO_PORT="27021"
MONGO_PORT="27020"
MONGO_USER="test"
MONGO_PASSWORD="test"
MONGO_DB="admin"
@ -21,6 +21,26 @@ PUBLIC_CURVE_KEY="-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEbnIvjIMle4rqVol6K
PRIVATE_CURVE_KEY="-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIKn0BKwF3vZvODgWAnUIwQhd8de5oZhY48gc23EWfrfs\n-----END PRIVATE KEY-----"
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2B
iw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikH
oKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAEC
gYAOphnVPXbk6lpYzdkLC1Xn5EOEuNfOLLURLxBnPWozZo26r/Mtahu/9mYhrYlv
PP8r6mxta3VIil8iOdZyOLa/4d1LPd+UehgEXIJEiYXLtn7RS5eUnoPuQxssfs1k
OWjdN8p6SzppleegFTvGRX4KM3cDLfSphOk8JuBCrpSSYQJBAOdqizTSrdKMTuVe
c7Jk1JOJkyFuFs+N5zeryyeFGH7IpRdWy0rkWMxIUAi8Ap1vYVBPHv4tDOo3sy5X
VLc/knkCQQCE62pg+0TmsrhO/2Pgog6MLBkzlzXYMRp/01HbmznwYF+ejfPnzLkz
hnUlxRUNK3lhXM/7H6oAjvqF2R72u/OPAkEAterkmdbQfEZ+MwNoEiH/lie9OLdx
SSI1VGdBYcTYN7qFRW6eizYstBJYkDU0HQ0Uw+we4hMKJwk4W0KdvxxDiQJAeqlB
V1QqBneBbK10PzVuFV8QtrJhJyxRVwrtbKq38iMNuqUnI4+ijXEUpJFWVvv6nKXo
7McQvEk12dU/JNTX8wJAOlAtSNjp9tVwpMpC0w2St1eKc1L2SknjeohA5ldoBz8sGeZsPhTU3eHSD1neAZXLKN5K68z3zFBr20ubY9nyLw==
-----END RSA PRIVATE KEY-----"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAE=\n-----END PUBLIC KEY-----"
JWT_ISSUER="pena-auth-service"
JWT_AUDIENCE="pena"
# SIGN_SECRET="group"
SIGN_SECRET="secret"

@ -6,8 +6,9 @@ services:
ports:
- "27020:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
- MONGO_INITDB_ROOT_USERNAME=test
- MONGO_INITDB_ROOT_PASSWORD=test
- MONGO_INITDB_AUTH_MECHANISM=SCRAM-SHA-1
volumes:
- mongo_data:/data/db

1
go.mod

@ -6,6 +6,7 @@ require (
github.com/caarlos0/env/v8 v8.0.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.51.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/joho/godotenv v1.5.1
github.com/pioz/faker v1.7.3
github.com/rs/xid v1.5.0

2
go.sum

@ -15,6 +15,8 @@ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/gofiber/fiber/v2 v2.51.0 h1:JNACcZy5e2tGApWB2QrRpenTWn0fq0hkFm6k0C86gKQ=
github.com/gofiber/fiber/v2 v2.51.0/go.mod h1:xaQRZQJGqnKOQnbQw+ltvku3/h8QxvNi8o6JiJ7Ll0U=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=

@ -10,6 +10,7 @@ import (
"codeword/internal/worker/purge_worker"
"codeword/internal/worker/recovery_worker"
"codeword/pkg/closer"
"codeword/utils"
"context"
"errors"
"github.com/twmb/franz-go/pkg/kgo"
@ -95,8 +96,11 @@ func Run(ctx context.Context, cfg initialize.Config, logger *zap.Logger) error {
DiscountClient: discountRpcClient,
})
jwtUtil := utils.NewJWT(&cfg)
authMiddleware := utils.NewAuthenticator(jwtUtil)
recoveryController := recovery.NewRecoveryController(logger, recoveryService, cfg.DefaultRedirectionURL)
promoCodeController := promocode.NewPromoCodeController(logger, promoService)
promoCodeController := promocode.NewPromoCodeController(promocode.Deps{Logger: logger, PromoCodeService: promoService, AuthMiddleware: authMiddleware})
recoveryWC := recovery_worker.NewRecoveryWC(recovery_worker.Deps{
Logger: logger,

@ -9,15 +9,23 @@ import (
"go.uber.org/zap"
)
type Deps struct {
Logger *zap.Logger
PromoCodeService *services.PromoCodeService
AuthMiddleware func(*fiber.Ctx) error
}
type PromoCodeController struct {
logger *zap.Logger
promoCodeService *services.PromoCodeService
authMiddleware func(*fiber.Ctx) error
}
func NewPromoCodeController(logger *zap.Logger, promoCodeService *services.PromoCodeService) *PromoCodeController {
func NewPromoCodeController(deps Deps) *PromoCodeController {
return &PromoCodeController{
logger: logger,
promoCodeService: promoCodeService,
logger: deps.Logger,
promoCodeService: deps.PromoCodeService,
authMiddleware: deps.AuthMiddleware,
}
}
@ -89,20 +97,27 @@ func (p *PromoCodeController) GetList(c *fiber.Ctx) error {
}
func (p *PromoCodeController) Activate(c *fiber.Ctx) error {
err := p.authMiddleware(c)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err})
}
userID := c.Locals(models.AuthJWTDecodedUserIDKey).(string)
if userID == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "failed to get jwt payload"})
}
var req models.ActivateReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request payload"})
}
if req.UserID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "userid is required"})
}
if req.Codeword == "" && req.FastLink == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "codeword or fastlink is required"})
}
greetings, err := p.promoCodeService.ActivatePromo(c.Context(), &req)
greetings, err := p.promoCodeService.ActivatePromo(c.Context(), &req, userID)
if err != nil {
p.logger.Error("Failed to activate promocode", zap.Error(err))
@ -113,10 +128,7 @@ func (p *PromoCodeController) Activate(c *fiber.Ctx) error {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Internal Server Error"})
}
resp := models.ActivateResp{
Greetings: greetings,
}
return c.Status(fiber.StatusOK).JSON(resp)
return c.Status(fiber.StatusOK).JSON(models.ActivateResp{Greetings: greetings})
}
func (p *PromoCodeController) Delete(c *fiber.Ctx) error {

@ -35,6 +35,10 @@ type Config struct {
KafkaTopic string `env:"KAFKA_TOPIC_TARIFF"`
DiscountServiceAddress string `env:"DISCOUNT_ADDRESS"`
RecoveryUrl string `env:"RECOVERY_URL"`
PrivateKey string `env:"JWT_PRIVATE_KEY"`
PublicKey string `env:"JWT_PUBLIC_KEY,required"`
Issuer string `env:"JWT_ISSUER,required"`
Audience string `env:"JWT_AUDIENCE,required"`
}
func LoadConfig() (*Config, error) {

@ -9,3 +9,6 @@ type RefreshResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
}
const AuthJWTDecodedUserIDKey = "userID"
const AuthJWTDecodedAccessTokenKey = "access-token"

@ -59,7 +59,6 @@ type GetPromoCodesListResp struct {
}
type ActivateReq struct {
UserID string `json:"userID"` // для кого активировать нужно для кафки
Codeword string `json:"codeword"`
FastLink string `json:"fastLink"`
}

@ -86,14 +86,14 @@ func (s *PromoCodeService) GetPromoCodesList(ctx context.Context, req *models.Ge
// разделяется ли она или они всегда вместе, если разделяются то что-то из этого может быть пустым либо все заполеннное,
// соответсвенно надо сделать соответствующие проверки до записи в кафку и до отправки в дискаунт сервис
func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.ActivateReq) (string, error) {
func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.ActivateReq, userID string) (string, error) {
promoCode, err := s.promoCodeRepo.ActivatePromo(ctx, req)
if err != nil {
s.logger.Error("Failed to activate promocode", zap.Error(err))
return "", err
}
err = s.statsRepo.UpdateStatistics(ctx, promoCode.ID.Hex(), req.UserID)
err = s.statsRepo.UpdateStatistics(ctx, promoCode.ID.Hex(), userID)
if err != nil {
s.logger.Error("Failed add in stats", zap.Error(err))
return "", err
@ -118,7 +118,7 @@ func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.Activa
Deleted: promoCode.Delete,
CreatedAt: promoCode.CreatedAt,
}
if err := s.kafka.Send(ctx, req.UserID, fakeTariff); err != nil {
if err := s.kafka.Send(ctx, userID, fakeTariff); err != nil {
s.logger.Error("Failed to send fake tariff to Kafka", zap.Error(err))
return "", err
}
@ -130,7 +130,7 @@ func (s *PromoCodeService) ActivatePromo(ctx context.Context, req *models.Activa
Description: "",
Condition: &discount.DiscountCondition{
Coupon: &promoCode.Codeword,
User: &req.UserID,
User: &userID,
},
Target: &discount.DiscountCalculationTarget{
Factor: promoCode.Bonus.Discount.Factor,

@ -2,6 +2,7 @@ package e2e
import (
"codeword/internal/models"
"codeword/tests/helpers"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v2"
@ -420,24 +421,30 @@ func TestGetPromoCodesList(t *testing.T) {
// ActivatePromoCode
func TestActivatePromoCode(t *testing.T) {
client := fiber.AcquireClient()
jwtUtil := helpers.InitializeJWT()
token, tokenErr := jwtUtil.Create(ExampleUserID)
fmt.Println(token)
if isNoError := assert.NoError(t, tokenErr); !isNoError {
return
}
t.Run("ActivatePromoCode-success codeword", func(t *testing.T) {
reqBody := models.ActivateReq{
UserID: ExampleUserID,
Codeword: "example",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Body(reqJSON)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.NoError(t, errs[0])
}
assert.Equal(t, fiber.StatusOK, statusCode)
fmt.Println(string(resBody))
assert.Equal(t, fiber.StatusOK, statusCode)
fmt.Println(statusCode)
var response models.ActivateResp
err := json.Unmarshal(resBody, &response)
assert.NoError(t, err)
@ -446,13 +453,12 @@ func TestActivatePromoCode(t *testing.T) {
t.Run("ActivatePromoCode-success fastLink", func(t *testing.T) {
reqBody := models.ActivateReq{
UserID: ExampleUserID,
FastLink: fastLink,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Body(reqJSON)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
@ -473,7 +479,7 @@ func TestActivatePromoCode(t *testing.T) {
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Body(reqJSON)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
@ -489,13 +495,8 @@ func TestActivatePromoCode(t *testing.T) {
})
t.Run("ActivatePromoCode-missing codeword and fastlink", func(t *testing.T) {
reqBody := models.ActivateReq{
UserID: ExampleUserID,
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Body(reqJSON)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(nil)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
@ -512,13 +513,12 @@ func TestActivatePromoCode(t *testing.T) {
t.Run("ActivatePromoCode-promocode not found", func(t *testing.T) {
reqBody := models.ActivateReq{
UserID: ExampleUserID,
Codeword: "none",
}
reqJSON, _ := json.Marshal(reqBody)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Body(reqJSON)
req := client.Post(BaseUrl+"/promocode/activate").Set("Content-Type", "application/json").Set("Authorization", "Bearer "+token).Body(reqJSON)
statusCode, resBody, errs := req.Bytes()
if len(errs) != 0 {
assert.Error(t, errs[0])

38
tests/helpers/jwt.go Normal file

@ -0,0 +1,38 @@
package helpers
import (
"codeword/internal/initialize"
"codeword/utils"
"strings"
)
func InitializeJWT() *utils.JWT {
publicKey := strings.Replace(`-----BEGIN PUBLIC KEY-----
MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgHgnvr7O2tiApjJfid1orFnIGm69
80fZp+Lpbjo+NC/0whMFga2Biw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6B
dA4TS2kB9Kf0wn0+7wSlyikHoKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y
+3GyaOY536H47qyXAgMBAAE=
-----END PUBLIC KEY-----`, "\t", "", -1)
privateKey := strings.Replace(`-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgHgnvr7O2tiApjJfid1orFnIGm6980fZp+Lpbjo+NC/0whMFga2B
iw5b1G2Q/B2u0tpO1Fs/E8z7Lv1nYfr5jx2S8x6BdA4TS2kB9Kf0wn0+7wSlyikH
oKhbtzwXHZl17GsyEi6wHnsqNBSauyIWhpha8i+Y+3GyaOY536H47qyXAgMBAAEC
gYAOphnVPXbk6lpYzdkLC1Xn5EOEuNfOLLURLxBnPWozZo26r/Mtahu/9mYhrYlv
PP8r6mxta3VIil8iOdZyOLa/4d1LPd+UehgEXIJEiYXLtn7RS5eUnoPuQxssfs1k
OWjdN8p6SzppleegFTvGRX4KM3cDLfSphOk8JuBCrpSSYQJBAOdqizTSrdKMTuVe
c7Jk1JOJkyFuFs+N5zeryyeFGH7IpRdWy0rkWMxIUAi8Ap1vYVBPHv4tDOo3sy5X
VLc/knkCQQCE62pg+0TmsrhO/2Pgog6MLBkzlzXYMRp/01HbmznwYF+ejfPnzLkz
hnUlxRUNK3lhXM/7H6oAjvqF2R72u/OPAkEAterkmdbQfEZ+MwNoEiH/lie9OLdx
SSI1VGdBYcTYN7qFRW6eizYstBJYkDU0HQ0Uw+we4hMKJwk4W0KdvxxDiQJAeqlB
V1QqBneBbK10PzVuFV8QtrJhJyxRVwrtbKq38iMNuqUnI4+ijXEUpJFWVvv6nKXo
7McQvEk12dU/JNTX8wJAOlAtSNjp9tVwpMpC0w2St1eKc1L2SknjeohA5ldoBz8sGeZsPhTU3eHSD1neAZXLKN5K68z3zFBr20ubY9nyLw==
-----END RSA PRIVATE KEY-----`, "\t", "", -1)
return utils.NewJWT(&initialize.Config{
PrivateKey: privateKey,
PublicKey: publicKey,
Audience: "pena",
Issuer: "pena-auth-service",
})
}

46
utils/authenticator.go Normal file

@ -0,0 +1,46 @@
package utils
import (
"codeword/internal/models"
"fmt"
"strings"
"github.com/gofiber/fiber/v2"
)
const (
prefix = "Bearer "
)
func NewAuthenticator(jwtUtil *JWT) func(*fiber.Ctx) error {
return func(c *fiber.Ctx) error {
if jwtUtil == nil {
return fmt.Errorf("jwt util is nil")
}
if err := authenticate(c, jwtUtil); err != nil {
return fmt.Errorf("authentication error:%d", err)
}
return nil
}
}
func authenticate(c *fiber.Ctx, jwtUtil *JWT) error {
authHeader := c.Get("Authorization")
if authHeader == "" || !strings.HasPrefix(authHeader, prefix) {
return fmt.Errorf("failed to parse jws from request header: %s", authHeader)
}
jws := strings.TrimPrefix(authHeader, prefix)
userID, validateErr := jwtUtil.Validate(jws)
if validateErr != nil {
return validateErr
}
c.Locals(models.AuthJWTDecodedUserIDKey, userID)
c.Locals(models.AuthJWTDecodedAccessTokenKey, jws)
return nil
}

89
utils/jwt.go Normal file

@ -0,0 +1,89 @@
package utils
import (
"codeword/internal/initialize"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"time"
)
type JWT struct {
privateKey []byte
publicKey []byte
algorithm *jwt.SigningMethodRSA
expiresIn time.Duration
issuer string
audience string
}
func NewJWT(configuration *initialize.Config) *JWT {
return &JWT{
privateKey: []byte(configuration.PrivateKey),
publicKey: []byte(configuration.PublicKey),
issuer: configuration.Issuer,
audience: configuration.Audience,
algorithm: jwt.SigningMethodRS256,
expiresIn: 15 * time.Minute,
}
}
func (j *JWT) Create(id string) (string, error) {
privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(j.privateKey)
if err != nil {
return "", fmt.Errorf("failed to parse private key on <Create> of <JWT>: %w", err)
}
now := time.Now().UTC()
claims := jwt.MapClaims{
"id": id,
"exp": now.Add(j.expiresIn).Unix(),
"aud": j.audience,
"iss": j.issuer,
}
token, err := jwt.NewWithClaims(j.algorithm, claims).SignedString(privateKey)
if err != nil {
return "", fmt.Errorf("failed to sing on <Create> of <JWT>: %w", err)
}
return token, nil
}
func (j *JWT) Validate(tokenString string) (string, error) {
key, err := jwt.ParseRSAPublicKeyFromPEM(j.publicKey)
if err != nil {
return "", fmt.Errorf("failed to parse rsa public key on <Validate> of <JWT>: %w", err)
}
parseCallback := func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %s", token.Header["alg"])
}
return key, nil
}
token, err := jwt.Parse(
tokenString,
parseCallback,
jwt.WithAudience(j.audience),
jwt.WithIssuer(j.issuer),
)
if err != nil {
return "", fmt.Errorf("failed to parse jwt token on <Validate> of <JWT>: %w", err)
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return "", errors.New("token is invalid on <Validate> of <JWT>")
}
data, ok := claims["id"].(string)
if !ok {
return "", errors.New("data is empty or not a string on <Validate> of <JWT>")
}
return data, nil
}