From f85929c575832e5ba73afa2a7f2626d0a6da7dd2 Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Sat, 20 Sep 2025 17:17:00 +0700 Subject: [PATCH] Add Refresh token --- config/configs.go | 6 ++- config/crypto.go | 23 +++++++++- config/jwt.go | 10 ++++- infra/development.yaml | 3 ++ internal/app/app.go | 3 +- internal/contract/user_contract.go | 8 ++-- internal/service/auth_service.go | 71 ++++++++++++++++++++++++------ 7 files changed, 100 insertions(+), 24 deletions(-) diff --git a/config/configs.go b/config/configs.go index b704bae..6a8dfe0 100644 --- a/config/configs.go +++ b/config/configs.go @@ -64,8 +64,10 @@ func LoadConfig() *Config { func (c *Config) Auth() *AuthConfig { return &AuthConfig{ - jwtTokenSecret: c.Jwt.Token.Secret, - jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL, + jwtTokenSecret: c.Jwt.Token.Secret, + jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL, + refreshTokenSecret: c.Jwt.RefreshToken.Secret, + refreshTokenExpiresTTL: c.Jwt.RefreshToken.ExpiresTTL, } } diff --git a/config/crypto.go b/config/crypto.go index e3e90c5..badbee5 100644 --- a/config/crypto.go +++ b/config/crypto.go @@ -3,8 +3,10 @@ package config import "time" type AuthConfig struct { - jwtTokenExpiresTTL int - jwtTokenSecret string + jwtTokenExpiresTTL int + jwtTokenSecret string + refreshTokenExpiresTTL int + refreshTokenSecret string } type JWT struct { @@ -20,3 +22,20 @@ func (c *AuthConfig) AccessTokenExpiresDate() time.Time { duration := time.Duration(c.jwtTokenExpiresTTL) return time.Now().UTC().Add(time.Minute * duration) } + +func (c *AuthConfig) RefreshTokenSecret() string { + return c.refreshTokenSecret +} + +func (c *AuthConfig) RefreshTokenExpiresDate() time.Time { + duration := time.Duration(c.refreshTokenExpiresTTL) + return time.Now().UTC().Add(time.Minute * duration) +} + +func (c *AuthConfig) AccessTokenTTL() time.Duration { + return time.Duration(c.jwtTokenExpiresTTL) * time.Minute +} + +func (c *AuthConfig) RefreshTokenTTL() time.Duration { + return time.Duration(c.refreshTokenExpiresTTL) * time.Minute +} diff --git a/config/jwt.go b/config/jwt.go index d8e9ff6..858c28e 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -1,8 +1,9 @@ package config type Jwt struct { - Token Token `mapstructure:"token"` - Customer Customer `mapstructure:"customer"` + Token Token `mapstructure:"token"` + RefreshToken RefreshToken `mapstructure:"refresh_token"` + Customer Customer `mapstructure:"customer"` } type Token struct { @@ -10,6 +11,11 @@ type Token struct { Secret string `mapstructure:"secret"` } +type RefreshToken struct { + ExpiresTTL int `mapstructure:"expires-ttl"` + Secret string `mapstructure:"secret"` +} + type Customer struct { ExpiresTTL int `mapstructure:"expires-ttl"` Secret string `mapstructure:"secret"` diff --git a/infra/development.yaml b/infra/development.yaml index 45172ed..6880e35 100644 --- a/infra/development.yaml +++ b/infra/development.yaml @@ -7,6 +7,9 @@ jwt: token: expires-ttl: 144000 secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs" + refresh_token: + expires-ttl: 7776000 # 3 months in minutes (90 days * 24 hours * 60 minutes) + secret: "R3fr3sh_T0k3n_S3cr3t_K3y_2024_P0S" customer: expires-ttl: 7776000 secret: "z8d5TlFCT58Q$i0%S^2M&3WtE$PMgd" diff --git a/internal/app/app.go b/internal/app/app.go index 5f4084e..28e93e6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -365,8 +365,7 @@ type services struct { func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services { authConfig := cfg.Auth() - jwtSecret := authConfig.AccessTokenSecret() - authService := service.NewAuthService(processors.userProcessor, jwtSecret) + authService := service.NewAuthService(processors.userProcessor, authConfig) organizationService := service.NewOrganizationService(processors.organizationProcessor) outletService := service.NewOutletService(processors.outletProcessor) outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor) diff --git a/internal/contract/user_contract.go b/internal/contract/user_contract.go index 617f5e7..5607005 100644 --- a/internal/contract/user_contract.go +++ b/internal/contract/user_contract.go @@ -40,9 +40,11 @@ type LoginRequest struct { } type LoginResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - User UserResponse `json:"user"` + Token string `json:"token"` + RefreshToken string `json:"refresh_token"` + ExpiresAt time.Time `json:"expires_at"` + RefreshExpiresAt time.Time `json:"refresh_expires_at"` + User UserResponse `json:"user"` } type UserResponse struct { diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index d00d51a..3a8d88c 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "apskel-pos-be/config" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" "apskel-pos-be/internal/transformer" @@ -23,9 +24,11 @@ type AuthService interface { } type AuthServiceImpl struct { - userProcessor UserProcessor - jwtSecret string - tokenTTL time.Duration + userProcessor UserProcessor + jwtSecret string + refreshSecret string + tokenTTL time.Duration + refreshTokenTTL time.Duration } type Claims struct { @@ -36,11 +39,13 @@ type Claims struct { jwt.RegisteredClaims } -func NewAuthService(userProcessor UserProcessor, jwtSecret string) AuthService { +func NewAuthService(userProcessor UserProcessor, authConfig *config.AuthConfig) AuthService { return &AuthServiceImpl{ - userProcessor: userProcessor, - jwtSecret: jwtSecret, - tokenTTL: 24 * time.Hour, + userProcessor: userProcessor, + jwtSecret: authConfig.AccessTokenSecret(), + refreshSecret: authConfig.RefreshTokenSecret(), + tokenTTL: authConfig.AccessTokenTTL(), + refreshTokenTTL: authConfig.RefreshTokenTTL(), } } @@ -71,10 +76,17 @@ func (s *AuthServiceImpl) Login(ctx context.Context, req *contract.LoginRequest) return nil, fmt.Errorf("failed to generate token: %w", err) } + refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse) + if err != nil { + return nil, fmt.Errorf("failed to generate refresh token: %w", err) + } + return &contract.LoginResponse{ - Token: token, - ExpiresAt: expiresAt, - User: *contractUserResponse, + Token: token, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, + RefreshExpiresAt: refreshExpiresAt, + User: *contractUserResponse, }, nil } @@ -119,10 +131,17 @@ func (s *AuthServiceImpl) RefreshToken(ctx context.Context, tokenString string) return nil, fmt.Errorf("failed to generate token: %w", err) } + refreshToken, refreshExpiresAt, err := s.generateRefreshToken(userResponse) + if err != nil { + return nil, fmt.Errorf("failed to generate refresh token: %w", err) + } + return &contract.LoginResponse{ - Token: newToken, - ExpiresAt: expiresAt, - User: *contractUserResponse, + Token: newToken, + RefreshToken: refreshToken, + ExpiresAt: expiresAt, + RefreshExpiresAt: refreshExpiresAt, + User: *contractUserResponse, }, nil } @@ -164,6 +183,32 @@ func (s *AuthServiceImpl) generateToken(user *models.UserResponse) (string, time return tokenString, expiresAt, nil } +func (s *AuthServiceImpl) generateRefreshToken(user *models.UserResponse) (string, time.Time, error) { + expiresAt := time.Now().Add(s.refreshTokenTTL) + + claims := &Claims{ + UserID: user.ID, + Email: user.Email, + Role: string(user.Role), + OrganizationID: user.OrganizationID, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "apskel-pos-refresh", + Subject: user.ID.String(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.refreshSecret)) + if err != nil { + return "", time.Time{}, err + } + + return tokenString, expiresAt, nil +} + func (s *AuthServiceImpl) parseToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {