Repository: appleboy/gin-jwt
Pull request #347 (2025-07-16 09:33:35)

feat: adopt OAuth2-style token fields in authentication responses
- Replace token response fields with OAuth2-style fields: access_token, token_type, expires_in, refresh_token, and scope
- Add a test assertion to verify that refresh_token is present and not empty in the response

fix https://github.com/appleboy/gin-jwt/issues/346

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Login and token refresh responses now include updated JSON fields: "access_token", "token_type", "expires_in", "refresh_token", and "scope" for improved clarity and compatibility.
  * Enhanced error handling returns a 401 Unauthorized response if refresh token retrieval fails.

* **Bug Fixes**
  * Token refresh responses now reliably include a non-empty "refresh_token" field.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Patch:
diff --git a/auth_jwt.go b/auth_jwt.go
index fabb6d9..07ef7ee 100644
--- a/auth_jwt.go
+++ b/auth_jwt.go
@@ -356,10 +356,20 @@ func (mw *GinJWTMiddleware) MiddlewareInit() error {
 
 	if mw.LoginResponse == nil {
 		mw.LoginResponse = func(c *gin.Context, code int, token string, expire time.Time) {
+			refreshToken, _, err := mw.RefreshToken(c)
+			if err != nil {
+				c.JSON(http.StatusUnauthorized, gin.H{
+					"code":    http.StatusUnauthorized,
+					"message": mw.HTTPStatusMessageFunc(err, c),
+				})
+				return
+			}
 			c.JSON(http.StatusOK, gin.H{
-				"code":   http.StatusOK,
-				"token":  token,
-				"expire": expire.Format(time.RFC3339),
+				"access_token":  token,
+				"token_type":    "Bearer",
+				"expires_in":    int(time.Until(expire).Seconds()),
+				"refresh_token": refreshToken,
+				"scope":         "create",
 			})
 		}
 	}
@@ -374,10 +384,20 @@ func (mw *GinJWTMiddleware) MiddlewareInit() error {
 
 	if mw.RefreshResponse == nil {
 		mw.RefreshResponse = func(c *gin.Context, code int, token string, expire time.Time) {
+			refreshToken, _, err := mw.RefreshToken(c)
+			if err != nil {
+				c.JSON(http.StatusUnauthorized, gin.H{
+					"code":    http.StatusUnauthorized,
+					"message": mw.HTTPStatusMessageFunc(err, c),
+				})
+				return
+			}
 			c.JSON(http.StatusOK, gin.H{
-				"code":   http.StatusOK,
-				"token":  token,
-				"expire": expire.Format(time.RFC3339),
+				"access_token":  token,
+				"token_type":    "Bearer",
+				"expires_in":    int(time.Until(expire).Seconds()),
+				"refresh_token": refreshToken,
+				"scope":         "create",
 			})
 		}
 	}


Test patch:
diff --git a/auth_jwt_test.go b/auth_jwt_test.go
index fa4036f..384dd02 100644
--- a/auth_jwt_test.go
+++ b/auth_jwt_test.go
@@ -718,7 +718,14 @@ func TestClaimsDuringAuthorization(t *testing.T) {
 				testkey = ""
 			}
 			// Set custom claim, to be checked in Authorizator method
-			return MapClaims{"identity": data.(string), "testkey": testkey, "exp": 0}
+			now := time.Now()
+			return MapClaims{
+				"identity": data.(string),
+				"testkey":  testkey,
+				"exp":      now.Add(time.Hour).Unix(),
+				"iat":      now.Unix(),
+				"nbf":      now.Unix(),
+			}
 		},
 		Authenticator: func(c *gin.Context) (interface{}, error) {
 			var loginVals Login
@@ -780,7 +787,7 @@ func TestClaimsDuringAuthorization(t *testing.T) {
 			"password": "admin",
 		}).
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			token := gjson.Get(r.Body.String(), "token")
+			token := gjson.Get(r.Body.String(), "access_token")
 			userToken = token.String()
 			assert.Equal(t, http.StatusOK, r.Code)
 		})
@@ -799,7 +806,7 @@ func TestClaimsDuringAuthorization(t *testing.T) {
 			"password": "test",
 		}).
 		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
-			token := gjson.Get(r.Body.String(), "token")
+			token := gjson.Get(r.Body.String(), "access_token")
 			userToken = token.String()
 			assert.Equal(t, http.StatusOK, r.Code)
 		})