Repository: appleboy/gin-jwt

Issue:
How to get token in register handler?
New to web development. I was wondering how to get token if I need respond token in register API. I do not use the middleware in register router we cannot get a token before we register an account.  

Discussion:
@storytellerGIT: you may have a look at func (mw *GinJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time, error)@Starballoon: I guess that you have joined the training camp organized by ByteDance also. The framework gin has designed a chain call API, which means you can call the LoginHandler of this project after calling register function that has already registered the user somewhere.@appleboy: See the documentation: https://github.com/appleboy/gin-jwt#token-generator-direct-token-creation

Solution patch:
diff --git a/README.md b/README.md
index 46fdbf3..6635fff 100644
--- a/README.md
+++ b/README.md
@@ -23,11 +23,16 @@ Easily add login, token refresh, and authorization to your Gin applications.
   - [Installation](#installation)
     - [Using Go Modules (Recommended)](#using-go-modules-recommended)
   - [Quick Start Example](#quick-start-example)
+  - [Token Generator (Direct Token Creation)](#token-generator-direct-token-creation)
+    - [Basic Usage](#basic-usage)
+    - [Token Structure](#token-structure)
+    - [Refresh Token Management](#refresh-token-management)
   - [Demo](#demo)
     - [Login](#login)
     - [Refresh Token](#refresh-token)
     - [Hello World](#hello-world)
     - [Authorization Example](#authorization-example)
+    - [Logout](#logout)
   - [Cookie Token](#cookie-token)
     - [Login request flow (using the LoginHandler)](#login-request-flow-using-the-loginhandler)
     - [Subsequent requests on endpoints requiring jwt token (using MiddlewareFunc)](#subsequent-requests-on-endpoints-requiring-jwt-token-using-middlewarefunc)
@@ -46,6 +51,8 @@ Easily add login, token refresh, and authorization to your Gin applications.
 - 📝 Easy integration and clear API
 - 🔐 RFC 6749 compliant refresh tokens (OAuth 2.0 standard)
 - 🗄️ Pluggable refresh token storage (in-memory, Redis, etc.)
+- 🏭 Direct token generation without HTTP middleware
+- 📦 Structured Token type with metadata
 
 ---
 
@@ -252,6 +259,101 @@ func helloHandler(c *gin.Context) {
 
 ```
 
+---
+
+## Token Generator (Direct Token Creation)
+
+The new `GenerateTokenPair` functionality allows you to create JWT tokens directly without HTTP middleware, perfect for programmatic authentication, testing, and custom flows.
+
+### Basic Usage
+
+```go
+package main
+
+import (
+    "fmt"
+    "log"
+    "time"
+
+    jwt "github.com/appleboy/gin-jwt/v2"
+    gojwt "github.com/golang-jwt/jwt/v5"
+)
+
+func main() {
+    // Initialize the middleware
+    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
+        Realm:      "example zone",
+        Key:        []byte("secret key"),
+        Timeout:    time.Hour,
+        MaxRefresh: time.Hour * 24,
+        PayloadFunc: func(data interface{}) gojwt.MapClaims {
+            return gojwt.MapClaims{
+                "user_id": data,
+            }
+        },
+    })
+    if err != nil {
+        log.Fatal("JWT Error:" + err.Error())
+    }
+
+    // Generate a complete token pair (access + refresh tokens)
+    userData := "user123"
+    tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+    if err != nil {
+        log.Fatal("Failed to generate token pair:", err)
+    }
+
+    fmt.Printf("Access Token: %s\n", tokenPair.AccessToken)
+    fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken)
+    fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn())
+}
+```
+
+### Token Structure
+
+The `GenerateTokenPair` method returns a structured `core.Token`:
+
+```go
+type Token struct {
+    AccessToken  string `json:"access_token"`   // JWT access token
+    TokenType    string `json:"token_type"`     // Always "Bearer"
+    RefreshToken string `json:"refresh_token"`  // Opaque refresh token
+    ExpiresAt    int64  `json:"expires_at"`     // Unix timestamp
+    CreatedAt    int64  `json:"created_at"`     // Unix timestamp
+}
+
+// Helper method
+func (t *Token) ExpiresIn() int64 // Returns seconds until expiry
+```
+
+### Refresh Token Management
+
+Use `GenerateTokenPairWithRevocation` to refresh tokens and automatically revoke old ones:
+
+```go
+// Refresh with automatic revocation of old token
+newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldRefreshToken)
+if err != nil {
+    log.Fatal("Failed to refresh token:", err)
+}
+
+// Old refresh token is now invalid
+fmt.Printf("New Access Token: %s\n", newTokenPair.AccessToken)
+fmt.Printf("New Refresh Token: %s\n", newTokenPair.RefreshToken)
+```
+
+**Use Cases:**
+
+- 🔧 **Programmatic Authentication**: Service-to-service communication
+- 🧪 **Testing**: Generate tokens for testing authenticated endpoints
+- 📝 **Registration Flow**: Issue tokens immediately after user signup
+- ⚙️ **Background Jobs**: Create tokens for automated processes
+- 🎛️ **Custom Auth Flows**: Build custom authentication logic
+
+See the [complete example](_example/token_generator/) for more details.
+
+---
+
 ## Demo
 
 Run the example server:
diff --git a/README.zh-CN.md b/README.zh-CN.md
index de37cd5..c3d0b75 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -23,11 +23,16 @@
   - [安装](#安装)
     - [使用 Go Modules（推荐）](#使用-go-modules推荐)
   - [快速开始示例](#快速开始示例)
+  - [Token 生成器（直接创建 Token）](#token-生成器直接创建-token)
+    - [基本用法](#基本用法)
+    - [Token 结构](#token-结构)
+    - [刷新 Token 管理](#刷新-token-管理)
   - [Demo](#demo)
     - [登录](#登录)
     - [刷新 Token](#刷新-token)
     - [Hello World](#hello-world)
     - [授权示例](#授权示例)
+    - [登出](#登出)
   - [Cookie Token](#cookie-token)
     - [登录流程（LoginHandler）](#登录流程loginhandler)
     - [需要 JWT Token 的端点（MiddlewareFunc）](#需要-jwt-token-的端点middlewarefunc)
@@ -48,6 +53,8 @@
 - 📝 易于集成，API 清晰
 - 🔐 符合 RFC 6749 规范的刷新令牌（OAuth 2.0 标准）
 - 🗄️ 可插拔的刷新令牌存储（内存、Redis 等）
+- 🏭 直接生成 Token，无需 HTTP 中间件
+- 📦 结构化 Token 类型与元数据
 
 ---
 
@@ -89,6 +96,99 @@ import "github.com/appleboy/gin-jwt/v2"
 
 ---
 
+## Token 生成器（直接创建 Token）
+
+新的 `GenerateTokenPair` 功能让你可以直接创建 JWT Token 而无需 HTTP 中间件，非常适合程序化认证、测试和自定义流程。
+
+### 基本用法
+
+```go
+package main
+
+import (
+    "fmt"
+    "log"
+    "time"
+
+    jwt "github.com/appleboy/gin-jwt/v2"
+    gojwt "github.com/golang-jwt/jwt/v5"
+)
+
+func main() {
+    // 初始化中间件
+    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
+        Realm:      "example zone",
+        Key:        []byte("secret key"),
+        Timeout:    time.Hour,
+        MaxRefresh: time.Hour * 24,
+        PayloadFunc: func(data interface{}) gojwt.MapClaims {
+            return gojwt.MapClaims{
+                "user_id": data,
+            }
+        },
+    })
+    if err != nil {
+        log.Fatal("JWT Error:" + err.Error())
+    }
+
+    // 生成完整的 Token 组（访问 + 刷新 Token）
+    userData := "user123"
+    tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+    if err != nil {
+        log.Fatal("Failed to generate token pair:", err)
+    }
+
+    fmt.Printf("Access Token: %s\n", tokenPair.AccessToken)
+    fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken)
+    fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn())
+}
+```
+
+### Token 结构
+
+`GenerateTokenPair` 方法返回结构化的 `core.Token`：
+
+```go
+type Token struct {
+    AccessToken  string `json:"access_token"`   // JWT 访问 Token
+    TokenType    string `json:"token_type"`     // 总是 "Bearer"
+    RefreshToken string `json:"refresh_token"`  // 不透明刷新 Token
+    ExpiresAt    int64  `json:"expires_at"`     // Unix 时间戳
+    CreatedAt    int64  `json:"created_at"`     // Unix 时间戳
+}
+
+// 辅助方法
+func (t *Token) ExpiresIn() int64 // 返回到期前的秒数
+```
+
+### 刷新 Token 管理
+
+使用 `GenerateTokenPairWithRevocation` 来刷新 Token 并自动撤销旧 Token：
+
+```go
+// 刷新并自动撤销旧 Token
+newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldRefreshToken)
+if err != nil {
+    log.Fatal("Failed to refresh token:", err)
+}
+
+// 旧刷新 Token 现在已失效
+fmt.Printf("New Access Token: %s\n", newTokenPair.AccessToken)
+fmt.Printf("New Refresh Token: %s\n", newTokenPair.RefreshToken)
+```
+
+**使用场景：**
+
+- 🔧 **程序化认证**：服务间通信
+- 🧪 **测试**：为测试认证端点生成 Token
+- 📝 **注册流程**：用户注册后立即发放 Token
+- ⚙️ **后台作业**：为自动化流程创建 Token
+- 🎛️ **自定义认证流程**：构建自定义认证逻辑
+
+详见[完整示例](_example/token_generator/)。
+
+---
+
 ## Demo
 
 启动示例服务器：
diff --git a/README.zh-TW.md b/README.zh-TW.md
index 7f7a7be..68b1330 100644
--- a/README.zh-TW.md
+++ b/README.zh-TW.md
@@ -23,11 +23,16 @@
   - [安裝](#安裝)
     - [使用 Go Modules（推薦）](#使用-go-modules推薦)
   - [快速開始範例](#快速開始範例)
+  - [Token 產生器（直接建立 Token）](#token-產生器直接建立-token)
+    - [基本用法](#基本用法)
+    - [Token 結構](#token-結構)
+    - [刷新 Token 管理](#刷新-token-管理)
   - [Demo](#demo)
     - [登入](#登入)
     - [刷新 Token](#刷新-token)
     - [Hello World](#hello-world)
     - [授權範例](#授權範例)
+    - [登出](#登出)
   - [Cookie Token](#cookie-token)
     - [登入流程（LoginHandler）](#登入流程loginhandler)
     - [需要 JWT Token 的端點（MiddlewareFunc）](#需要-jwt-token-的端點middlewarefunc)
@@ -48,6 +53,8 @@
 - 📝 易於整合，API 清晰
 - 🔐 符合 RFC 6749 規範的刷新 Token（OAuth 2.0 標準）
 - 🗄️ 可插拔的刷新 Token 儲存（記憶體、Redis 等）
+- 🏭 直接產生 Token，無需 HTTP 中介軟體
+- 📦 結構化 Token 類型與中繼資料
 
 ---
 
@@ -89,6 +96,99 @@ import "github.com/appleboy/gin-jwt/v2"
 
 ---
 
+## Token 產生器（直接建立 Token）
+
+新的 `GenerateTokenPair` 功能讓你可以直接建立 JWT Token 而無需 HTTP 中介軟體，非常適合程式化驗證、測試和自訂流程。
+
+### 基本用法
+
+```go
+package main
+
+import (
+    "fmt"
+    "log"
+    "time"
+
+    jwt "github.com/appleboy/gin-jwt/v2"
+    gojwt "github.com/golang-jwt/jwt/v5"
+)
+
+func main() {
+    // 初始化中介軟體
+    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
+        Realm:      "example zone",
+        Key:        []byte("secret key"),
+        Timeout:    time.Hour,
+        MaxRefresh: time.Hour * 24,
+        PayloadFunc: func(data interface{}) gojwt.MapClaims {
+            return gojwt.MapClaims{
+                "user_id": data,
+            }
+        },
+    })
+    if err != nil {
+        log.Fatal("JWT Error:" + err.Error())
+    }
+
+    // 產生完整的 Token 組（存取 + 刷新 Token）
+    userData := "user123"
+    tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+    if err != nil {
+        log.Fatal("Failed to generate token pair:", err)
+    }
+
+    fmt.Printf("Access Token: %s\n", tokenPair.AccessToken)
+    fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken)
+    fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn())
+}
+```
+
+### Token 結構
+
+`GenerateTokenPair` 方法回傳結構化的 `core.Token`：
+
+```go
+type Token struct {
+    AccessToken  string `json:"access_token"`   // JWT 存取 Token
+    TokenType    string `json:"token_type"`     // 總是 "Bearer"
+    RefreshToken string `json:"refresh_token"`  // 不透明刷新 Token
+    ExpiresAt    int64  `json:"expires_at"`     // Unix 時間戳
+    CreatedAt    int64  `json:"created_at"`     // Unix 時間戳
+}
+
+// 輔助方法
+func (t *Token) ExpiresIn() int64 // 回傳到期前的秒數
+```
+
+### 刷新 Token 管理
+
+使用 `GenerateTokenPairWithRevocation` 來刷新 Token 並自動撤銷舊 Token：
+
+```go
+// 刷新並自動撤銷舊 Token
+newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldRefreshToken)
+if err != nil {
+    log.Fatal("Failed to refresh token:", err)
+}
+
+// 舊刷新 Token 現在已失效
+fmt.Printf("New Access Token: %s\n", newTokenPair.AccessToken)
+fmt.Printf("New Refresh Token: %s\n", newTokenPair.RefreshToken)
+```
+
+**使用情境：**
+
+- 🔧 **程式化驗證**：服務間通訊
+- 🧪 **測試**：為測試驗證端點產生 Token
+- 📝 **註冊流程**：使用者註冊後立即發放 Token
+- ⚙️ **背景作業**：為自動化流程建立 Token
+- 🎛️ **自訂驗證流程**：建立自訂驗證邏輯
+
+詳見[完整範例](_example/token_generator/)。
+
+---
+
 ## Demo
 
 啟動範例伺服器：
diff --git a/_example/token_generator/README.md b/_example/token_generator/README.md
new file mode 100644
index 0000000..cb57bd1
--- /dev/null
+++ b/_example/token_generator/README.md
@@ -0,0 +1,86 @@
+# Token Generator Example
+
+This example demonstrates how to use the new `GenerateTokenPair` functionality to create JWT tokens directly without using HTTP middleware handlers.
+
+## Features
+
+- **Direct Token Generation**: Generate complete token pairs (access + refresh) programmatically
+- **Refresh Token Management**: Handle refresh token rotation and revocation
+- **RFC 6749 Compliant**: Follows OAuth 2.0 standards for token management
+- **No HTTP Required**: Generate tokens without needing HTTP requests
+
+## Usage
+
+Run the example:
+
+```bash
+cd _example/token_generator
+go run main.go
+```
+
+## Key Methods
+
+### `GenerateTokenPair(userData interface{}) (*core.Token, error)`
+
+Generates a complete token pair containing:
+
+- Access token (JWT)
+- Refresh token (opaque)
+- Token metadata (expiry, creation time, etc.)
+
+```go
+tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+if err != nil {
+    log.Fatal("Failed to generate token pair:", err)
+}
+
+fmt.Printf("Access Token: %s\n", tokenPair.AccessToken)
+fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken)
+fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn())
+```
+
+### `GenerateTokenPairWithRevocation(userData interface{}, oldRefreshToken string) (*core.Token, error)`
+
+Generates a new token pair and automatically revokes the old refresh token:
+
+```go
+newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldRefreshToken)
+if err != nil {
+    log.Fatal("Failed to refresh token pair:", err)
+}
+```
+
+## Token Structure
+
+The `core.Token` struct contains:
+
+```go
+type Token struct {
+    AccessToken  string `json:"access_token"`   // JWT access token
+    TokenType    string `json:"token_type"`     // Always "Bearer"
+    RefreshToken string `json:"refresh_token"`  // Opaque refresh token
+    ExpiresAt    int64  `json:"expires_at"`     // Unix timestamp
+    CreatedAt    int64  `json:"created_at"`     // Unix timestamp
+}
+```
+
+### Utility Methods
+
+- `ExpiresIn()` - Returns seconds until token expires
+- Server-side refresh token storage and validation
+- Automatic token rotation on refresh
+
+## Use Cases
+
+1. **Programmatic Authentication**: Generate tokens for service-to-service communication
+2. **Testing**: Create tokens for testing authenticated endpoints
+3. **Registration Flow**: Issue tokens immediately after user registration
+4. **Background Jobs**: Generate tokens for background processing
+5. **Custom Authentication**: Build custom authentication flows
+
+## Security Features
+
+- **Refresh Token Rotation**: Old tokens are automatically revoked
+- **Server-side Storage**: Refresh tokens are stored securely server-side
+- **Expiry Management**: Both access and refresh tokens have proper expiry
+- **RFC 6749 Compliance**: Follows OAuth 2.0 security standards
diff --git a/_example/token_generator/go.mod b/_example/token_generator/go.mod
new file mode 100644
index 0000000..7a09365
--- /dev/null
+++ b/_example/token_generator/go.mod
@@ -0,0 +1,3 @@
+module token_generator_example
+
+go 1.25.1
diff --git a/_example/token_generator/main.go b/_example/token_generator/main.go
new file mode 100644
index 0000000..a5dd584
--- /dev/null
+++ b/_example/token_generator/main.go
@@ -0,0 +1,67 @@
+// Example demonstrating the new GenerateTokenPair functionality
+package main
+
+import (
+	"fmt"
+	"log"
+	"time"
+
+	jwt "github.com/appleboy/gin-jwt/v2"
+	gojwt "github.com/golang-jwt/jwt/v5"
+)
+
+func main() {
+	// Initialize the middleware
+	authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
+		Realm:      "example zone",
+		Key:        []byte("secret key"),
+		Timeout:    time.Hour,
+		MaxRefresh: time.Hour * 24,
+		PayloadFunc: func(data interface{}) gojwt.MapClaims {
+			return gojwt.MapClaims{
+				"user_id": data,
+			}
+		},
+	})
+	if err != nil {
+		log.Fatal("JWT Error:" + err.Error())
+	}
+
+	// Example user data
+	userData := "user123"
+
+	// Generate a complete token pair (access + refresh tokens)
+	fmt.Println("=== Generating Token Pair ===")
+	tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+	if err != nil {
+		log.Fatal("Failed to generate token pair:", err)
+	}
+
+	fmt.Printf("Access Token: %s\n", tokenPair.AccessToken[:50]+"...")
+	fmt.Printf("Token Type: %s\n", tokenPair.TokenType)
+	fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken)
+	fmt.Printf("Expires At: %d (%s)\n", tokenPair.ExpiresAt, time.Unix(tokenPair.ExpiresAt, 0))
+	fmt.Printf("Created At: %d (%s)\n", tokenPair.CreatedAt, time.Unix(tokenPair.CreatedAt, 0))
+	fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn())
+
+	// Simulate refresh token usage
+	fmt.Println("\n=== Refreshing Token Pair ===")
+	newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, tokenPair.RefreshToken)
+	if err != nil {
+		log.Fatal("Failed to refresh token pair:", err)
+	}
+
+	fmt.Printf("New Access Token: %s\n", newTokenPair.AccessToken[:50]+"...")
+	fmt.Printf("New Refresh Token: %s\n", newTokenPair.RefreshToken)
+	fmt.Printf("Old refresh token revoked: %t\n", tokenPair.RefreshToken != newTokenPair.RefreshToken)
+
+	// Verify old refresh token is invalid
+	fmt.Println("\n=== Verifying Old Token Revocation ===")
+	_, err = authMiddleware.GenerateTokenPairWithRevocation(userData, tokenPair.RefreshToken)
+	if err != nil {
+		fmt.Printf("Old refresh token correctly rejected: %s\n", err)
+	}
+
+	fmt.Println("\n=== Token Generation Complete! ===")
+	fmt.Println("You can now use these tokens without needing middleware handlers!")
+}
\ No newline at end of file
diff --git a/auth_jwt.go b/auth_jwt.go
index 5c876c7..5f3ef8c 100644
--- a/auth_jwt.go
+++ b/auth_jwt.go
@@ -565,47 +565,19 @@ func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
 		return
 	}
 
-	// Create the token
-	token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
-	claims, ok := token.Claims.(jwt.MapClaims)
-	if !ok {
-		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
-		return
-	}
-
-	if mw.PayloadFunc != nil {
-		for key, value := range mw.PayloadFunc(data) {
-			claims[key] = value
-		}
-	}
-
-	expire := mw.TimeFunc().Add(mw.TimeoutFunc(claims))
-	claims[mw.ExpField] = expire.Unix()
-	claims["orig_iat"] = mw.TimeFunc().Unix()
-	tokenString, err := mw.signedString(token)
+	// Generate complete token pair
+	tokenPair, err := mw.GenerateTokenPair(data)
 	if err != nil {
-		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
-		return
-	}
-
-	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)
+	// Set cookie and context for response generation
+	mw.SetCookie(c, tokenPair.AccessToken)
+	c.Set("REFRESH_TOKEN", tokenPair.RefreshToken)
 
-	mw.LoginResponse(c, http.StatusOK, tokenString, expire)
+	expire := time.Unix(tokenPair.ExpiresAt, 0)
+	mw.LoginResponse(c, http.StatusOK, tokenPair.AccessToken, expire)
 }
 
 func (mw *GinJWTMiddleware) extractRefreshToken(c *gin.Context) string {
@@ -712,11 +684,6 @@ func (mw *GinJWTMiddleware) revokeRefreshToken(token string) error {
 // 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) {
-	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 == "" {
@@ -731,38 +698,19 @@ func (mw *GinJWTMiddleware) rfc6749RefreshHandler(c *gin.Context) {
 		return
 	}
 
-	// Generate new access token
-	newTokenString, expire, err := mw.TokenGenerator(userData)
-	if err != nil {
-		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
-		return
-	}
-
-	// Generate new refresh token and revoke old one
-	newRefreshToken, err := mw.generateRefreshToken()
+	// Generate new token pair and revoke old refresh token
+	tokenPair, err := mw.GenerateTokenPairWithRevocation(userData, refreshToken)
 	if err != nil {
 		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
 		return
 	}
 
-	// Store new refresh token
-	if err := mw.storeRefreshToken(newRefreshToken, userData); err != nil {
-		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(err, c))
-		return
-	}
-
-	// 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, newTokenString)
-
-	// Store new refresh token in context for response generation
-	c.Set("REFRESH_TOKEN", newRefreshToken)
+	// Set cookie and context for response generation
+	mw.SetCookie(c, tokenPair.AccessToken)
+	c.Set("REFRESH_TOKEN", tokenPair.RefreshToken)
 
-	mw.RefreshResponse(c, http.StatusOK, newTokenString, expire)
+	expire := time.Unix(tokenPair.ExpiresAt, 0)
+	mw.RefreshResponse(c, http.StatusOK, tokenPair.AccessToken, expire)
 }
 
 // RefreshToken is deprecated. Use RefreshHandler instead for RFC 6749 compliant refresh tokens.
@@ -833,6 +781,51 @@ func (mw *GinJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time,
 	return tokenString, expire, nil
 }
 
+// GenerateTokenPair generates a complete token pair (access + refresh) with RFC 6749 compliance
+func (mw *GinJWTMiddleware) GenerateTokenPair(data interface{}) (*core.Token, error) {
+	// Generate access token
+	accessToken, expire, err := mw.TokenGenerator(data)
+	if err != nil {
+		return nil, err
+	}
+
+	// Generate refresh token
+	refreshToken, err := mw.generateRefreshToken()
+	if err != nil {
+		return nil, err
+	}
+
+	// Store refresh token
+	if err := mw.storeRefreshToken(refreshToken, data); err != nil {
+		return nil, err
+	}
+
+	now := mw.TimeFunc()
+	return &core.Token{
+		AccessToken:  accessToken,
+		TokenType:    "Bearer",
+		RefreshToken: refreshToken,
+		ExpiresAt:    expire.Unix(),
+		CreatedAt:    now.Unix(),
+	}, nil
+}
+
+// GenerateTokenPairWithRevocation generates a new token pair and revokes the old refresh token
+func (mw *GinJWTMiddleware) GenerateTokenPairWithRevocation(data interface{}, oldRefreshToken string) (*core.Token, error) {
+	// Generate new token pair
+	tokenPair, err := mw.GenerateTokenPair(data)
+	if err != nil {
+		return nil, err
+	}
+
+	// Revoke old refresh token, ignore if token already doesn't exist
+	if err := mw.revokeRefreshToken(oldRefreshToken); err != nil && !errors.Is(err, core.ErrRefreshTokenNotFound) {
+		return nil, err
+	}
+
+	return tokenPair, nil
+}
+
 func (mw *GinJWTMiddleware) jwtFromHeader(c *gin.Context, key string) (string, error) {
 	authHeader := c.Request.Header.Get(key)
 
diff --git a/core/store.go b/core/store.go
index 7bae7f0..439202b 100644
--- a/core/store.go
+++ b/core/store.go
@@ -47,4 +47,18 @@ type RefreshTokenData struct {
 // IsExpired checks if the token data has expired
 func (r *RefreshTokenData) IsExpired() bool {
 	return time.Now().After(r.Expiry)
+}
+
+// Token represents a complete JWT token pair with metadata
+type Token struct {
+	AccessToken  string `json:"access_token"`
+	TokenType    string `json:"token_type"`
+	RefreshToken string `json:"refresh_token,omitempty"`
+	ExpiresAt    int64  `json:"expires_at"`
+	CreatedAt    int64  `json:"created_at"`
+}
+
+// ExpiresIn returns the number of seconds until the access token expires
+func (t *Token) ExpiresIn() int64 {
+	return t.ExpiresAt - time.Now().Unix()
 }
\ No newline at end of file


Test patch:
diff --git a/auth_jwt_test.go b/auth_jwt_test.go
index 7193313..e7a73e0 100644
--- a/auth_jwt_test.go
+++ b/auth_jwt_test.go
@@ -1438,3 +1438,140 @@ func TestSetCookie(t *testing.T) {
 	assert.Equal(t, "example.com", cookie.Domain)
 	assert.Equal(t, true, cookie.HttpOnly)
 }
+
+func TestGenerateTokenPair(t *testing.T) {
+	authMiddleware, err := New(&GinJWTMiddleware{
+		Realm:      "test zone",
+		Key:        key,
+		Timeout:    time.Hour,
+		MaxRefresh: time.Hour * 24,
+		Authenticator: func(c *gin.Context) (interface{}, error) {
+			return "admin", nil
+		},
+		PayloadFunc: func(data interface{}) jwt.MapClaims {
+			return jwt.MapClaims{
+				"identity": data,
+			}
+		},
+		Authorizator: func(data interface{}, c *gin.Context) bool {
+			return data == "admin"
+		},
+		Unauthorized: func(c *gin.Context, code int, message string) {
+			c.JSON(code, gin.H{
+				"code":    code,
+				"message": message,
+			})
+		},
+	})
+
+	assert.NoError(t, err)
+
+	userData := "admin"
+	tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+
+	assert.NoError(t, err)
+	assert.NotNil(t, tokenPair)
+	assert.NotEmpty(t, tokenPair.AccessToken)
+	assert.NotEmpty(t, tokenPair.RefreshToken)
+	assert.Equal(t, "Bearer", tokenPair.TokenType)
+	assert.True(t, tokenPair.ExpiresAt > time.Now().Unix())
+	assert.True(t, tokenPair.CreatedAt <= time.Now().Unix())
+	assert.True(t, tokenPair.ExpiresIn() > 0)
+
+	// Validate that the access token is properly signed
+	token, err := authMiddleware.ParseTokenString(tokenPair.AccessToken)
+	assert.NoError(t, err)
+	assert.True(t, token.Valid)
+
+	claims, ok := token.Claims.(jwt.MapClaims)
+	assert.True(t, ok)
+	assert.Equal(t, userData, claims["identity"])
+}
+
+func TestGenerateTokenPairWithRevocation(t *testing.T) {
+	authMiddleware, err := New(&GinJWTMiddleware{
+		Realm:      "test zone",
+		Key:        key,
+		Timeout:    time.Hour,
+		MaxRefresh: time.Hour * 24,
+		Authenticator: func(c *gin.Context) (interface{}, error) {
+			return "admin", nil
+		},
+		PayloadFunc: func(data interface{}) jwt.MapClaims {
+			return jwt.MapClaims{
+				"identity": data,
+			}
+		},
+	})
+
+	assert.NoError(t, err)
+
+	userData := "admin"
+
+	// Generate first token pair
+	oldTokenPair, err := authMiddleware.GenerateTokenPair(userData)
+	assert.NoError(t, err)
+
+	// Verify old refresh token exists in store
+	storedData, err := authMiddleware.validateRefreshToken(oldTokenPair.RefreshToken)
+	assert.NoError(t, err)
+	assert.Equal(t, userData, storedData)
+
+	// Generate new token pair with revocation
+	newTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldTokenPair.RefreshToken)
+	assert.NoError(t, err)
+	assert.NotNil(t, newTokenPair)
+
+	// Verify refresh tokens are different (access tokens might be the same if generated in same second)
+	assert.NotEqual(t, oldTokenPair.RefreshToken, newTokenPair.RefreshToken)
+
+	// Verify old refresh token is revoked
+	_, err = authMiddleware.validateRefreshToken(oldTokenPair.RefreshToken)
+	assert.Error(t, err)
+
+	// Verify new refresh token works
+	storedData, err = authMiddleware.validateRefreshToken(newTokenPair.RefreshToken)
+	assert.NoError(t, err)
+	assert.Equal(t, userData, storedData)
+
+	// Test revoking already revoked token (should not fail)
+	anotherTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, oldTokenPair.RefreshToken)
+	assert.NoError(t, err)
+	assert.NotNil(t, anotherTokenPair)
+
+	// Test revoking non-existent token (should not fail)
+	finalTokenPair, err := authMiddleware.GenerateTokenPairWithRevocation(userData, "non_existent_token")
+	assert.NoError(t, err)
+	assert.NotNil(t, finalTokenPair)
+}
+
+func TestTokenStruct(t *testing.T) {
+	authMiddleware, err := New(&GinJWTMiddleware{
+		Realm:      "test zone",
+		Key:        key,
+		Timeout:    time.Hour,
+		MaxRefresh: time.Hour * 24,
+		Authenticator: func(c *gin.Context) (interface{}, error) {
+			return "admin", nil
+		},
+	})
+
+	assert.NoError(t, err)
+
+	userData := "admin"
+	tokenPair, err := authMiddleware.GenerateTokenPair(userData)
+	assert.NoError(t, err)
+
+	// Test ExpiresIn method
+	expiresIn := tokenPair.ExpiresIn()
+	assert.True(t, expiresIn > 3500) // Should be close to 3600 (1 hour)
+	assert.True(t, expiresIn <= 3600)
+
+	// Test Token struct fields directly
+	assert.NotEmpty(t, tokenPair.AccessToken)
+	assert.Equal(t, "Bearer", tokenPair.TokenType)
+	assert.NotEmpty(t, tokenPair.RefreshToken)
+	assert.True(t, tokenPair.ExpiresAt > time.Now().Unix())
+	assert.True(t, tokenPair.CreatedAt > 0)
+	assert.True(t, tokenPair.CreatedAt <= time.Now().Unix())
+}
diff --git a/core/token_test.go b/core/token_test.go
new file mode 100644
index 0000000..3d8a8b6
--- /dev/null
+++ b/core/token_test.go
@@ -0,0 +1,64 @@
+package core
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestToken(t *testing.T) {
+	now := time.Now()
+	expiresAt := now.Add(time.Hour).Unix()
+	createdAt := now.Unix()
+
+	token := &Token{
+		AccessToken:  "test.access.token",
+		TokenType:    "Bearer",
+		RefreshToken: "test-refresh-token",
+		ExpiresAt:    expiresAt,
+		CreatedAt:    createdAt,
+	}
+
+	// Test basic fields
+	assert.Equal(t, "test.access.token", token.AccessToken)
+	assert.Equal(t, "Bearer", token.TokenType)
+	assert.Equal(t, "test-refresh-token", token.RefreshToken)
+	assert.Equal(t, expiresAt, token.ExpiresAt)
+	assert.Equal(t, createdAt, token.CreatedAt)
+
+	// Test ExpiresIn method
+	expiresIn := token.ExpiresIn()
+	assert.True(t, expiresIn > 3500) // Should be close to 3600 (1 hour)
+	assert.True(t, expiresIn <= 3600)
+}
+
+func TestTokenExpiresIn(t *testing.T) {
+	testCases := []struct {
+		name      string
+		expiresAt int64
+		expected  int64
+	}{
+		{
+			name:      "Future expiry",
+			expiresAt: time.Now().Add(30 * time.Minute).Unix(),
+			expected:  30 * 60, // approximately 30 minutes
+		},
+		{
+			name:      "Past expiry",
+			expiresAt: time.Now().Add(-30 * time.Minute).Unix(),
+			expected:  -30 * 60, // approximately -30 minutes
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			token := &Token{ExpiresAt: tc.expiresAt}
+			expiresIn := token.ExpiresIn()
+
+			// Allow for some time difference due to test execution
+			diff := expiresIn - tc.expected
+			assert.True(t, diff >= -5 && diff <= 5, "ExpiresIn should be within 5 seconds of expected")
+		})
+	}
+}
\ No newline at end of file