Repository: appleboy/gin-jwt
Pull request #224 (2019-11-19 00:02:48)

Add LogoutHandler and LogoutResponse to allow user to remove jwt cookie
Adds a basic `LogoutHandler` (with customizable `LogoutResponse` object like `LoginResponse`) to the middleware allowing users to add a route to have the jwt cookie deleted using the same settings as were used to create the cookie.


Patch:
diff --git a/README.md b/README.md
index 67fb135..cb5f9b3 100644
--- a/README.md
+++ b/README.md
@@ -264,7 +264,7 @@ Www-Authenticate: JWT realm=test zone
 ```
 
 ### Cookie Token
-Use these options for setting the JWT in a cookie. See the Mozilla [documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies) for more information on these options. 
+Use these options for setting the JWT in a cookie. See the Mozilla [documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies) for more information on these options.
 
 ```go
 	SendCookie:       true,
@@ -275,13 +275,15 @@ Use these options for setting the JWT in a cookie. See the Mozilla [documentatio
 	TokenLookup:      "cookie:token",
 ```
 
-### Login Flow 
-1. Authenticator: handles the login logic. On success LoginResponse is called, on failure Unauthorized is called. 
+Adding a route to the `LogoutHandler` route will the deletion of the auth cookie, effectively logging the user out. The `LoginResponse` object can optionally be set to customize the response of this endpoint.
+
+### Login Flow
+1. Authenticator: handles the login logic. On success LoginResponse is called, on failure Unauthorized is called.
 2. LoginResponse: optional, allows setting a custom response such as a redirect.
 
 
-### JWT Flow 
-1. PayloadFunc: maps the claims in the JWT. 
-2. IdentityHandler: extracts identity from claims. 
+### JWT Flow
+1. PayloadFunc: maps the claims in the JWT.
+2. IdentityHandler: extracts identity from claims.
 3. Authorizator: receives identity and handles authorization logic.
 4. Unauthorized: handles unauthorized logic.
diff --git a/auth_jwt.go b/auth_jwt.go
index 0f7f3eb..efb1d24 100644
--- a/auth_jwt.go
+++ b/auth_jwt.go
@@ -65,6 +65,9 @@ type GinJWTMiddleware struct {
 	// User can define own LoginResponse func.
 	LoginResponse func(*gin.Context, int, string, time.Time)
 
+	// User can define own LogoutResponse func.
+	LogoutResponse func(*gin.Context, int)
+
 	// User can define own RefreshResponse func.
 	RefreshResponse func(*gin.Context, int, string, time.Time)
 
@@ -293,6 +296,14 @@ func (mw *GinJWTMiddleware) MiddlewareInit() error {
 		}
 	}
 
+	if mw.LogoutResponse == nil {
+		mw.LogoutResponse = func(c *gin.Context, code int) {
+			c.JSON(http.StatusOK, gin.H{
+				"code": http.StatusOK,
+			})
+		}
+	}
+
 	if mw.RefreshResponse == nil {
 		mw.RefreshResponse = func(c *gin.Context, code int, token string, expire time.Time) {
 			c.JSON(http.StatusOK, gin.H{
@@ -457,6 +468,24 @@ func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
 	mw.LoginResponse(c, http.StatusOK, tokenString, expire)
 }
 
+// LogoutHandler can be used by clients to remove the jwt cookie (if set)
+func (mw *GinJWTMiddleware) LogoutHandler(c *gin.Context) {
+	// delete auth cookie
+	if mw.SendCookie {
+		c.SetCookie(
+			mw.CookieName,
+			"",
+			-1,
+			"/",
+			mw.CookieDomain,
+			mw.SecureCookie,
+			mw.CookieHTTPOnly,
+		)
+	}
+
+	mw.LogoutResponse(c, http.StatusOK)
+}
+
 func (mw *GinJWTMiddleware) signedString(token *jwt.Token) (string, error) {
 	var tokenString string
 	var err error
diff --git a/go.sum b/go.sum
index 0daac8b..53c701d 100644
--- a/go.sum
+++ b/go.sum
@@ -59,6 +59,7 @@ github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBf
 github.com/siddontang/ledisdb v0.0.0-20181029004158-becf5f38d373/go.mod h1:mF1DpOSOUiJRMR+FDqaqu3EBqrybQtrDDszLUZ6oxPg=
 github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA=
 github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE=
+github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=


Test patch:
diff --git a/auth_jwt_test.go b/auth_jwt_test.go
index e71d309..79c431a 100644
--- a/auth_jwt_test.go
+++ b/auth_jwt_test.go
@@ -2,6 +2,7 @@ package jwt
 
 import (
 	"errors"
+	"fmt"
 	"io/ioutil"
 	"log"
 	"net/http"
@@ -154,6 +155,7 @@ func ginHandler(auth *GinJWTMiddleware) *gin.Engine {
 	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)
 
@@ -1195,3 +1197,28 @@ func TestCheckTokenString(t *testing.T) {
 	assert.Error(t, err)
 	assert.Equal(t, MapClaims{}, ExtractClaimsFromToken(nil))
 }
+
+func TestLogout(t *testing.T) {
+	cookieName := "jwt"
+	cookieDomain := "example.com"
+	// the middleware to test
+	authMiddleware, _ := New(&GinJWTMiddleware{
+		Realm:         "test zone",
+		Key:           key,
+		Timeout:       time.Hour,
+		Authenticator: defaultAuthenticator,
+		SendCookie:    true,
+		CookieName:    cookieName,
+		CookieDomain:  cookieDomain,
+	})
+
+	handler := ginHandler(authMiddleware)
+
+	r := gofight.New()
+
+	r.POST("/logout").
+		Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
+			assert.Equal(t, http.StatusOK, r.Code)
+			assert.Equal(t, fmt.Sprintf("%s=; Path=/; Domain=%s; Max-Age=0", cookieName, cookieDomain), r.HeaderMap.Get("Set-Cookie"))
+		})
+}