Skip to content

Commit 12d72b5

Browse files
authored
Merge pull request #4 from hervibest/feature/implement-jwt-middleware
Validate jwt and check cache before accesing auth service
2 parents c619a8c + b1b57bf commit 12d72b5

17 files changed

Lines changed: 1266 additions & 926 deletions

File tree

transaction-svc/cmd/web/main.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ func webServer(ctx context.Context) error {
120120
cacheAdapter := adapter.NewCacheAdapter(redisConfig)
121121
paymentAdapter := adapter.NewPaymentAdapter(midtransConfig, cacheAdapter, logs)
122122
messagingAdapter := adapter.NewMessagingAdapter(jetStreamConfig)
123+
// jwtAdapter := adapter.NewJWTAdapter()
124+
123125
transactionProducer := producer.NewTransactionProducer(cacheAdapter, messagingAdapter, logs)
124126

125127
customValidator := helper.NewCustomValidator()
@@ -157,7 +159,9 @@ func webServer(ctx context.Context) error {
157159
walletController := http.NewWalletController(walletUseCase, logs)
158160
transactionWalletCtrl := http.NewTransactionWalletController(transactionWalletUC, customValidator, logs)
159161

160-
authMiddleware := middleware.NewUserAuth(userAdapter, tracer, logs)
162+
// authMiddlewareV2 := middleware.NewAuthMiddleware(userAdapter, logs, tracer, jwtAdapter, cacheAdapter)
163+
164+
// authMiddleware := middleware.NewUserAuth(userAdapter, tracer, logs)
161165
creatorMiddleware := middleware.NewCreatorMiddleware(photoAdapter, tracer, logs)
162166
walletMiddleware := middleware.NewWalletMiddleware(walletUseCase, tracer, logs)
163167

@@ -199,7 +203,7 @@ func webServer(ctx context.Context) error {
199203

200204
serverErrors := make(chan error, 1)
201205
route := route.NewRoute(app, transactionController, bankController, bankWalletController, reviewController,
202-
withdarawlController, walletController, transactionWalletCtrl, authMiddleware, creatorMiddleware, walletMiddleware)
206+
withdarawlController, walletController, transactionWalletCtrl, middleware.NewUserAuth(userAdapter, tracer, logs), creatorMiddleware, walletMiddleware)
203207

204208
route.SetupRoute()
205209
app.Use(cors.New())

transaction-svc/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/go-playground/validator/v10 v10.26.0
1111
github.com/gofiber/contrib/otelfiber v1.0.10
1212
github.com/gofiber/fiber/v2 v2.52.6
13+
github.com/golang-jwt/jwt v3.2.2+incompatible
1314
github.com/google/uuid v1.6.0
1415
github.com/hashicorp/consul/api v1.31.2
1516
github.com/hashicorp/vault/api v1.16.0

transaction-svc/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ github.com/gofiber/contrib/otelfiber v1.0.10/go.mod h1:jN6AvS1HolDHTQHFURsV+7jSX
8383
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
8484
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
8585
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
86+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
87+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
8688
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
8789
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
8890
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package adapter
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"strconv"
7+
"time"
8+
9+
"github.com/golang-jwt/jwt"
10+
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/entity"
11+
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper/utils"
12+
"github.com/oklog/ulid/v2"
13+
)
14+
15+
type JWTAdapter interface {
16+
GenerateAccessToken(userId string) (*entity.AccessToken, error)
17+
GenerateRefreshToken(userId string) (*entity.RefreshToken, error)
18+
VerifyAccessToken(token string) (*entity.AccessToken, error)
19+
VerifyRefreshToken(token string) (*entity.RefreshToken, error)
20+
}
21+
22+
type jwtAdapter struct {
23+
accessSecretByte []byte
24+
refreshSecretByte []byte
25+
accessExpireTime time.Duration
26+
refreshExpireTime time.Duration
27+
}
28+
29+
func NewJWTAdapter() JWTAdapter {
30+
accessSecret := utils.GetEnv("ACCESS_TOKEN_SECRET")
31+
refreshSecret := utils.GetEnv("REFRESH_TOKEN_SECRET")
32+
33+
accessExpireStr := utils.GetEnv("ACCESS_TOKEN_EXP_MINUTE")
34+
refreshExpireStr := utils.GetEnv("REFRESH_TOKEN_EXP_DAY")
35+
36+
accessExpireInt, _ := strconv.Atoi(accessExpireStr)
37+
refreshExpirInt, _ := strconv.Atoi(refreshExpireStr)
38+
39+
return &jwtAdapter{
40+
accessSecretByte: []byte(accessSecret),
41+
refreshSecretByte: []byte(refreshSecret),
42+
accessExpireTime: time.Duration(accessExpireInt),
43+
refreshExpireTime: time.Duration(refreshExpirInt),
44+
}
45+
}
46+
47+
func (c *jwtAdapter) GenerateAccessToken(userId string) (*entity.AccessToken, error) {
48+
expirationTime := time.Now().Add(time.Minute * c.accessExpireTime)
49+
50+
claims := jwt.MapClaims{}
51+
claims["authorized"] = true
52+
claims["user_id"] = userId
53+
claims["exp"] = expirationTime.Unix()
54+
55+
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
56+
stringToken, err := token.SignedString(c.accessSecretByte)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
return &entity.AccessToken{
62+
UserId: userId,
63+
Token: stringToken,
64+
ExpiresAt: expirationTime,
65+
}, nil
66+
}
67+
68+
func (c *jwtAdapter) GenerateRefreshToken(userId string) (*entity.RefreshToken, error) {
69+
expirationTime := time.Now().Add(time.Hour * 24 * c.refreshExpireTime)
70+
71+
claims := jwt.MapClaims{}
72+
claims["user_id"] = userId
73+
claims["exp"] = expirationTime.Unix()
74+
75+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
76+
stringToken, err := token.SignedString(c.refreshSecretByte)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return &entity.RefreshToken{
82+
UserId: userId,
83+
Token: stringToken,
84+
ExpiresAt: expirationTime,
85+
}, nil
86+
}
87+
88+
func (c *jwtAdapter) VerifyAccessToken(token string) (*entity.AccessToken, error) {
89+
tokenClaims, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
90+
return c.accessSecretByte, nil
91+
})
92+
if err != nil {
93+
return nil, err
94+
}
95+
96+
accessTokenDetail := &entity.AccessToken{}
97+
claims, ok := tokenClaims.Claims.(jwt.MapClaims)
98+
if ok && tokenClaims.Valid {
99+
userIdStr, ok := claims["user_id"].(string)
100+
if !ok {
101+
log.Println("user_id not a string")
102+
return nil, fmt.Errorf("Invalid token claims")
103+
}
104+
105+
authorized, ok := claims["authorized"].(bool)
106+
if !ok {
107+
log.Println("authorized is not a bool")
108+
return nil, fmt.Errorf("Invalid token claims")
109+
}
110+
111+
if !authorized {
112+
log.Println("unathorize")
113+
return nil, fmt.Errorf("Invalid token claims")
114+
}
115+
116+
_, err := ulid.Parse(userIdStr)
117+
if err != nil {
118+
log.Println("failed to parse ulid:", err)
119+
return nil, fmt.Errorf("Invalid token claims")
120+
}
121+
122+
accessTokenDetail.UserId = userIdStr
123+
expFloat, ok := claims["exp"].(float64)
124+
if !ok {
125+
log.Println("exp is not a float")
126+
return nil, fmt.Errorf("Invalid exp in token claims")
127+
}
128+
129+
expiresAt := time.Unix(int64(expFloat), 0)
130+
accessTokenDetail.ExpiresAt = expiresAt
131+
accessTokenDetail.Token = token
132+
}
133+
134+
return accessTokenDetail, nil
135+
136+
}
137+
138+
func (c *jwtAdapter) VerifyRefreshToken(token string) (*entity.RefreshToken, error) {
139+
tokenClaims, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
140+
return c.refreshSecretByte, nil
141+
})
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
refreshTokenDetail := &entity.RefreshToken{}
147+
claims, ok := tokenClaims.Claims.(jwt.MapClaims)
148+
if ok && tokenClaims.Valid {
149+
userIdStr, ok := claims["user_id"].(string)
150+
if !ok {
151+
log.Println("user_id not a string")
152+
return nil, fmt.Errorf("Invalid token claims")
153+
}
154+
155+
_, err := ulid.Parse(userIdStr)
156+
if err != nil {
157+
log.Println("failed to parse ulid:", err)
158+
return nil, fmt.Errorf("Invalid token claims")
159+
}
160+
161+
refreshTokenDetail.UserId = userIdStr
162+
expFloat, ok := claims["exp"].(float64)
163+
if !ok {
164+
log.Println("exp is not a float")
165+
return nil, fmt.Errorf("Invalid exp in token claims")
166+
}
167+
168+
expiresAt := time.Unix(int64(expFloat), 0)
169+
refreshTokenDetail.ExpiresAt = expiresAt
170+
refreshTokenDetail.Token = token
171+
}
172+
173+
return refreshTokenDetail, nil
174+
}

transaction-svc/internal/adapter/user_adapter.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ package adapter
33
import (
44
"context"
55
"log"
6+
"time"
67

78
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper"
89
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper/discovery"
910
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper/logger"
1011
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper/utils"
12+
"google.golang.org/protobuf/types/known/timestamppb"
1113

1214
userpb "github.com/hervibest/be-yourmoments-backup/pb/user"
1315
)
1416

1517
type UserAdapter interface {
1618
AuthenticateUser(ctx context.Context, token string) (*userpb.AuthenticateResponse, error)
19+
AuthenticateUserV2(ctx context.Context, token, userId string, expiresAt time.Time) (*userpb.AuthenticateResponseV2, error)
1720
}
1821

1922
type userAdapter struct {
@@ -47,3 +50,18 @@ func (a *userAdapter) AuthenticateUser(ctx context.Context, token string) (*user
4750

4851
return response, nil
4952
}
53+
54+
func (a *userAdapter) AuthenticateUserV2(ctx context.Context, token, userId string, expiresAt time.Time) (*userpb.AuthenticateResponseV2, error) {
55+
processPhotoRequest := &userpb.AuthenticateRequestV2{
56+
Token: token,
57+
UserId: userId,
58+
ExpiresAt: timestamppb.New(expiresAt),
59+
}
60+
61+
response, err := a.client.AuthenticateV2(ctx, processPhotoRequest)
62+
if err != nil {
63+
return nil, helper.FromGRPCError(err)
64+
}
65+
66+
return response, nil
67+
}

transaction-svc/internal/delivery/http/middleware/auth_middleware.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package middleware
22

33
import (
4+
"errors"
45
"strings"
56

7+
"github.com/bytedance/sonic"
68
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/adapter"
9+
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/entity"
710
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper"
811
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/helper/logger"
912
"github.com/hervibest/be-yourmoments-backup/transaction-svc/internal/model"
13+
"github.com/redis/go-redis/v9"
1014

1115
"github.com/gofiber/fiber/v2"
1216
"go.opentelemetry.io/otel/trace"
@@ -43,6 +47,87 @@ func NewUserAuth(userAdapter adapter.UserAdapter, tracer trace.Tracer, logs *log
4347
}
4448
}
4549

50+
type AuthMiddleware interface {
51+
NewUserAuthV2() fiber.Handler
52+
}
53+
type authMiddleware struct {
54+
userAdapter adapter.UserAdapter
55+
logs *logger.Log
56+
tracer trace.Tracer
57+
jwtAdapter adapter.JWTAdapter
58+
cacheAdapter adapter.CacheAdapter
59+
}
60+
61+
func NewAuthMiddleware(userAdapter adapter.UserAdapter, logs *logger.Log, tracer trace.Tracer,
62+
jwtAdapter adapter.JWTAdapter, cacheAdapter adapter.CacheAdapter) AuthMiddleware {
63+
return &authMiddleware{
64+
userAdapter: userAdapter,
65+
logs: logs,
66+
tracer: tracer,
67+
jwtAdapter: jwtAdapter,
68+
cacheAdapter: cacheAdapter,
69+
}
70+
}
71+
72+
func (m *authMiddleware) NewUserAuthV2() fiber.Handler {
73+
return func(ctx *fiber.Ctx) error {
74+
75+
m.logs.Log("Accessed the user auth new middleware")
76+
context, span := m.tracer.Start(ctx.Context(), "authenticateUser", oteltrace.WithAttributes())
77+
defer span.End()
78+
token := strings.TrimPrefix(ctx.Get("Authorization", ""), "Bearer ")
79+
if token == "" || token == "NOT_FOUND" {
80+
return fiber.NewError(fiber.ErrUnauthorized.Code, "Unauthorized access")
81+
}
82+
83+
accessTokenDetail, err := m.jwtAdapter.VerifyAccessToken(token)
84+
if err != nil {
85+
return fiber.NewError(fiber.ErrUnauthorized.Code, "Invalid access token")
86+
}
87+
88+
userId, _ := m.cacheAdapter.Get(context, token)
89+
if userId != "" {
90+
return fiber.NewError(fiber.ErrUnauthorized.Code, "User already signed out")
91+
}
92+
93+
cachedUserStr, err := m.cacheAdapter.Get(context, accessTokenDetail.UserId)
94+
if err != nil && !errors.Is(err, redis.Nil) {
95+
return fiber.NewError(fiber.StatusInternalServerError, "Something went wrong, please try again later")
96+
}
97+
98+
authEntity := new(entity.Auth)
99+
//If redis stale, get from db
100+
if errors.Is(err, redis.Nil) {
101+
authResponse, err := m.userAdapter.AuthenticateUserV2(context, token, accessTokenDetail.UserId, accessTokenDetail.ExpiresAt)
102+
if err != nil {
103+
return fiber.NewError(fiber.ErrBadRequest.Code, "Invalid access token")
104+
}
105+
authEntity.Email = authResponse.GetUser().GetEmail()
106+
authEntity.Id = authResponse.GetUser().GetUserId()
107+
authEntity.PhoneNumber = authResponse.GetUser().GetPhoneNumber()
108+
authEntity.Similarity = uint(authResponse.GetUser().GetSimilarity())
109+
110+
} else {
111+
if err := sonic.ConfigFastest.Unmarshal([]byte(cachedUserStr), &authEntity); err != nil {
112+
return fiber.NewError(fiber.StatusInternalServerError, "Something went wrong, please try again later")
113+
}
114+
}
115+
116+
auth := &model.AuthResponse{
117+
UserId: authEntity.Id,
118+
Username: authEntity.Username,
119+
Email: authEntity.Email,
120+
PhoneNumber: authEntity.PhoneNumber,
121+
Similarity: uint32(authEntity.Similarity),
122+
// CreatorId: authResponse.GetUser().GetCreatorId(),
123+
// WalletId: authResponse.GetUser().GetWalletId(),
124+
}
125+
126+
ctx.Locals("auth", auth)
127+
return ctx.Next()
128+
}
129+
}
130+
46131
func GetUser(ctx *fiber.Ctx) *model.AuthResponse {
47132
return ctx.Locals("auth").(*model.AuthResponse)
48133
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package entity
2+
3+
type Auth struct {
4+
Id string `json:"id"`
5+
Username string `json:"username"`
6+
Email string `json:"email" db:"email"`
7+
PhoneNumber string `json:"phone_number"`
8+
UserProfileID string `json:"user_profile_id"`
9+
Similarity uint `json:"similarity"`
10+
}

0 commit comments

Comments
 (0)