Repository: appleboy/gin-jwt

Issue:
Refresh Token implementation and RFC 6749
As far as I see gin-jwt consider Access token as same as Refresh token.
When we login, in response we can find a token that later we can send to refresh endpoint.
Additionally, we have also MaxRefresh settings, that allows us to refresh token, even if it is expired.
As far as I understand this approach increases security risks. The idea of refresh token - get it once, hide it better than access token and use it only for auth purposes, but not for access to resources.

Here RFC that clarify flow: https://tools.ietf.org/html/rfc6749#section-1.4.

Question is: what is the reason that gin-gwt implement it like this? How about to follow RFC6749?


Discussion:
@ewgRa: Any update?@meruiden: Any update? it has been more then a year since the initial request for this feature@merfrei: For the same reason I have decided not to use it and implement my own. A shame.@gnuletik: @appleboy would you consider merging a PR implementing this ? That would be a breaking change for v3.0 I guess.@axispx: This issue makes this package non usable for me.@YKPDang: You could also explicitly state that in the docs the refresh token isn't implemented as one would expect, which would make everything a lot clearer@appleboy: I will take it.

Solution patch:
diff --git a/README.md b/README.md
index 4657432..8f63800 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,8 @@ Easily add login, token refresh, and authorization to your Gin applications.
 - 🛡️ Customizable authentication, authorization, and claims
 - 🍪 Cookie and header token support
 - 📝 Easy integration and clear API
+- 🔐 RFC 6749 compliant refresh tokens (OAuth 2.0 standard)
+- 🗄️ Pluggable refresh token storage (in-memory, Redis, etc.)
 
 ---
 
@@ -54,6 +56,10 @@ Easily add login, token refresh, and authorization to your Gin applications.
 > JWT tokens with weak secrets (e.g., short or simple passwords) are vulnerable to brute-force attacks.
 > **Recommendation:** Use strong, long secrets or `RS256` tokens.
 > See the [jwt-cracker repository](https://github.com/lmammino/jwt-cracker) for more information.
+> **OAuth 2.0 Security:**
+> This library follows RFC 6749 OAuth 2.0 standards by default, using separate opaque refresh tokens
+> that are stored server-side and rotated on each use. This provides better security than using
+> JWT tokens for both access and refresh purposes.
 
 ---
 
@@ -87,6 +93,7 @@ import (
 
   jwt "github.com/appleboy/gin-jwt/v2"
   "github.com/gin-gonic/gin"
+  "github.com/golang-jwt/jwt/v5"
 )
 
 type login struct {
@@ -135,10 +142,11 @@ func main() {
 
 func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) {
   r.POST("/login", handle.LoginHandler)
+  r.POST("/refresh", handle.RefreshHandler) // RFC 6749 compliant refresh endpoint
   r.NoRoute(handle.MiddlewareFunc(), handleNoRoute())
 
   auth := r.Group("/auth", handle.MiddlewareFunc())
-  auth.GET("/refresh_token", handle.RefreshHandler)
+  auth.POST("/refresh_token", handle.RefreshHandler) // Alternative refresh endpoint
   auth.GET("/hello", helloHandler)
 }
 
@@ -271,8 +279,14 @@ http -v --json POST localhost:8000/login username=admin password=admin
 
 ### Refresh Token
 
+Using RFC 6749 compliant refresh tokens (default behavior):
+
 ```sh
-http -v -f GET localhost:8000/auth/refresh_token "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"
+# First login to get refresh token
+http -v --json POST localhost:8000/login username=admin password=admin
+
+# Use refresh token to get new access token
+http -v --json POST localhost:8000/auth/refresh_token refresh_token=your_refresh_token_here
 ```
 
 ![Refresh screenshot](screenshot/refresh_token.png)
@@ -373,11 +387,11 @@ This should likely just return back to the user the http status code, if logout
 
 PROVIDED: `RefreshHandler`:
 
-This is a provided function to be called on any refresh token endpoint. If the token passed in is was issued within the `MaxRefreshTime` time frame, then this handler will create/set a new token similar to the `LoginHandler`, and pass this token into `RefreshResponse`
+This is a provided function to be called on any refresh token endpoint. The handler expects a `refresh_token` parameter (RFC 6749 compliant) and validates it against the server-side token store. If the refresh token is valid and not expired, the handler will create a new access token and refresh token, revoke the old refresh token, and pass the new tokens into `RefreshResponse`. This follows OAuth 2.0 security best practices by rotating refresh tokens.
 
 OPTIONAL: `RefreshResponse`:
 
-This should likely return a JSON of the token back to the user, similar to `LoginResponse`
+This should return a JSON response containing the new `access_token`, `token_type`, `expires_in`, and `refresh_token` fields, following RFC 6749 token response format.
 
 ### Failures with logging in, bad tokens, or lacking privileges
 
diff --git a/README.zh-CN.md b/README.zh-CN.md
index e51c449..92ffef5 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -47,15 +47,21 @@
 - 🛡️ 可自定义认证、授权与 Claims
 - 🍪 支持 Cookie 与 Header Token
 - 📝 易于集成，API 清晰
+- 🔐 符合 RFC 6749 规范的刷新令牌（OAuth 2.0 标准）
+- 🗄️ 可插拔的刷新令牌存储（内存、Redis 等）
 
 ---
 
 ## 安全性注意事项
 
-> **警告：**  
-> 使用弱密码（如短或简单密码）的 JWT Token 易受暴力破解攻击。  
-> **建议：**请使用强且长的密钥或 `RS256` Token。  
+> **警告：**
+> 使用弱密码（如短或简单密码）的 JWT Token 易受暴力破解攻击。
+> **建议：**请使用强且长的密钥或 `RS256` Token。
 > 详见 [jwt-cracker repository](https://github.com/lmammino/jwt-cracker)。
+> **OAuth 2.0 安全性：**
+> 此库默认遵循 RFC 6749 OAuth 2.0 标准，使用分离的不透明刷新令牌，
+> 这些令牌在服务器端存储并在每次使用时轮替。这比同时使用 JWT 令牌
+> 作为访问和刷新用途提供更好的安全性。
 
 ---
 
@@ -104,8 +110,14 @@ http -v --json POST localhost:8000/login username=admin password=admin
 
 ### 刷新 Token
 
+使用符合 RFC 6749 规范的刷新令牌（默认行为）：
+
 ```sh
-http -v -f GET localhost:8000/auth/refresh_token "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"
+# 首先登录获取刷新令牌
+http -v --json POST localhost:8000/login username=admin password=admin
+
+# 使用刷新令牌获取新的访问令牌
+http -v --json POST localhost:8000/auth/refresh_token refresh_token=your_refresh_token_here
 ```
 
 ![刷新截图](screenshot/refresh_token.png)
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 3a2810b..8c2a211 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -47,15 +47,21 @@
 - 🛡️ 可自訂驗證、授權與 Claims
 - 🍪 支援 Cookie 與 Header Token
 - 📝 易於整合，API 清晰
+- 🔐 符合 RFC 6749 規範的刷新 Token（OAuth 2.0 標準）
+- 🗄️ 可插拔的刷新 Token 儲存（記憶體、Redis 等）
 
 ---
 
 ## 安全性注意事項
 
-> **警告：**  
-> 使用弱密碼（如短或簡單密碼）的 JWT Token 易受暴力破解攻擊。  
-> **建議：**請使用強且長的密鑰或 `RS256` Token。  
+> **警告：**
+> 使用弱密碼（如短或簡單密碼）的 JWT Token 易受暴力破解攻擊。
+> **建議：**請使用強且長的密鑰或 `RS256` Token。
 > 詳見 [jwt-cracker repository](https://github.com/lmammino/jwt-cracker)。
+> **OAuth 2.0 安全性：**
+> 此函式庫預設遵循 RFC 6749 OAuth 2.0 標準，使用分離的不透明刷新 Token，
+> 這些 Token 在伺服器端儲存並在每次使用時輪替。這比同時使用 JWT Token
+> 作為存取與刷新用途提供更好的安全性。
 
 ---
 
@@ -104,8 +110,14 @@ http -v --json POST localhost:8000/login username=admin password=admin
 
 ### 刷新 Token
 
+使用符合 RFC 6749 規範的刷新 Token（預設行為）：
+
 ```sh
-http -v -f GET localhost:8000/auth/refresh_token "Authorization:Bearer xxxxxxxxx"  "Content-Type: application/json"
+# 首先登入取得刷新 Token
+http -v --json POST localhost:8000/login username=admin password=admin
+
+# 使用刷新 Token 取得新的存取 Token
+http -v --json POST localhost:8000/auth/refresh_token refresh_token=your_refresh_token_here
 ```
 
 ![刷新截圖](screenshot/refresh_token.png)
diff --git a/_example/basic/go.mod b/_example/basic/go.mod
index 49aa262..67ff73c 100644
--- a/_example/basic/go.mod
+++ b/_example/basic/go.mod
@@ -17,7 +17,7 @@ require (
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/go-playground/validator/v10 v10.27.0 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
-	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+	github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
diff --git a/_example/basic/go.sum b/_example/basic/go.sum
index 7c51f05..fb133d2 100644
--- a/_example/basic/go.sum
+++ b/_example/basic/go.sum
@@ -28,8 +28,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
 github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
 github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
-github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
-github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
+github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
diff --git a/_example/basic/server.go b/_example/basic/server.go
index 0a30240..e917a50 100644
--- a/_example/basic/server.go
+++ b/_example/basic/server.go
@@ -8,6 +8,7 @@ import (
 
 	jwt "github.com/appleboy/gin-jwt/v2"
 	"github.com/gin-gonic/gin"
+	gojwt "github.com/golang-jwt/jwt/v5"
 )
 
 type login struct {
@@ -55,12 +56,16 @@ func main() {
 }
 
 func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) {
+	// Public routes
 	r.POST("/login", handle.LoginHandler)
+	r.POST("/refresh", handle.RefreshHandler) // RFC 6749 compliant refresh endpoint
+
 	r.NoRoute(handle.MiddlewareFunc(), handleNoRoute())
 
+	// Protected routes
 	auth := r.Group("/auth", handle.MiddlewareFunc())
-	auth.GET("/refresh_token", handle.RefreshHandler)
 	auth.GET("/hello", helloHandler)
+	auth.POST("/logout", handle.LogoutHandler) // Logout with refresh token revocation
 }
 
 func handlerMiddleWare(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc {
@@ -93,14 +98,14 @@ func initParams() *jwt.GinJWTMiddleware {
 	}
 }
 
-func payloadFunc() func(data interface{}) jwt.MapClaims {
-	return func(data interface{}) jwt.MapClaims {
+func payloadFunc() func(data interface{}) gojwt.MapClaims {
+	return func(data interface{}) gojwt.MapClaims {
 		if v, ok := data.(*User); ok {
-			return jwt.MapClaims{
+			return gojwt.MapClaims{
 				identityKey: v.UserName,
 			}
 		}
-		return jwt.MapClaims{}
+		return gojwt.MapClaims{}
 	}
 }
 
diff --git a/auth_jwt.go b/auth_jwt.go
index 91a50cd..141fdba 100644
--- a/auth_jwt.go
+++ b/auth_jwt.go
@@ -1,7 +1,9 @@
 package jwt
 
 import (
+	"crypto/rand"
 	"crypto/rsa"
+	"encoding/base64"
 	"errors"
 	"log"
 	"net/http"
@@ -9,6 +11,9 @@ import (
 	"strings"
 	"time"
 
+	"github.com/appleboy/gin-jwt/v2/core"
+	"github.com/appleboy/gin-jwt/v2/store"
+
 	"github.com/gin-gonic/gin"
 	"github.com/golang-jwt/jwt/v5"
 	"github.com/youmark/pkcs8"
@@ -158,6 +163,20 @@ type GinJWTMiddleware struct {
 
 	// Default value is "exp"
 	ExpField string
+
+	// RefreshTokenTimeout specifies how long refresh tokens are valid
+	// Defaults to 30 days if not set
+	RefreshTokenTimeout time.Duration
+
+	// RefreshTokenStore interface for storing and retrieving refresh tokens
+	// If nil, an in-memory store will be used
+	RefreshTokenStore core.TokenStore
+
+	// RefreshTokenLength specifies the byte length of refresh tokens (default: 32)
+	RefreshTokenLength int
+
+	// inMemoryStore internal fallback refresh token store
+	inMemoryStore *store.InMemoryRefreshTokenStore
 }
 
 var (
@@ -220,6 +239,12 @@ var (
 
 	// IdentityKey default identity key
 	IdentityKey = "identity"
+
+	// ErrInvalidRefreshToken indicates the refresh token is invalid or expired
+	ErrInvalidRefreshToken = errors.New("invalid or expired refresh token")
+
+	// ErrRefreshTokenNotFound indicates the refresh token was not found in storage
+	ErrRefreshTokenNotFound = errors.New("refresh token not found")
 )
 
 // New creates and initializes a new GinJWTMiddleware instance
@@ -433,6 +458,18 @@ func (mw *GinJWTMiddleware) MiddlewareInit() error {
 		mw.ExpField = "exp"
 	}
 
+	// Initialize refresh token settings (RFC 6749 compliant by default)
+	if mw.RefreshTokenTimeout == 0 {
+		mw.RefreshTokenTimeout = 30 * 24 * time.Hour // 30 days default
+	}
+	if mw.RefreshTokenLength == 0 {
+		mw.RefreshTokenLength = 32 // 256 bits default
+	}
+	if mw.RefreshTokenStore == nil {
+		mw.inMemoryStore = store.NewInMemoryRefreshTokenStore()
+		mw.RefreshTokenStore = mw.inMemoryStore
+	}
+
 	// bypass other key settings if KeyFunc is set
 	if mw.KeyFunc != nil {
 		return nil
@@ -553,11 +590,50 @@ func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
 
 	mw.SetCookie(c, tokenString)
 
+	// Generate RFC 6749 compliant refresh token
+	refreshToken, err := mw.generateRefreshToken()
+	if err != nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
+		return
+	}
+
+	if err := mw.storeRefreshToken(refreshToken, data); err != nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
+		return
+	}
+
+	// Store refresh token in context for response generation
+	c.Set("REFRESH_TOKEN", refreshToken)
+
 	mw.LoginResponse(c, http.StatusOK, tokenString, expire)
 }
 
-// LogoutHandler can be used by clients to remove the jwt cookie (if set)
+func (mw *GinJWTMiddleware) extractRefreshToken(c *gin.Context) string {
+	token := c.PostForm("refresh_token")
+	if token == "" {
+		token = c.Query("refresh_token")
+	}
+	if token == "" {
+		var reqBody struct {
+			RefreshToken string `json:"refresh_token"`
+		}
+		if err := c.ShouldBindJSON(&reqBody); err == nil {
+			token = reqBody.RefreshToken
+		}
+	}
+	return token
+}
+
+// LogoutHandler can be used by clients to remove the jwt cookie and revoke refresh token
 func (mw *GinJWTMiddleware) LogoutHandler(c *gin.Context) {
+	// Handle refresh token revocation (RFC 6749 compliant)
+	refreshToken := mw.extractRefreshToken(c)
+	if refreshToken != "" {
+		if err := mw.revokeRefreshToken(refreshToken); err != nil {
+			log.Printf("Failed to revoke refresh token on logout: %v", err)
+		}
+	}
+
 	// delete auth cookie
 	if mw.SendCookie {
 		if mw.CookieSameSite != 0 {
@@ -589,48 +665,99 @@ func (mw *GinJWTMiddleware) signedString(token *jwt.Token) (string, error) {
 	return tokenString, err
 }
 
-// RefreshHandler can be used to refresh a token. The token still needs to be valid on refresh.
-// Shall be put under an endpoint that is using the GinJWTMiddleware.
-// Reply will be of the form {"token": "TOKEN"}.
+// generateRefreshToken creates a cryptographically secure random refresh token
+func (mw *GinJWTMiddleware) generateRefreshToken() (string, error) {
+	bytes := make([]byte, mw.RefreshTokenLength)
+	if _, err := rand.Read(bytes); err != nil {
+		return "", err
+	}
+	return base64.URLEncoding.EncodeToString(bytes), nil
+}
+
+// storeRefreshToken stores a refresh token with user data
+func (mw *GinJWTMiddleware) storeRefreshToken(token string, userData interface{}) error {
+	expiry := mw.TimeFunc().Add(mw.RefreshTokenTimeout)
+	return mw.RefreshTokenStore.Set(token, userData, expiry)
+}
+
+// validateRefreshToken validates a refresh token and returns associated user data
+func (mw *GinJWTMiddleware) validateRefreshToken(token string) (interface{}, error) {
+	userData, err := mw.RefreshTokenStore.Get(token)
+	if err != nil {
+		if err == core.ErrRefreshTokenNotFound {
+			return nil, ErrInvalidRefreshToken
+		}
+		return nil, err
+	}
+	return userData, nil
+}
+
+// revokeRefreshToken removes a refresh token from storage
+func (mw *GinJWTMiddleware) revokeRefreshToken(token string) error {
+	return mw.RefreshTokenStore.Delete(token)
+}
+
+// RefreshHandler can be used to refresh a token using RFC 6749 compliant refresh tokens.
+// This handler expects a refresh_token parameter and returns a new access token and refresh token.
+// Reply will be of the form {"access_token": "TOKEN", "refresh_token": "REFRESH_TOKEN"}.
 func (mw *GinJWTMiddleware) RefreshHandler(c *gin.Context) {
-	tokenString, expire, err := mw.RefreshToken(c)
+	mw.rfc6749RefreshHandler(c)
+}
+
+// rfc6749RefreshHandler handles refresh token requests in RFC 6749 compliant mode
+func (mw *GinJWTMiddleware) rfc6749RefreshHandler(c *gin.Context) {
+	// Extract refresh token from request
+	refreshToken := mw.extractRefreshToken(c)
+	if refreshToken == "" {
+		mw.unauthorized(c, http.StatusBadRequest, "missing refresh_token parameter")
+		return
+	}
+
+	// Validate refresh token
+	userData, err := mw.validateRefreshToken(refreshToken)
 	if err != nil {
 		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
 		return
 	}
 
-	mw.RefreshResponse(c, http.StatusOK, tokenString, expire)
-}
-
-// RefreshToken refresh token and check if token is expired
-func (mw *GinJWTMiddleware) RefreshToken(c *gin.Context) (string, time.Time, error) {
-	claims, err := mw.CheckIfTokenExpire(c)
+	// Generate new access token
+	newTokenString, expire, err := mw.TokenGenerator(userData)
 	if err != nil {
-		return "", time.Now(), err
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
+		return
 	}
 
-	// Create the token
-	newToken := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
-	newClaims, ok := newToken.Claims.(jwt.MapClaims)
-	if !ok {
-		return "", time.Now(), ErrFailedTokenCreation
+	// Generate new refresh token and revoke old one
+	newRefreshToken, err := mw.generateRefreshToken()
+	if err != nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
+		return
 	}
 
-	for key := range claims {
-		newClaims[key] = claims[key]
+	// Store new refresh token
+	if err := mw.storeRefreshToken(newRefreshToken, userData); err != nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
+		return
 	}
 
-	expire := mw.TimeFunc().Add(mw.TimeoutFunc(claims))
-	newClaims[mw.ExpField] = expire.Unix()
-	newClaims["orig_iat"] = mw.TimeFunc().Unix()
-	tokenString, err := mw.signedString(newToken)
-	if err != nil {
-		return "", time.Now(), err
+	// Revoke old refresh token
+	if err := mw.revokeRefreshToken(refreshToken); err != nil {
+		// Log error but don't fail the request
+		log.Printf("Failed to revoke old refresh token: %v", err)
 	}
 
-	mw.SetCookie(c, tokenString)
+	mw.SetCookie(c, newTokenString)
 
-	return tokenString, expire, nil
+	// Store new refresh token in context for response generation
+	c.Set("REFRESH_TOKEN", newRefreshToken)
+
+	mw.RefreshResponse(c, http.StatusOK, newTokenString, expire)
+}
+
+// RefreshToken is deprecated. Use RefreshHandler instead for RFC 6749 compliant refresh tokens.
+// This method now returns an error to encourage migration to the proper refresh token flow.
+func (mw *GinJWTMiddleware) RefreshToken(c *gin.Context) (string, time.Time, error) {
+	return "", time.Now(), errors.New("RefreshToken method is deprecated: use RefreshHandler with refresh_token parameter for RFC 6749 compliant behavior")
 }
 
 // CheckIfTokenExpire check if token expire
@@ -909,19 +1036,20 @@ func (mw *GinJWTMiddleware) handleTokenError(c *gin.Context, err error) {
 	}
 }
 
-// generateTokenResponse creates a standard token response with refresh token
+// generateTokenResponse creates a RFC 6749 compliant token response with refresh token
 func (mw *GinJWTMiddleware) generateTokenResponse(c *gin.Context, token string, expire time.Time) (gin.H, error) {
-	refreshToken, _, err := mw.RefreshToken(c)
-	if err != nil {
-		return nil, err
+	response := gin.H{
+		"access_token": token,
+		"token_type":   "Bearer",
+		"expires_in":   int(time.Until(expire).Seconds()),
 	}
 
-	return gin.H{
-		"access_token":  token,
-		"token_type":    "Bearer",
-		"expires_in":    int(time.Until(expire).Seconds()),
-		"refresh_token": refreshToken,
-	}, nil
+	// Always include refresh token from context (RFC 6749 compliant)
+	if refreshToken, exists := c.Get("REFRESH_TOKEN"); exists {
+		response["refresh_token"] = refreshToken
+	}
+
+	return response, nil
 }
 
 // ClearSensitiveData clears sensitive data from memory
@@ -964,4 +1092,9 @@ func (mw *GinJWTMiddleware) ClearSensitiveData() {
 	// due to Go's garbage collector, but setting to nil helps
 	mw.privKey = nil
 	mw.pubKey = nil
+
+	// Clear refresh token store if using in-memory store
+	if mw.inMemoryStore != nil {
+		mw.inMemoryStore.Clear()
+	}
 }
diff --git a/core/store.go b/core/store.go
new file mode 100644
index 0000000..7bae7f0
--- /dev/null
+++ b/core/store.go
@@ -0,0 +1,50 @@
+// Package core provides core interfaces and types for gin-jwt
+package core
+
+import (
+	"errors"
+	"time"
+)
+
+var (
+	// ErrRefreshTokenNotFound indicates the refresh token was not found in storage
+	ErrRefreshTokenNotFound = errors.New("refresh token not found")
+
+	// ErrRefreshTokenExpired indicates the refresh token has expired
+	ErrRefreshTokenExpired = errors.New("refresh token expired")
+)
+
+// TokenStore defines the interface for storing and retrieving refresh tokens
+type TokenStore interface {
+	// Set stores a refresh token with associated user data and expiration
+	// Returns an error if the operation fails
+	Set(token string, userData interface{}, expiry time.Time) error
+
+	// Get retrieves user data associated with a refresh token
+	// Returns ErrRefreshTokenNotFound if token doesn't exist or is expired
+	Get(token string) (interface{}, error)
+
+	// Delete removes a refresh token from storage
+	// Returns an error if the operation fails, but should not error if token doesn't exist
+	Delete(token string) error
+
+	// Cleanup removes expired tokens (optional, for cleanup routines)
+	// Returns the number of tokens cleaned up and any error encountered
+	Cleanup() (int, error)
+
+	// Count returns the total number of active refresh tokens
+	// Useful for monitoring and debugging
+	Count() (int, error)
+}
+
+// RefreshTokenData holds the data stored with each refresh token
+type RefreshTokenData struct {
+	UserData interface{} `json:"user_data"`
+	Expiry   time.Time   `json:"expiry"`
+	Created  time.Time   `json:"created"`
+}
+
+// IsExpired checks if the token data has expired
+func (r *RefreshTokenData) IsExpired() bool {
+	return time.Now().After(r.Expiry)
+}
\ No newline at end of file
diff --git a/store/memory.go b/store/memory.go
new file mode 100644
index 0000000..ace2012
--- /dev/null
+++ b/store/memory.go
@@ -0,0 +1,137 @@
+package store
+
+import (
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/appleboy/gin-jwt/v2/core"
+)
+
+// InMemoryRefreshTokenStore provides a simple in-memory refresh token store
+// This implementation is thread-safe and suitable for single-instance applications
+// For distributed systems, consider using Redis or database-based implementations
+type InMemoryRefreshTokenStore struct {
+	tokens map[string]*core.RefreshTokenData
+	mu     sync.RWMutex
+}
+
+// NewInMemoryRefreshTokenStore creates a new in-memory refresh token store
+func NewInMemoryRefreshTokenStore() *InMemoryRefreshTokenStore {
+	return &InMemoryRefreshTokenStore{
+		tokens: make(map[string]*core.RefreshTokenData),
+	}
+}
+
+// Set stores a refresh token with associated user data and expiration
+func (s *InMemoryRefreshTokenStore) Set(token string, userData interface{}, expiry time.Time) error {
+	if token == "" {
+		return errors.New("token cannot be empty")
+	}
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.tokens[token] = &core.RefreshTokenData{
+		UserData: userData,
+		Expiry:   expiry,
+		Created:  time.Now(),
+	}
+
+	return nil
+}
+
+// Get retrieves user data associated with a refresh token
+func (s *InMemoryRefreshTokenStore) Get(token string) (interface{}, error) {
+	if token == "" {
+		return nil, ErrRefreshTokenNotFound
+	}
+
+	s.mu.RLock()
+	data, exists := s.tokens[token]
+	s.mu.RUnlock()
+
+	if !exists {
+		return nil, core.ErrRefreshTokenNotFound
+	}
+
+	if data.IsExpired() {
+		// Clean up expired token
+		s.mu.Lock()
+		delete(s.tokens, token)
+		s.mu.Unlock()
+		return nil, core.ErrRefreshTokenNotFound
+	}
+
+	return data.UserData, nil
+}
+
+// Delete removes a refresh token from storage
+func (s *InMemoryRefreshTokenStore) Delete(token string) error {
+	if token == "" {
+		return nil // No error for empty token deletion
+	}
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	delete(s.tokens, token)
+	return nil
+}
+
+// Cleanup removes expired tokens and returns the number of tokens cleaned up
+func (s *InMemoryRefreshTokenStore) Cleanup() (int, error) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	var cleaned int
+	now := time.Now()
+
+	for token, data := range s.tokens {
+		if now.After(data.Expiry) {
+			delete(s.tokens, token)
+			cleaned++
+		}
+	}
+
+	return cleaned, nil
+}
+
+// Count returns the total number of active refresh tokens
+func (s *InMemoryRefreshTokenStore) Count() (int, error) {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	return len(s.tokens), nil
+}
+
+// GetAll returns all active tokens (for debugging/monitoring purposes)
+// Note: This method is not part of the RefreshTokenStorer interface
+// and should be used carefully in production environments
+func (s *InMemoryRefreshTokenStore) GetAll() map[string]*core.RefreshTokenData {
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	// Create a copy to prevent external modifications
+	result := make(map[string]*core.RefreshTokenData)
+	for token, data := range s.tokens {
+		if !data.IsExpired() {
+			result[token] = &core.RefreshTokenData{
+				UserData: data.UserData,
+				Expiry:   data.Expiry,
+				Created:  data.Created,
+			}
+		}
+	}
+
+	return result
+}
+
+// Clear removes all tokens from the store (useful for testing)
+// Note: This method is not part of the RefreshTokenStorer interface
+func (s *InMemoryRefreshTokenStore) Clear() {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	s.tokens = make(map[string]*core.RefreshTokenData)
+}
\ No newline at end of file
diff --git a/store/store.go b/store/store.go
new file mode 100644
index 0000000..8378d35
--- /dev/null
+++ b/store/store.go
@@ -0,0 +1,16 @@
+// Package store provides implementations for refresh token storage
+package store
+
+import (
+	"github.com/appleboy/gin-jwt/v2/core"
+)
+
+// Re-export types from core for backward compatibility
+type RefreshTokenStorer = core.TokenStore
+type RefreshTokenData = core.RefreshTokenData
+
+// Re-export errors from core for backward compatibility
+var (
+	ErrRefreshTokenNotFound = core.ErrRefreshTokenNotFound
+	ErrRefreshTokenExpired  = core.ErrRefreshTokenExpired
+)
\ No newline at end of file


Test patch:
diff --git a/auth_jwt_test.go b/auth_jwt_test.go
index 4b5719e..7193313 100644
--- a/auth_jwt_test.go
+++ b/auth_jwt_test.go
@@ -182,23 +182,44 @@ func helloHandler(c *gin.Context) {
 	})
 }
 
+// getRefreshTokenFromLogin performs a login and returns the refresh token from the response
+func getRefreshTokenFromLogin(handler *gin.Engine) string {
+	r := gofight.New()
+	var refreshToken string
+
+	r.POST("/login").
+		SetJSON(gofight.D{
+			"username": "admin",
+			"password": "admin",
+		}).
+		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+			if r.Code == http.StatusOK {
+				refreshToken = gjson.Get(r.Body.String(), "refresh_token").String()
+			}
+		})
+
+	return refreshToken
+}
+
 func ginHandler(auth *GinJWTMiddleware) *gin.Engine {
 	gin.SetMode(gin.TestMode)
 	r := gin.New()
 
 	r.POST("/login", auth.LoginHandler)
 	r.POST("/logout", auth.LogoutHandler)
-	// test token in path
-	r.GET("/g/:token/refresh_token", auth.RefreshHandler)
+	r.POST("/refresh", auth.RefreshHandler)
 
 	group := r.Group("/auth")
 	// Refresh time can be longer than token timeout
-	group.GET("/refresh_token", auth.RefreshHandler)
+	group.POST("/refresh_token", auth.RefreshHandler)
 	group.Use(auth.MiddlewareFunc())
 	{
 		group.GET("/hello", helloHandler)
 	}
 
+	// Add back the param-based endpoint for testing
+	r.GET("/g/:token/hello", auth.MiddlewareFunc(), helloHandler)
+
 	return r
 }
 
@@ -502,35 +523,40 @@ func TestRefreshHandlerRS256(t *testing.T) {
 
 	r := gofight.New()
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "",
-		}).
+	// Test missing refresh token
+	r.POST("/auth/refresh_token").
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusUnauthorized, r.Code)
+			assert.Equal(t, http.StatusBadRequest, r.Code)
 		})
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Test 1234",
+	// Test invalid refresh token
+	r.POST("/auth/refresh_token").
+		SetJSON(gofight.D{
+			"refresh_token": "invalid_token",
 		}).
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
 			assert.Equal(t, http.StatusUnauthorized, r.Code)
 		})
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + makeTokenString("RS256", "admin"),
-		}).
-		SetCookie(gofight.H{
-			"jwt": makeTokenString("RS256", "admin"),
-		}).
-		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			message := gjson.Get(r.Body.String(), "message")
-			cookie := gjson.Get(r.Body.String(), "cookie")
-			assert.Equal(t, "refresh successfully", message.String())
-			assert.Equal(t, http.StatusOK, r.Code)
-			assert.Equal(t, makeTokenString("RS256", "admin"), cookie.String())
-		})
+
+	// Test valid refresh token
+	refreshToken := getRefreshTokenFromLogin(handler)
+	if refreshToken != "" {
+		r.POST("/auth/refresh_token").
+			SetJSON(gofight.D{
+				"refresh_token": refreshToken,
+			}).
+			Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+				message := gjson.Get(r.Body.String(), "message")
+				assert.Equal(t, "refresh successfully", message.String())
+				assert.Equal(t, http.StatusOK, r.Code)
+				// Verify we get new tokens
+				accessToken := gjson.Get(r.Body.String(), "access_token")
+				newRefreshToken := gjson.Get(r.Body.String(), "refresh_token")
+				assert.NotEmpty(t, accessToken.String())
+				assert.NotEmpty(t, newRefreshToken.String())
+				assert.NotEqual(t, refreshToken, newRefreshToken.String()) // New refresh token should be different
+			})
+	}
 }
 
 func TestRefreshHandler(t *testing.T) {
@@ -547,89 +573,95 @@ func TestRefreshHandler(t *testing.T) {
 
 	r := gofight.New()
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "",
-		}).
+	// Test missing refresh token
+	r.POST("/auth/refresh_token").
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusUnauthorized, r.Code)
+			assert.Equal(t, http.StatusBadRequest, r.Code)
 		})
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Test 1234",
+	// Test invalid refresh token
+	r.POST("/auth/refresh_token").
+		SetJSON(gofight.D{
+			"refresh_token": "invalid_token",
 		}).
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
 			assert.Equal(t, http.StatusUnauthorized, r.Code)
 		})
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + makeTokenString("HS256", "admin"),
-		}).
-		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusOK, r.Code)
-		})
+	// Test valid refresh token
+	refreshToken := getRefreshTokenFromLogin(handler)
+	if refreshToken != "" {
+		r.POST("/auth/refresh_token").
+			SetJSON(gofight.D{
+				"refresh_token": refreshToken,
+			}).
+			Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+				assert.Equal(t, http.StatusOK, r.Code)
+				// Verify we get new tokens
+				accessToken := gjson.Get(r.Body.String(), "access_token")
+				newRefreshToken := gjson.Get(r.Body.String(), "refresh_token")
+				assert.NotEmpty(t, accessToken.String())
+				assert.NotEmpty(t, newRefreshToken.String())
+			})
+	}
 }
 
-func TestExpiredTokenWithinMaxRefreshOnRefreshHandler(t *testing.T) {
+func TestValidRefreshToken(t *testing.T) {
 	// the middleware to test
 	authMiddleware, _ := New(&GinJWTMiddleware{
-		Realm:         "test zone",
-		Key:           key,
-		Timeout:       time.Hour,
-		MaxRefresh:    2 * time.Hour,
-		Authenticator: defaultAuthenticator,
+		Realm:                "test zone",
+		Key:                  key,
+		Timeout:              time.Hour,
+		MaxRefresh:           2 * time.Hour,
+		RefreshTokenTimeout:  24 * time.Hour, // Long refresh token timeout
+		Authenticator:        defaultAuthenticator,
 	})
 
 	handler := ginHandler(authMiddleware)
 
 	r := gofight.New()
 
-	token := jwt.New(jwt.GetSigningMethod("HS256"))
-	claims := token.Claims.(jwt.MapClaims)
-	claims["identity"] = "admin"
-	claims["exp"] = time.Now().Add(-time.Minute).Unix()
-	claims["orig_iat"] = time.Now().Add(-time.Hour).Unix()
-	tokenString, _ := token.SignedString(key)
-
-	// We should be able to refresh a token that has expired but is within the MaxRefresh time
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + tokenString,
-		}).
-		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusOK, r.Code)
-		})
+	// Test that a valid refresh token still works
+	refreshToken := getRefreshTokenFromLogin(handler)
+	if refreshToken != "" {
+		r.POST("/auth/refresh_token").
+			SetJSON(gofight.D{
+				"refresh_token": refreshToken,
+			}).
+			Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+				assert.Equal(t, http.StatusOK, r.Code)
+			})
+	}
 }
 
 func TestExpiredTokenOnRefreshHandler(t *testing.T) {
 	// the middleware to test
 	authMiddleware, _ := New(&GinJWTMiddleware{
-		Realm:         "test zone",
-		Key:           key,
-		Timeout:       time.Hour,
-		Authenticator: defaultAuthenticator,
+		Realm:                "test zone",
+		Key:                  key,
+		Timeout:              time.Hour,
+		RefreshTokenTimeout:  time.Millisecond, // Very short refresh token timeout
+		Authenticator:        defaultAuthenticator,
 	})
 
 	handler := ginHandler(authMiddleware)
 
 	r := gofight.New()
 
-	token := jwt.New(jwt.GetSigningMethod("HS256"))
-	claims := token.Claims.(jwt.MapClaims)
-	claims["identity"] = "admin"
-	claims["exp"] = time.Now().Add(time.Hour).Unix()
-	claims["orig_iat"] = 0
-	tokenString, _ := token.SignedString(key)
-
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + tokenString,
-		}).
-		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusUnauthorized, r.Code)
-		})
+	// Get a refresh token and wait for it to expire
+	refreshToken := getRefreshTokenFromLogin(handler)
+	if refreshToken != "" {
+		// Wait for the refresh token to expire
+		time.Sleep(2 * time.Millisecond)
+
+		r.POST("/auth/refresh_token").
+			SetJSON(gofight.D{
+				"refresh_token": refreshToken,
+			}).
+			Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+				assert.Equal(t, http.StatusUnauthorized, r.Code)
+			})
+	}
 }
 
 func TestAuthorizator(t *testing.T) {
@@ -910,16 +942,10 @@ func TestTokenExpire(t *testing.T) {
 
 	r := gofight.New()
 
-	userToken, _, _ := authMiddleware.TokenGenerator(jwt.MapClaims{
-		"identity": "admin",
-	})
-
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + userToken,
-		}).
+	// Test with missing refresh token (should return 400 bad request)
+	r.POST("/auth/refresh_token").
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			assert.Equal(t, http.StatusUnauthorized, r.Code)
+			assert.Equal(t, http.StatusBadRequest, r.Code)
 		})
 }
 
@@ -944,7 +970,7 @@ func TestTokenFromQueryString(t *testing.T) {
 		"identity": "admin",
 	})
 
-	r.GET("/auth/refresh_token").
+	r.GET("/auth/hello").
 		SetHeader(gofight.H{
 			"Authorization": "Bearer " + userToken,
 		}).
@@ -952,7 +978,7 @@ func TestTokenFromQueryString(t *testing.T) {
 			assert.Equal(t, http.StatusUnauthorized, r.Code)
 		})
 
-	r.GET("/auth/refresh_token?token="+userToken).
+	r.GET("/auth/hello?token="+userToken).
 		SetHeader(gofight.H{
 			"Authorization": "Bearer " + userToken,
 		}).
@@ -982,7 +1008,7 @@ func TestTokenFromParamPath(t *testing.T) {
 		"identity": "admin",
 	})
 
-	r.GET("/auth/refresh_token").
+	r.GET("/auth/hello").
 		SetHeader(gofight.H{
 			"Authorization": "Bearer " + userToken,
 		}).
@@ -990,7 +1016,7 @@ func TestTokenFromParamPath(t *testing.T) {
 			assert.Equal(t, http.StatusUnauthorized, r.Code)
 		})
 
-	r.GET("/g/"+userToken+"/refresh_token").
+	r.GET("/g/"+userToken+"/hello").
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
 			assert.Equal(t, http.StatusOK, r.Code)
 		})
@@ -1017,7 +1043,7 @@ func TestTokenFromCookieString(t *testing.T) {
 		"identity": "admin",
 	})
 
-	r.GET("/auth/refresh_token").
+	r.GET("/auth/hello").
 		SetHeader(gofight.H{
 			"Authorization": "Bearer " + userToken,
 		}).
@@ -1035,7 +1061,7 @@ func TestTokenFromCookieString(t *testing.T) {
 			assert.Equal(t, "", token.String())
 		})
 
-	r.GET("/auth/refresh_token").
+	r.GET("/auth/hello").
 		SetCookie(gofight.H{
 			"token": userToken,
 		}).
@@ -1193,9 +1219,10 @@ func TestBadTokenOnRefreshHandler(t *testing.T) {
 
 	r := gofight.New()
 
-	r.GET("/auth/refresh_token").
-		SetHeader(gofight.H{
-			"Authorization": "Bearer " + "BadToken",
+	// Test with bad refresh token
+	r.POST("/auth/refresh_token").
+		SetJSON(gofight.D{
+			"refresh_token": "BadToken",
 		}).
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
 			assert.Equal(t, http.StatusUnauthorized, r.Code)
diff --git a/store/memory_test.go b/store/memory_test.go
new file mode 100644
index 0000000..06423f9
--- /dev/null
+++ b/store/memory_test.go
@@ -0,0 +1,455 @@
+package store
+
+import (
+	"fmt"
+	"sync"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// User represents a test user for testing purposes
+type User struct {
+	ID       string `json:"id"`
+	Username string `json:"username"`
+	Email    string `json:"email"`
+}
+
+func TestNewInMemoryRefreshTokenStore(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	if store == nil {
+		t.Fatal("NewInMemoryRefreshTokenStore returned nil")
+	}
+
+	if store.tokens == nil {
+		t.Fatal("store.tokens is nil")
+	}
+
+	count, err := store.Count()
+	if err != nil {
+		t.Fatalf("Count() returned error: %v", err)
+	}
+
+	if count != 0 {
+		t.Fatalf("Expected count to be 0, got %d", count)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Set(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser", Email: "test@example.com"}
+	expiry := time.Now().Add(time.Hour)
+
+	err := store.Set("token123", user, expiry)
+	if err != nil {
+		t.Fatalf("Set() returned error: %v", err)
+	}
+
+	count, _ := store.Count()
+	if count != 1 {
+		t.Fatalf("Expected count to be 1, got %d", count)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_SetEmptyToken(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(time.Hour)
+
+	err := store.Set("", user, expiry)
+	if err == nil {
+		t.Fatal("Set() should return error for empty token")
+	}
+
+	if err.Error() != "token cannot be empty" {
+		t.Fatalf("Expected 'token cannot be empty' error, got: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Get(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser", Email: "test@example.com"}
+	expiry := time.Now().Add(time.Hour)
+
+	// Set a token
+	err := store.Set("token123", user, expiry)
+	if err != nil {
+		t.Fatalf("Set() returned error: %v", err)
+	}
+
+	// Get the token
+	userData, err := store.Get("token123")
+	if err != nil {
+		t.Fatalf("Get() returned error: %v", err)
+	}
+
+	retrievedUser, ok := userData.(*User)
+	if !ok {
+		t.Fatal("Retrieved user data is not of type *User")
+	}
+
+	if retrievedUser.ID != user.ID || retrievedUser.Username != user.Username || retrievedUser.Email != user.Email {
+		t.Fatalf("Retrieved user data doesn't match. Expected: %+v, Got: %+v", user, retrievedUser)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_GetNonExistent(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	_, err := store.Get("nonexistent")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("Expected ErrRefreshTokenNotFound, got: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_GetEmptyToken(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	_, err := store.Get("")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("Expected ErrRefreshTokenNotFound for empty token, got: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_GetExpired(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(-time.Hour) // Expired 1 hour ago
+
+	// Set an expired token
+	err := store.Set("expired_token", user, expiry)
+	if err != nil {
+		t.Fatalf("Set() returned error: %v", err)
+	}
+
+	// Try to get the expired token
+	_, err = store.Get("expired_token")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("Expected ErrRefreshTokenNotFound for expired token, got: %v", err)
+	}
+
+	// Verify the expired token was cleaned up
+	count, _ := store.Count()
+	if count != 0 {
+		t.Fatalf("Expected count to be 0 after expired token cleanup, got %d", count)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Delete(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(time.Hour)
+
+	// Set a token
+	err := store.Set("token123", user, expiry)
+	if err != nil {
+		t.Fatalf("Set() returned error: %v", err)
+	}
+
+	// Delete the token
+	err = store.Delete("token123")
+	if err != nil {
+		t.Fatalf("Delete() returned error: %v", err)
+	}
+
+	// Verify the token is gone
+	_, err = store.Get("token123")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("Expected ErrRefreshTokenNotFound after deletion, got: %v", err)
+	}
+
+	count, _ := store.Count()
+	if count != 0 {
+		t.Fatalf("Expected count to be 0 after deletion, got %d", count)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_DeleteNonExistent(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Should not return error for deleting non-existent token
+	err := store.Delete("nonexistent")
+	if err != nil {
+		t.Fatalf("Delete() should not return error for non-existent token, got: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_DeleteEmptyToken(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Should not return error for empty token
+	err := store.Delete("")
+	if err != nil {
+		t.Fatalf("Delete() should not return error for empty token, got: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Cleanup(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Add some tokens: 2 valid, 2 expired
+	validExpiry := time.Now().Add(time.Hour)
+	expiredExpiry := time.Now().Add(-time.Hour)
+
+	err := store.Set("valid1", &User{ID: "1"}, validExpiry)
+	assert.NoError(t, err)
+	err = store.Set("valid2", &User{ID: "2"}, validExpiry)
+	assert.NoError(t, err)
+	err = store.Set("expired1", &User{ID: "3"}, expiredExpiry)
+	assert.NoError(t, err)
+	err = store.Set("expired2", &User{ID: "4"}, expiredExpiry)
+	assert.NoError(t, err)
+
+	// Verify initial count
+	count, _ := store.Count()
+	if count != 4 {
+		t.Fatalf("Expected initial count to be 4, got %d", count)
+	}
+
+	// Cleanup expired tokens
+	cleaned, err := store.Cleanup()
+	if err != nil {
+		t.Fatalf("Cleanup() returned error: %v", err)
+	}
+
+	if cleaned != 2 {
+		t.Fatalf("Expected 2 tokens to be cleaned up, got %d", cleaned)
+	}
+
+	// Verify final count
+	count, _ = store.Count()
+	if count != 2 {
+		t.Fatalf("Expected final count to be 2, got %d", count)
+	}
+
+	// Verify valid tokens still exist
+	_, err = store.Get("valid1")
+	if err != nil {
+		t.Fatalf("valid1 token should still exist: %v", err)
+	}
+
+	_, err = store.Get("valid2")
+	if err != nil {
+		t.Fatalf("valid2 token should still exist: %v", err)
+	}
+
+	// Verify expired tokens are gone
+	_, err = store.Get("expired1")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("expired1 token should be gone: %v", err)
+	}
+
+	_, err = store.Get("expired2")
+	if err != ErrRefreshTokenNotFound {
+		t.Fatalf("expired2 token should be gone: %v", err)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Count(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Initially empty
+	count, err := store.Count()
+	if err != nil {
+		t.Fatalf("Count() returned error: %v", err)
+	}
+	if count != 0 {
+		t.Fatalf("Expected initial count to be 0, got %d", count)
+	}
+
+	// Add tokens
+	expiry := time.Now().Add(time.Hour)
+	for i := 0; i < 5; i++ {
+		token := fmt.Sprintf("token%d", i)
+		user := &User{ID: fmt.Sprintf("%d", i)}
+		_ = store.Set(token, user, expiry)
+	}
+
+	count, err = store.Count()
+	if err != nil {
+		t.Fatalf("Count() returned error: %v", err)
+	}
+	if count != 5 {
+		t.Fatalf("Expected count to be 5, got %d", count)
+	}
+}
+
+func TestInMemoryRefreshTokenStore_GetAll(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Add some tokens
+	validExpiry := time.Now().Add(time.Hour)
+	expiredExpiry := time.Now().Add(-time.Hour)
+
+	_ = store.Set("valid1", &User{ID: "1"}, validExpiry)
+	_ = store.Set("valid2", &User{ID: "2"}, validExpiry)
+	_ = store.Set("expired1", &User{ID: "3"}, expiredExpiry)
+
+	all := store.GetAll()
+
+	// Should only return valid tokens
+	if len(all) != 2 {
+		t.Fatalf("Expected 2 valid tokens, got %d", len(all))
+	}
+
+	if _, exists := all["valid1"]; !exists {
+		t.Fatal("valid1 should be in GetAll() result")
+	}
+
+	if _, exists := all["valid2"]; !exists {
+		t.Fatal("valid2 should be in GetAll() result")
+	}
+
+	if _, exists := all["expired1"]; exists {
+		t.Fatal("expired1 should not be in GetAll() result")
+	}
+}
+
+func TestInMemoryRefreshTokenStore_Clear(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+
+	// Add some tokens
+	expiry := time.Now().Add(time.Hour)
+	_ = store.Set("token1", &User{ID: "1"}, expiry)
+	_ = store.Set("token2", &User{ID: "2"}, expiry)
+
+	count, _ := store.Count()
+	if count != 2 {
+		t.Fatalf("Expected count to be 2, got %d", count)
+	}
+
+	// Clear all tokens
+	store.Clear()
+
+	count, _ = store.Count()
+	if count != 0 {
+		t.Fatalf("Expected count to be 0 after Clear(), got %d", count)
+	}
+}
+
+// TestInMemoryRefreshTokenStore_ConcurrentAccess tests thread safety
+func TestInMemoryRefreshTokenStore_ConcurrentAccess(t *testing.T) {
+	store := NewInMemoryRefreshTokenStore()
+	var wg sync.WaitGroup
+	numGoroutines := 100
+
+	// Concurrent writes
+	wg.Add(numGoroutines)
+	for i := 0; i < numGoroutines; i++ {
+		go func(id int) {
+			defer wg.Done()
+			token := fmt.Sprintf("token%d", id)
+			user := &User{ID: fmt.Sprintf("%d", id)}
+			expiry := time.Now().Add(time.Hour)
+			_ = store.Set(token, user, expiry)
+		}(i)
+	}
+	wg.Wait()
+
+	// Verify all tokens were added
+	count, _ := store.Count()
+	if count != numGoroutines {
+		t.Fatalf("Expected count to be %d, got %d", numGoroutines, count)
+	}
+
+	// Concurrent reads
+	wg.Add(numGoroutines)
+	for i := 0; i < numGoroutines; i++ {
+		go func(id int) {
+			defer wg.Done()
+			token := fmt.Sprintf("token%d", id)
+			_, err := store.Get(token)
+			if err != nil {
+				t.Errorf("Failed to get token%d: %v", id, err)
+			}
+		}(i)
+	}
+	wg.Wait()
+
+	// Concurrent deletes
+	wg.Add(numGoroutines)
+	for i := 0; i < numGoroutines; i++ {
+		go func(id int) {
+			defer wg.Done()
+			token := fmt.Sprintf("token%d", id)
+			_ = store.Delete(token)
+		}(i)
+	}
+	wg.Wait()
+
+	// Verify all tokens were deleted
+	count, _ = store.Count()
+	if count != 0 {
+		t.Fatalf("Expected count to be 0 after concurrent deletes, got %d", count)
+	}
+}
+
+func TestRefreshTokenData_IsExpired(t *testing.T) {
+	// Test non-expired token
+	data := &RefreshTokenData{
+		UserData: &User{ID: "123"},
+		Expiry:   time.Now().Add(time.Hour),
+		Created:  time.Now(),
+	}
+
+	if data.IsExpired() {
+		t.Fatal("Token should not be expired")
+	}
+
+	// Test expired token
+	data.Expiry = time.Now().Add(-time.Hour)
+	if !data.IsExpired() {
+		t.Fatal("Token should be expired")
+	}
+}
+
+// Benchmark tests
+func BenchmarkInMemoryRefreshTokenStore_Set(b *testing.B) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(time.Hour)
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		token := fmt.Sprintf("token%d", i)
+		_ = store.Set(token, user, expiry)
+	}
+}
+
+func BenchmarkInMemoryRefreshTokenStore_Get(b *testing.B) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(time.Hour)
+
+	// Pre-populate with tokens
+	for i := 0; i < 1000; i++ {
+		token := fmt.Sprintf("token%d", i)
+		_ = store.Set(token, user, expiry)
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		token := fmt.Sprintf("token%d", i%1000)
+		_, _ = store.Get(token)
+	}
+}
+
+func BenchmarkInMemoryRefreshTokenStore_Delete(b *testing.B) {
+	store := NewInMemoryRefreshTokenStore()
+	user := &User{ID: "123", Username: "testuser"}
+	expiry := time.Now().Add(time.Hour)
+
+	// Pre-populate with tokens
+	for i := 0; i < b.N; i++ {
+		token := fmt.Sprintf("token%d", i)
+		_ = store.Set(token, user, expiry)
+	}
+
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		token := fmt.Sprintf("token%d", i)
+		_ = store.Delete(token)
+	}
+}
\ No newline at end of file