From 9204b3d1ceeacd6ce1f5fc48ec6cb19d7b9d88fd Mon Sep 17 00:00:00 2001 From: Pajri Zahrawaani Ahmad Date: Fri, 1 Mar 2024 20:21:01 +0700 Subject: [PATCH 1/2] Refresh token feature - Store refresh token to db - If refresh token exist in db, login will redirecting --- config.example.json | 1 + config/jwt.go | 28 ++++- controller/login_controller.go | 119 ++++++++++++++---- domain/login_domain.go | 4 +- domain/refresh_token_domain.go | 22 ++++ middleware/middleware.go | 9 +- migrations/user.sql | 29 ++++- .../postgres/refresh_token_repository.go | 70 +++++++++++ route/login_route.go | 17 ++- route/route.go | 5 - usecase/login_usecase.go | 4 + usecase/refresh_token_usecase.go | 47 +++++++ 12 files changed, 303 insertions(+), 52 deletions(-) create mode 100644 domain/refresh_token_domain.go create mode 100644 repository/postgres/refresh_token_repository.go create mode 100644 usecase/refresh_token_usecase.go diff --git a/config.example.json b/config.example.json index 6e3294a..83ec196 100644 --- a/config.example.json +++ b/config.example.json @@ -7,6 +7,7 @@ "x-api-key": "njirlah", "access_token_secret": "BJIRSECRET", "access_token_expiry": 24, + "refresh_token_secret": "BJIRREFRESH", "refresh_token_expiry": 168 }, "database":{ diff --git a/config/jwt.go b/config/jwt.go index a3e8ec6..9631e30 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -67,6 +67,30 @@ func IsAuthorized(authToken string, secret string) (bool, error) { } -func IsExpired(authToken, secret string) (bool, string) { - return false, "" +func GetUsernameFromClaim(authToken string, secret string) (user string, err error) { + token, err := jwt.Parse(authToken, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Cannot signing method with algorithm: %v", token.Header["alg"]) + } + return []byte(secret), nil + }) + if err != nil { + if validErr, ok := err.(*jwt.ValidationError); ok { + if validErr.Errors&jwt.ValidationErrorMalformed != 0 { + log.Printf("ErrTokenMalformed: %v", err) + return "", err + } else if validErr.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { + log.Printf("ErrTokenExpired: %v", err) + return "", err + } + } + log.Printf("Error parsing token: %v", err) + return "false", err + } + claims := token.Claims.(jwt.MapClaims) + return claims["username"].(string), nil +} + +func IsExpired(authToken, secret string) (string, string, bool) { + return "", "", false } diff --git a/controller/login_controller.go b/controller/login_controller.go index 6bbffa4..6ca0789 100644 --- a/controller/login_controller.go +++ b/controller/login_controller.go @@ -10,8 +10,9 @@ import ( ) type LoginController struct { - LoginUseCase domain.LoginUseCase - Env *config.Config + LoginUseCase domain.LoginUseCase + RefreshTokenUseCase domain.RefreshTokenUseCase + Env *config.Config } func checkHashPass(hashed string, realPass string) bool { @@ -46,30 +47,68 @@ func (l LoginController) Login(gctx *gin.Context) { return } - accessToken, err := l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) + hasRefreshTokenData, err := l.RefreshTokenUseCase.GetRefreshToken(gctx, data.Username) if err != nil { gctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Internal Server Error", - "cause": err, + "error": "Error get refresh token", + "cause": err.Error(), }) return } - refreshToken, err := l.LoginUseCase.CreateAccessToken(data, l.Env.Server.RefreshTokenSecret, l.Env.Server.RefreshTokenExpiry) - if err != nil { - gctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Internal Server Error", - "cause": err, - }) + + var newAccessToken string + if hasRefreshTokenData == nil { + newAccessToken, err = l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err, + }) + return + } + + refreshToken, err := l.LoginUseCase.CreateRefreshToken(data, l.Env.Server.RefreshTokenSecret, l.Env.Server.RefreshTokenExpiry) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err, + }) + return + } + refreshTokenData := domain.RefreshTokenData{ + Username: data.Username, + RefreshToken: refreshToken, + } + + err = l.RefreshTokenUseCase.StoreRefreshToken(gctx, refreshTokenData) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err.Error(), + }) + return + } + + } else { + gctx.Request.Method = "GET" + gctx.Writer.Header().Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5qaXIgICAgICAgICAgICAgICAgIiwiZXhwIjoxNzA5MzAwNjAxfQ.spYgQzKUSq5V7soS2aAXrF7qx-gCCJKLZ-9WN8JDjLY") + gctx.Redirect(http.StatusSeeOther, "/user") + gctx.Next() return } resp := &domain.LoginResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, + AccessToken: newAccessToken, } gctx.JSON(http.StatusOK, resp) } +func (l LoginController) RefreshToken(gctx *gin.Context) { + gctx.JSON(http.StatusOK, gin.H{ + "status": "200", + }) +} + func (l LoginController) GetLogin(gctx *gin.Context) { data, err := l.LoginUseCase.CheckUser(gctx, "njir") @@ -81,27 +120,55 @@ func (l LoginController) GetLogin(gctx *gin.Context) { return } - accessToken, err := l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) + hasRefreshTokenData, err := l.RefreshTokenUseCase.GetRefreshToken(gctx, data.Username) if err != nil { gctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Internal Server Error", - "cause": err, + "error": "Error get refresh token", + "cause": err.Error(), }) return } - refreshToken, err := l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) - if err != nil { - gctx.JSON(http.StatusInternalServerError, gin.H{ - "error": "Internal Server Error", - "cause": err, - }) + resp := &domain.LoginResponse{} + if hasRefreshTokenData == nil { + resp.AccessToken, err = l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err, + }) + return + } + + refreshToken, err := l.LoginUseCase.CreateRefreshToken(data, l.Env.Server.RefreshTokenSecret, l.Env.Server.RefreshTokenExpiry) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err, + }) + return + } + refreshTokenData := domain.RefreshTokenData{ + Username: data.Username, + RefreshToken: refreshToken, + } + + err = l.RefreshTokenUseCase.StoreRefreshToken(gctx, refreshTokenData) + if err != nil { + gctx.JSON(http.StatusInternalServerError, gin.H{ + "error": "Internal Server Error", + "cause": err.Error(), + }) + return + } + + } else { + gctx.Request.Method = "GET" + gctx.Writer.Header().Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5qaXIgICAgICAgICAgICAgICAgIiwiZXhwIjoxNzA5MzAwNjAxfQ.spYgQzKUSq5V7soS2aAXrF7qx-gCCJKLZ-9WN8JDjLY") + gctx.Redirect(http.StatusSeeOther, "/user") + gctx.Next() return } - resp := &domain.LoginResponse{ - AccessToken: accessToken, - RefreshToken: refreshToken, - } gctx.JSON(http.StatusOK, resp) } diff --git a/domain/login_domain.go b/domain/login_domain.go index 4e30d1f..2a890bb 100644 --- a/domain/login_domain.go +++ b/domain/login_domain.go @@ -17,12 +17,12 @@ type JWTClaims struct { } type LoginResponse struct { - AccessToken string - RefreshToken string + AccessToken string } type LoginUseCase interface { CheckUser(c context.Context, usecase string) (*User, error) CreateAccessToken(user *User, secret string, expire int) (string, error) CreateRefreshToken(user *User, secret string, expire int) (string, error) + GetUsernameFromClaim(user string, secret string) (string, error) } diff --git a/domain/refresh_token_domain.go b/domain/refresh_token_domain.go new file mode 100644 index 0000000..6c66c76 --- /dev/null +++ b/domain/refresh_token_domain.go @@ -0,0 +1,22 @@ +package domain + +import "context" + +type RefreshTokenData struct { + Username string + RefreshToken string +} + +type RefreshTokenRepository interface { + StoreRefreshToken(context.Context, RefreshTokenData) (err error) + GetRefreshToken(context.Context, string) (*RefreshTokenData, error) + // UpdateRefreshToken(context.Context, string) (string, error) + // DeleteRefreshToken(context.Context, string) (bool, error) +} + +type RefreshTokenUseCase interface { + StoreRefreshToken(context.Context, RefreshTokenData) (err error) + GetRefreshToken(context.Context, string) (*RefreshTokenData, error) + // UpdateRefreshToken(context.Context, string) (string, error) + // DeleteRefreshToken(context.Context, string) (bool, error) +} diff --git a/middleware/middleware.go b/middleware/middleware.go index be23878..a3dca79 100644 --- a/middleware/middleware.go +++ b/middleware/middleware.go @@ -46,9 +46,8 @@ func JWTMiddleware(tokenSecret string) gin.HandlerFunc { ok, err := config.IsAuthorized(authToken, tokenSecret) if err != nil { gctx.JSON(http.StatusInternalServerError, gin.H{ - "Error": "Internal Status Error", - "middleware": "JWTMiddleware", - "Cause": err.Error(), + "Error": "Internal Status Error", + "Cause": err.Error(), }) gctx.Abort() return @@ -60,6 +59,8 @@ func JWTMiddleware(tokenSecret string) gin.HandlerFunc { gctx.Abort() return } + gctx.Next() + return } else { gctx.JSON(http.StatusUnauthorized, gin.H{ "error": "Unauthorized", @@ -67,7 +68,5 @@ func JWTMiddleware(tokenSecret string) gin.HandlerFunc { gctx.Abort() return } - gctx.Next() - return } } diff --git a/migrations/user.sql b/migrations/user.sql index 042e94e..805dffd 100644 --- a/migrations/user.sql +++ b/migrations/user.sql @@ -1,12 +1,23 @@ CREATE TABLE IF NOT EXISTS "User" ( - username CHAR(20) NOT NULL PRIMARY KEY, - full_name VARCHAR(50) NOT NULL, - email VARCHAR(50) NOT NULL, - password VARCHAR(16) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL + username CHAR(20) NOT NULL PRIMARY KEY, + full_name VARCHAR(50) NOT NULL, + email VARCHAR(50) NOT NULL, + password VARCHAR(16) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL ); + +CREATE TABLE IF NOT EXISTS "refresh_token" ( + id_refresh SERIAL PRIMARY KEY, + username CHAR(20) NOT NULL, + refresh_token TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY (username) REFERENCES "User"(username) ON UPDATE CASCADE ON DELETE CASCADE +); +CREATE INDEX refresh_token_index ON refresh_token(username) + CREATE OR REPLACE FUNCTION updated_auto_func() RETURNS TRIGGER LANGUAGE 'plpgsql' AS $$ @@ -24,6 +35,12 @@ CREATE OR REPLACE TRIGGER updated_auto FOR EACH ROW EXECUTE PROCEDURE updated_auto_func(); +CREATE OR REPLACE TRIGGER updated_auto + BEFORE UPDATE + ON + "refresh_token" + FOR EACH ROW +EXECUTE PROCEDURE updated_auto_func(); INSERT INTO "User" (username,full_name,email,password) VALUES ('njir','njirlah coeg','njircoeg@tahoo.com','thispassword'), diff --git a/repository/postgres/refresh_token_repository.go b/repository/postgres/refresh_token_repository.go new file mode 100644 index 0000000..5dd15c6 --- /dev/null +++ b/repository/postgres/refresh_token_repository.go @@ -0,0 +1,70 @@ +package postgres + +import ( + "context" + "database/sql" + "log" + + "github.com/jrione/gin-crud/domain" +) + +type postgreRefreshTokenRepository struct { + DBClient *sql.DB +} + +func NewRefreshTokenRepository(conn *sql.DB) domain.RefreshTokenRepository { + return &postgreRefreshTokenRepository{ + DBClient: conn, + } +} + +func (p *postgreRefreshTokenRepository) StoreRefreshToken(ctx context.Context, data domain.RefreshTokenData) (err error) { + query := `INSERT INTO refresh_token(username,refresh_token) VALUES( $1, $2 )` + state, err := p.DBClient.PrepareContext(ctx, query) + if err != nil { + log.Printf("Error PrepareContext: %s", err) + return err + } + defer state.Close() + _, err = state.ExecContext(ctx, data.Username, data.RefreshToken) + if err != nil { + log.Printf("Error Exec: %s", err) + return err + } + return nil +} + +func (p *postgreRefreshTokenRepository) GetRefreshToken(ctx context.Context, us string) (res *domain.RefreshTokenData, err error) { + query := `SELECT username,refresh_token FROM refresh_token WHERE username=$1` + state, err := p.DBClient.PrepareContext(ctx, query) + log.Print(query) + if err != nil { + log.Printf("Error PrepareContext: %s", err) + return nil, err + } + defer state.Close() + row := state.QueryRow(us) + + res = &domain.RefreshTokenData{} + err = row.Scan( + &res.Username, + &res.RefreshToken, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil + } + log.Print(err) + return nil, err + } + return res, nil + +} + +// func (p *postgreRefreshTokenRepository) UpdateRefreshToken(ctx context.Context, refreshToken string) (string, error) { +// return "", nil +// } + +// func (p *postgreRefreshTokenRepository) DeleteRefreshToken(ctx context.Context, refreshToken string) (ok bool, err error) { +// return false, nil +// } diff --git a/route/login_route.go b/route/login_route.go index 4ff7485..acac09b 100644 --- a/route/login_route.go +++ b/route/login_route.go @@ -8,19 +8,24 @@ import ( "github.com/jrione/gin-crud/config" "github.com/jrione/gin-crud/controller" - loginRepo "github.com/jrione/gin-crud/repository/postgres" - loginUseCase "github.com/jrione/gin-crud/usecase" + repo "github.com/jrione/gin-crud/repository/postgres" + useCase "github.com/jrione/gin-crud/usecase" ) func NewLoginRoute(env *config.Config, timeout time.Duration, dbclient *sql.DB, gr *gin.RouterGroup) { - lr := loginRepo.NewUserRepository(dbclient) - lu := loginUseCase.NewLoginUseCase(lr, timeout) + lr := repo.NewUserRepository(dbclient) + lu := useCase.NewLoginUseCase(lr, timeout) + + rr := repo.NewRefreshTokenRepository(dbclient) + ru := useCase.NewRefreshTokenUseCase(rr, timeout) lc := &controller.LoginController{ - LoginUseCase: lu, - Env: env, + LoginUseCase: lu, + RefreshTokenUseCase: ru, + Env: env, } gr.POST("/login", lc.Login) + gr.POST("/refresh", lc.RefreshToken) gr.GET("/getLogin", lc.GetLogin) } diff --git a/route/route.go b/route/route.go index 31a7a62..eed0ed2 100644 --- a/route/route.go +++ b/route/route.go @@ -19,11 +19,6 @@ func SetupRoute(env *config.Config, timeout time.Duration, dbclient *sql.DB, r * NewLoginRoute(env, timeout, dbclient, authRouter) sessionRouter := r.Group("") - // store := cookie.NewStore([]byte("secret")) - // store.Options(sessions.Options{MaxAge: int(60 * time.Minute / time.Second)}) - // sessionRouter.Use(sessions.Sessions("sess", store)) - // sessionRouter.Use(middleware.SessionMiddleware()) - sessionRouter.Use(middleware.JWTMiddleware(env.Server.AccessTokenSecret)) NewUserRoute(env, timeout, dbclient, sessionRouter) diff --git a/usecase/login_usecase.go b/usecase/login_usecase.go index aca103b..9d0b390 100644 --- a/usecase/login_usecase.go +++ b/usecase/login_usecase.go @@ -36,3 +36,7 @@ func (l *loginUseCase) CreateAccessToken(user *domain.User, secret string, expir func (l *loginUseCase) CreateRefreshToken(user *domain.User, secret string, expire int) (refreshToken string, err error) { return jwtConfig.CreateRefreshToken(user, secret, expire) } + +func (l *loginUseCase) GetUsernameFromClaim(user string, secret string) (string, error) { + return jwtConfig.GetUsernameFromClaim(user, secret) +} diff --git a/usecase/refresh_token_usecase.go b/usecase/refresh_token_usecase.go new file mode 100644 index 0000000..f26035c --- /dev/null +++ b/usecase/refresh_token_usecase.go @@ -0,0 +1,47 @@ +package usecase + +import ( + "context" + "time" + + "github.com/jrione/gin-crud/domain" +) + +type refreshTokenUseCase struct { + refreshTokenRepository domain.RefreshTokenRepository + contextTimeout time.Duration +} + +func NewRefreshTokenUseCase(refreshTokenRepo domain.RefreshTokenRepository, timeout time.Duration) domain.RefreshTokenUseCase { + return &refreshTokenUseCase{ + refreshTokenRepository: refreshTokenRepo, + contextTimeout: timeout, + } +} + +func (r *refreshTokenUseCase) StoreRefreshToken(ctx context.Context, refreshTokenData domain.RefreshTokenData) (err error) { + ctx, cancel := context.WithTimeout(ctx, r.contextTimeout) + defer cancel() + err = r.refreshTokenRepository.StoreRefreshToken(ctx, refreshTokenData) + return +} + +func (r *refreshTokenUseCase) GetRefreshToken(ctx context.Context, username string) (res *domain.RefreshTokenData, err error) { + ctx, cancel := context.WithTimeout(ctx, r.contextTimeout) + defer cancel() + res, err = r.refreshTokenRepository.GetRefreshToken(ctx, username) + return +} + +// func (r *refreshTokenUseCase) UpdateRefreshToken(ctx context.Context, refreshToken string) (string, error) { +// ctx, cancel := context.WithTimeout(ctx, r.contextTimeout) +// defer cancel() +// return "", nil +// } + +// func (r *refreshTokenUseCase) DeleteRefreshToken(ctx context.Context, refreshToken string) (ok bool, err error) { +// ctx, cancel := context.WithTimeout(ctx, r.contextTimeout) +// defer cancel() + +// return false, nil +// } From 8300ae9b6ee130e800e75fa0143137fcd65274eb Mon Sep 17 00:00:00 2001 From: Pajri Zahrawaani Ahmad Date: Fri, 1 Mar 2024 20:26:42 +0700 Subject: [PATCH 2/2] ketinggalan satu --- controller/login_controller.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/controller/login_controller.go b/controller/login_controller.go index 6ca0789..062e38f 100644 --- a/controller/login_controller.go +++ b/controller/login_controller.go @@ -56,9 +56,9 @@ func (l LoginController) Login(gctx *gin.Context) { return } - var newAccessToken string + resp := &domain.LoginResponse{ if hasRefreshTokenData == nil { - newAccessToken, err = l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) + resp.AccessToken, err = l.LoginUseCase.CreateAccessToken(data, l.Env.Server.AccessTokenSecret, l.Env.Server.AccessTokenExpiry) if err != nil { gctx.JSON(http.StatusInternalServerError, gin.H{ "error": "Internal Server Error", @@ -97,9 +97,6 @@ func (l LoginController) Login(gctx *gin.Context) { return } - resp := &domain.LoginResponse{ - AccessToken: newAccessToken, - } gctx.JSON(http.StatusOK, resp) }