Skip to content

Commit 4b6c4a9

Browse files
alielmi98naeemaei
andauthored
feat: add refresh token endpoint and related configurations (#9)
* feat: add refresh token endpoint and related configurations New Feature: -Implemented the RefreshToken method in the TokenUsecase to handle token refreshing. -The method retrieves the refresh token from the HTTP cookie, validates it, and generates a new access and refresh token pair. Details: -Extracts the refresh token from the cookie using c.Cookie. -Validates the refresh token and extracts claims using the GetClaims method. -Converts roles from []interface{} to []string for proper type handling. -Generates a new token pair using the GenerateToken method. Reason for Addition: -To provide functionality for refreshing expired access tokens while maintaining security through refresh tokens. -This is a critical feature for session management in the application. Benefits: -Enables secure token lifecycle management. -Improves user experience by allowing seamless token refresh without requiring re-login. * Fix refresh token method, set CSRF SameSite, and minor domain config correction - change refresh token request method from GET to POST for better security and API alignment. - Added 'SameSite' attribute to CSRF cookie in production for improved security. - Corrected a minor typo in domain config (added missing 'a' in "domain") * feat: add token usecase and config to UsersHandler --------- Co-authored-by: Hamed Naeemaei <h.naimaei@gmail.com>
1 parent 45401e6 commit 4b6c4a9

File tree

13 files changed

+228
-18
lines changed

13 files changed

+228
-18
lines changed

src/api/handler/base_generic_crud.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ var logger = logging.NewLogger(config.GetConfig())
2929
func Create[TRequest any, TUInput any, TUOutput any, TResponse any](c *gin.Context,
3030
requestMapper func(req TRequest) (res TUInput),
3131
responseMapper func(req TUOutput) (res TResponse),
32-
usecaseCreate func(ctx context.Context,
33-
req TUInput) (TUOutput, error)) {
32+
usecaseCreate func(ctx context.Context, req TUInput) (TUOutput, error)) {
3433

3534
// bind http request
3635
request := new(TRequest)

src/api/handler/user.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,23 @@ import (
77
"github.com/naeemaei/golang-clean-web-api/api/dto"
88
"github.com/naeemaei/golang-clean-web-api/api/helper"
99
"github.com/naeemaei/golang-clean-web-api/config"
10+
"github.com/naeemaei/golang-clean-web-api/constant"
1011
"github.com/naeemaei/golang-clean-web-api/dependency"
1112
"github.com/naeemaei/golang-clean-web-api/usecase"
1213
)
1314

1415
type UsersHandler struct {
15-
userUsecase *usecase.UserUsecase
16-
otpUsecase *usecase.OtpUsecase
16+
userUsecase *usecase.UserUsecase
17+
otpUsecase *usecase.OtpUsecase
18+
tokenUsecase *usecase.TokenUsecase
19+
config *config.Config
1720
}
1821

1922
func NewUserHandler(cfg *config.Config) *UsersHandler {
2023
userUsecase := usecase.NewUserUsecase(cfg, dependency.GetUserRepository(cfg))
2124
otpUsecase := usecase.NewOtpUsecase(cfg)
22-
return &UsersHandler{userUsecase: userUsecase, otpUsecase: otpUsecase}
25+
tokenUsecase := usecase.NewTokenUsecase(cfg)
26+
return &UsersHandler{userUsecase: userUsecase, otpUsecase: otpUsecase, tokenUsecase: tokenUsecase, config: cfg}
2327
}
2428

2529
// LoginByUsername godoc
@@ -48,6 +52,18 @@ func (h *UsersHandler) LoginByUsername(c *gin.Context) {
4852
return
4953
}
5054

55+
// Set the refresh token in a cookie
56+
http.SetCookie(c.Writer, &http.Cookie{
57+
Name: constant.RefreshTokenCookieName,
58+
Value: token.RefreshToken,
59+
MaxAge: int(h.config.JWT.RefreshTokenExpireDuration * 60),
60+
Path: "/",
61+
Domain: h.config.Server.Domain,
62+
Secure: true,
63+
HttpOnly: true,
64+
SameSite: http.SameSiteStrictMode,
65+
})
66+
5167
c.JSON(http.StatusCreated, helper.GenerateBaseResponse(token, true, helper.Success))
5268
}
5369

@@ -106,6 +122,18 @@ func (h *UsersHandler) RegisterLoginByMobileNumber(c *gin.Context) {
106122
return
107123
}
108124

125+
// Set the refresh token in a cookie
126+
http.SetCookie(c.Writer, &http.Cookie{
127+
Name: constant.RefreshTokenCookieName,
128+
Value: token.RefreshToken,
129+
MaxAge: int(h.config.JWT.RefreshTokenExpireDuration * 60),
130+
Path: "/",
131+
Domain: h.config.Server.Domain,
132+
Secure: true,
133+
HttpOnly: true,
134+
SameSite: http.SameSiteStrictMode,
135+
})
136+
109137
c.JSON(http.StatusCreated, helper.GenerateBaseResponse(token, true, helper.Success))
110138
}
111139

@@ -137,3 +165,34 @@ func (h *UsersHandler) SendOtp(c *gin.Context) {
137165
// TODO: Call internal SMS service
138166
c.JSON(http.StatusCreated, helper.GenerateBaseResponse(nil, true, helper.Success))
139167
}
168+
169+
// RefreshToken godoc
170+
// @Summary RefreshToken
171+
// @Description RefreshToken
172+
// @Tags Users
173+
// @Accept json
174+
// @Produce json
175+
// @Success 200 {object} helper.BaseHttpResponse "Success"
176+
// @Failure 400 {object} helper.BaseHttpResponse "Failed"
177+
// @Failure 401 {object} helper.BaseHttpResponse "Failed"
178+
// @Router /v1/users/refresh-token [post]
179+
func (h *UsersHandler) RefreshToken(c *gin.Context) {
180+
token, err := h.tokenUsecase.RefreshToken(c)
181+
if err != nil {
182+
c.AbortWithStatusJSON(helper.TranslateErrorToStatusCode(err),
183+
helper.GenerateBaseResponseWithError(nil, false, helper.InternalError, err))
184+
return
185+
}
186+
// Set the refresh token in a cookie
187+
http.SetCookie(c.Writer, &http.Cookie{
188+
Name: constant.RefreshTokenCookieName,
189+
Value: token.RefreshToken,
190+
MaxAge: int(h.config.JWT.RefreshTokenExpireDuration * 60),
191+
Path: "/",
192+
Domain: h.config.Server.Domain,
193+
Secure: true,
194+
HttpOnly: true,
195+
SameSite: http.SameSiteStrictMode,
196+
})
197+
c.JSON(http.StatusOK, helper.GenerateBaseResponse(token, true, helper.Success))
198+
}

src/api/router/users.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ func User(router *gin.RouterGroup, cfg *config.Config) {
1414
router.POST("/login-by-username", h.LoginByUsername)
1515
router.POST("/register-by-username", h.RegisterByUsername)
1616
router.POST("/login-by-mobile", h.RegisterLoginByMobileNumber)
17+
router.POST("/refresh-token", h.RefreshToken)
1718
}

src/config/config-development.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ server:
22
internalPort: 5005
33
externalPort: 5005
44
runMode: debug
5+
domain: localhost
56
logger:
67
filePath: ../logs/
78
encoding: json

src/config/config-docker.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ server:
22
internalPort: 5000
33
externalPort: 0
44
runMode: release
5+
domain: localhost
56
logger:
67
filePath: /app/logs/
78
encoding: json

src/config/config-production.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ server:
22
internalPort: 5010
33
externalPort: 5010
44
runMode: release
5+
domain: localhost
56
logger:
67
filePath: logs/
78
encoding: json

src/config/config.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ type Config struct {
2121
}
2222

2323
type ServerConfig struct {
24-
InternalPort string
25-
ExternalPort string
26-
RunMode string
24+
InternalPort string
25+
ExternalPort string
26+
RunMode string
27+
Domain string
2728
}
2829

2930
type LoggerConfig struct {
@@ -93,10 +94,10 @@ func GetConfig() *Config {
9394

9495
cfg, err := ParseConfig(v)
9596
envPort := os.Getenv("PORT")
96-
if envPort != ""{
97+
if envPort != "" {
9798
cfg.Server.ExternalPort = envPort
9899
log.Printf("Set external port from environment -> %s", cfg.Server.ExternalPort)
99-
}else{
100+
} else {
100101
cfg.Server.ExternalPort = cfg.Server.InternalPort
101102
log.Printf("Set external port from environment -> %s", cfg.Server.ExternalPort)
102103
}

src/constant/constanst.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,7 @@ const (
1717
MobileNumberKey string = "MobileNumber"
1818
RolesKey string = "Roles"
1919
ExpireTimeKey string = "Exp"
20+
21+
// JWT
22+
RefreshTokenCookieName string = "refresh_token"
2023
)

src/docs/docs.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4661,6 +4661,41 @@ const docTemplate = `{
46614661
}
46624662
}
46634663
},
4664+
"/v1/users/refresh-token": {
4665+
"get": {
4666+
"description": "RefreshToken",
4667+
"consumes": [
4668+
"application/json"
4669+
],
4670+
"produces": [
4671+
"application/json"
4672+
],
4673+
"tags": [
4674+
"Users"
4675+
],
4676+
"summary": "RefreshToken",
4677+
"responses": {
4678+
"200": {
4679+
"description": "Success",
4680+
"schema": {
4681+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4682+
}
4683+
},
4684+
"400": {
4685+
"description": "Failed",
4686+
"schema": {
4687+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4688+
}
4689+
},
4690+
"401": {
4691+
"description": "Failed",
4692+
"schema": {
4693+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4694+
}
4695+
}
4696+
}
4697+
}
4698+
},
46644699
"/v1/users/register-by-username": {
46654700
"post": {
46664701
"description": "RegisterByUsername",

src/docs/swagger.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4650,6 +4650,41 @@
46504650
}
46514651
}
46524652
},
4653+
"/v1/users/refresh-token": {
4654+
"get": {
4655+
"description": "RefreshToken",
4656+
"consumes": [
4657+
"application/json"
4658+
],
4659+
"produces": [
4660+
"application/json"
4661+
],
4662+
"tags": [
4663+
"Users"
4664+
],
4665+
"summary": "RefreshToken",
4666+
"responses": {
4667+
"200": {
4668+
"description": "Success",
4669+
"schema": {
4670+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4671+
}
4672+
},
4673+
"400": {
4674+
"description": "Failed",
4675+
"schema": {
4676+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4677+
}
4678+
},
4679+
"401": {
4680+
"description": "Failed",
4681+
"schema": {
4682+
"$ref": "#/definitions/github_com_naeemaei_golang-clean-web-api_api_helper.BaseHttpResponse"
4683+
}
4684+
}
4685+
}
4686+
}
4687+
},
46534688
"/v1/users/register-by-username": {
46544689
"post": {
46554690
"description": "RegisterByUsername",

0 commit comments

Comments
 (0)