Skip to content

Commit 05f1596

Browse files
Merge pull request #35 from DoWithLogic/feat/revoked-token-after-logout
✨ feat: revoked token after logout with redis
2 parents f21a3c5 + d89c6af commit 05f1596

File tree

15 files changed

+245
-78
lines changed

15 files changed

+245
-78
lines changed

config/config-local.yaml

+6-6
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ Database:
2121
Authentication:
2222
Key: DoWithLogic!@#
2323

24-
JWT:
25-
Key: DoWithLogic!@#
26-
Expired: 60
27-
Label: XXXXX
28-
2924
Observability:
3025
Enable: false
31-
Mode: "otlp/http"
26+
Mode: "otlp/http"
27+
28+
Redis:
29+
Addr: "localhost:6379"
30+
Password: ""
31+
DB: 0

config/config.go

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type (
1717
Authentication AuthenticationConfig
1818
Observability ObservabilityConfig
1919
JWT JWTConfig
20+
Redis RedisConfig
2021
}
2122

2223
// AppConfig holds the configuration related to the application settings.
@@ -44,6 +45,12 @@ type (
4445
Password string
4546
}
4647

48+
RedisConfig struct {
49+
Addr string // The address of the database.
50+
Password string // The password for connecting to the database.
51+
DB int // The name of the database.
52+
}
53+
4754
AuthenticationConfig struct {
4855
Key string
4956
}

config/config.yaml

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ Authentication:
2222

2323
Observability:
2424
Enable: false
25-
Mode: "otlp/http"
25+
Mode: "otlp/http"
26+
27+
Redis:
28+
Addr: "localhost:6379"
29+
Password: ""
30+
DB: 0

docker-compose.yml

+12
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ services:
2121
sh -c "
2222
echo 'CREATE DATABASE IF NOT EXISTS users;' > /docker-entrypoint-initdb.d/init.sql;
2323
/usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
24+
25+
redis:
26+
image: redis:latest
27+
container_name: redis-db
28+
restart: unless-stopped
29+
ports:
30+
- 6379:6379
31+
healthcheck:
32+
test: ["CMD", "redis-cli", "ping"]
33+
interval: 0.5s
34+
timeout: 10s
35+
retries: 5

go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ toolchain go1.21.5
77
require (
88
github.com/go-faker/faker/v4 v4.4.1
99
github.com/go-playground/validator/v10 v10.20.0
10+
github.com/go-redis/redis/v8 v8.11.5
1011
github.com/go-sql-driver/mysql v1.8.1
1112
github.com/golang-jwt/jwt v3.2.2+incompatible
1213
github.com/invopop/validation v0.3.0
@@ -41,6 +42,7 @@ require (
4142
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
4243
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4344
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
45+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
4446
github.com/fsnotify/fsnotify v1.7.0 // indirect
4547
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
4648
github.com/go-logr/logr v1.4.1 // indirect
@@ -55,6 +57,7 @@ require (
5557
github.com/mattn/go-colorable v0.1.13 // indirect
5658
github.com/mattn/go-isatty v0.0.20 // indirect
5759
github.com/mitchellh/mapstructure v1.5.0 // indirect
60+
github.com/onsi/gomega v1.25.0 // indirect
5861
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
5962
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
6063
github.com/prometheus/client_golang v1.19.0 // indirect

go.sum

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
1515
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
1616
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
1717
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
19+
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
1820
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
1921
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2022
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
@@ -38,6 +40,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
3840
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
3941
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
4042
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
43+
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
44+
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
4145
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
4246
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
4347
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -81,6 +85,12 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
8185
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
8286
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
8387
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
88+
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
89+
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
90+
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
91+
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
92+
github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y=
93+
github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
8494
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
8595
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
8696
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -204,6 +214,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
204214
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
205215
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
206216
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
217+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
218+
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
207219
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
208220
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
209221
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/app/service.go

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,48 @@
11
package app
22

33
import (
4+
"context"
45
"net/http"
56

7+
"github.com/DoWithLogic/golang-clean-architecture/internal/middleware"
68
userV1 "github.com/DoWithLogic/golang-clean-architecture/internal/users/delivery/http/v1"
79
userRepository "github.com/DoWithLogic/golang-clean-architecture/internal/users/repository"
810
userUseCase "github.com/DoWithLogic/golang-clean-architecture/internal/users/usecase"
911
"github.com/DoWithLogic/golang-clean-architecture/pkg/app_crypto"
1012
"github.com/DoWithLogic/golang-clean-architecture/pkg/app_jwt"
13+
"github.com/DoWithLogic/golang-clean-architecture/pkg/app_redis"
14+
"github.com/go-redis/redis/v8"
1115
"github.com/labstack/echo/v4"
1216
)
1317

1418
func (app *App) startService() error {
15-
domain := app.echo.Group("/api/v1/users")
19+
domain := app.echo.Group("/api/v1")
1620
domain.GET("/ping", func(c echo.Context) error {
1721
return c.String(http.StatusOK, "Hello Word 👋")
1822
})
1923

24+
client := redis.NewClient(&redis.Options{
25+
Addr: app.cfg.Redis.Addr,
26+
Password: app.cfg.Redis.Password,
27+
DB: app.cfg.Redis.DB,
28+
})
29+
30+
if err := client.Ping(context.Background()).Err(); err != nil {
31+
return err
32+
}
33+
2034
var (
21-
crypto = app_crypto.NewCrypto(app.cfg.Authentication.Key)
22-
appJwt = app_jwt.NewJWT(app.cfg.JWT)
35+
redis = app_redis.NewRedis(client)
36+
crypto = app_crypto.NewCrypto(app.cfg.Authentication.Key)
37+
jwt = app_jwt.NewJWT(app.cfg.JWT, redis)
38+
middleware = middleware.NewMiddleware(jwt)
2339

2440
userRepo = userRepository.NewRepository(app.db)
25-
userUC = userUseCase.NewUseCase(userRepo, appJwt, crypto)
41+
userUC = userUseCase.NewUseCase(userRepo, jwt, crypto)
2642
userCTRL = userV1.NewHandlers(userUC)
2743
)
2844

29-
return userCTRL.UserRoutes(domain, app.cfg)
45+
userCTRL.UserRoutes(domain, middleware)
46+
47+
return nil
3048
}

internal/middleware/guard.go

+16-42
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,41 @@
11
package middleware
22

33
import (
4-
"fmt"
5-
6-
"github.com/DoWithLogic/golang-clean-architecture/config"
4+
"github.com/DoWithLogic/golang-clean-architecture/pkg/app_jwt"
75
"github.com/DoWithLogic/golang-clean-architecture/pkg/apperror"
86
"github.com/DoWithLogic/golang-clean-architecture/pkg/constant"
97
"github.com/DoWithLogic/golang-clean-architecture/pkg/response"
10-
"github.com/golang-jwt/jwt"
118
"github.com/labstack/echo/v4"
129
"github.com/pkg/errors"
1310
)
1411

15-
type PayloadToken struct {
16-
Data *Data `json:"data"`
17-
jwt.StandardClaims
12+
var (
13+
errMissingJwtToken = errors.New("Missing JWT token")
14+
errInvalidJwtToken = errors.New("Invalid JWT token")
15+
)
16+
17+
type Middleware struct {
18+
jwt *app_jwt.JWT
1819
}
1920

20-
type Data struct {
21-
UserID int64 `json:"user_id"`
22-
Email string `json:"email"`
21+
func NewMiddleware(jwt *app_jwt.JWT) *Middleware {
22+
return &Middleware{jwt: jwt}
2323
}
2424

2525
// Middleware function to validate JWT token
26-
func JWTMiddleware(cfg config.Config) echo.MiddlewareFunc {
26+
func (m *Middleware) JWTMiddleware() echo.MiddlewareFunc {
2727
return func(next echo.HandlerFunc) echo.HandlerFunc {
2828
return func(c echo.Context) error {
29-
tokenString := c.Request().Header.Get(constant.AuthorizationHeaderKey)
30-
if tokenString == "" {
31-
return response.ErrorBuilder(apperror.Unauthorized(errors.New("Missing JWT token"))).Send(c)
29+
token := c.Request().Header.Get(constant.AuthorizationHeaderKey)
30+
if token == "" {
31+
return response.ErrorBuilder(apperror.Unauthorized(errMissingJwtToken)).Send(c)
3232
}
3333

34-
// Remove "Bearer " prefix from token string
35-
tokenString = tokenString[len("Bearer "):]
36-
37-
token, err := jwt.ParseWithClaims(tokenString, &PayloadToken{}, func(token *jwt.Token) (interface{}, error) {
38-
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
39-
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
40-
}
41-
return []byte(cfg.JWT.Key), nil
42-
})
43-
44-
if err != nil {
45-
return response.ErrorBuilder(apperror.Unauthorized(errors.New("Invalid JWT token"))).Send(c)
46-
}
47-
if !token.Valid {
48-
return response.ErrorBuilder(apperror.Unauthorized(errors.New("JWT token is not valid"))).Send(c)
34+
if err := m.jwt.ValidateToken(c, token); err != nil {
35+
return err
4936
}
5037

51-
// Store the token claims in the request context for later use
52-
claims := token.Claims.(*PayloadToken)
53-
c.Set(constant.AuthCredentialKey, claims)
54-
5538
return next(c)
5639
}
5740
}
5841
}
59-
60-
func NewTokenInformation(ctx echo.Context) (*PayloadToken, error) {
61-
tokenInformation, ok := ctx.Get(constant.AuthCredentialKey).(*PayloadToken)
62-
if !ok {
63-
return tokenInformation, apperror.Unauthorized(apperror.ErrFailedGetTokenInformation)
64-
}
65-
66-
return tokenInformation, nil
67-
}

internal/users/delivery/http/v1/handlers.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package v1
22

33
import (
4-
"github.com/DoWithLogic/golang-clean-architecture/internal/middleware"
54
"github.com/DoWithLogic/golang-clean-architecture/internal/users"
65
"github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos"
6+
"github.com/DoWithLogic/golang-clean-architecture/pkg/app_jwt"
77
"github.com/DoWithLogic/golang-clean-architecture/pkg/apperror"
88
"github.com/DoWithLogic/golang-clean-architecture/pkg/observability/instrumentation"
99
"github.com/DoWithLogic/golang-clean-architecture/pkg/response"
@@ -64,7 +64,7 @@ func (h *handlers) UserDetail(c echo.Context) error {
6464
ctx, span := instrumentation.NewTraceSpan(c.Request().Context(), "UserDetailHandler")
6565
defer span.End()
6666

67-
userData, err := middleware.NewTokenInformation(c)
67+
userData, err := app_jwt.NewTokenInformation(c)
6868
if err != nil {
6969
return response.ErrorBuilder(err).Send(c)
7070
}
@@ -90,7 +90,7 @@ func (h *handlers) UpdateUser(c echo.Context) error {
9090
return response.ErrorBuilder(apperror.BadRequest(err)).Send(c)
9191
}
9292

93-
userData, err := middleware.NewTokenInformation(c)
93+
userData, err := app_jwt.NewTokenInformation(c)
9494
if err != nil {
9595
return response.ErrorBuilder(err).Send(c)
9696
}
@@ -120,7 +120,7 @@ func (h *handlers) UpdateUserStatus(c echo.Context) error {
120120
return response.ErrorBuilder(apperror.BadRequest(err)).Send(c)
121121
}
122122

123-
userData, err := middleware.NewTokenInformation(c)
123+
userData, err := app_jwt.NewTokenInformation(c)
124124
if err != nil {
125125
return response.ErrorBuilder(err).Send(c)
126126
}
+11-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
package v1
22

33
import (
4-
"github.com/DoWithLogic/golang-clean-architecture/config"
54
"github.com/DoWithLogic/golang-clean-architecture/internal/middleware"
65
"github.com/labstack/echo/v4"
76
)
87

9-
func (h *handlers) UserRoutes(domain *echo.Group, cfg config.Config) error {
10-
domain.POST("", h.CreateUser)
11-
domain.POST("/login", h.Login)
12-
domain.GET("/detail", h.UserDetail, middleware.JWTMiddleware(cfg))
13-
domain.PATCH("/update", h.UpdateUser, middleware.JWTMiddleware(cfg))
14-
domain.PUT("/update/status", h.UpdateUserStatus, middleware.JWTMiddleware(cfg))
8+
func (h *handlers) UserRoutes(domain *echo.Group, mw *middleware.Middleware) {
9+
// privates
10+
{
11+
private := domain.Group("/user", mw.JWTMiddleware())
12+
private.GET("/detail", h.UserDetail)
13+
private.PATCH("/update", h.UpdateUser)
14+
private.PUT("/update/status", h.UpdateUserStatus)
15+
}
1516

16-
return nil
17+
// publics
18+
domain.POST("/user", h.CreateUser)
19+
domain.POST("/user/login", h.Login)
1720
}

internal/users/usecase/usecase.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"strings"
77
"time"
88

9-
"github.com/DoWithLogic/golang-clean-architecture/internal/middleware"
109
"github.com/DoWithLogic/golang-clean-architecture/internal/users"
1110
"github.com/DoWithLogic/golang-clean-architecture/internal/users/dtos"
1211
"github.com/DoWithLogic/golang-clean-architecture/internal/users/entities"
@@ -42,8 +41,8 @@ func (uc *usecase) Login(ctx context.Context, request dtos.UserLoginRequest) (re
4241
return response, apperror.Unauthorized(apperror.ErrInvalidPassword)
4342
}
4443

45-
claims := middleware.PayloadToken{
46-
Data: &middleware.Data{
44+
claims := app_jwt.PayloadToken{
45+
Data: &app_jwt.Data{
4746
UserID: dataLogin.UserID,
4847
Email: dataLogin.Email,
4948
},

0 commit comments

Comments
 (0)