├── .github ├── dependabot.yml └── workflows │ ├── bearer.yml │ ├── codeql.yml │ ├── go.yml │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── _example └── basic │ ├── go.mod │ ├── go.sum │ └── server.go ├── auth_jwt.go ├── auth_jwt_test.go ├── go.mod ├── go.sum ├── screenshot ├── login.png └── refresh_token.png └── testdata ├── invalidprivkey.key ├── invalidpubkey.key ├── jwtRS256.key └── jwtRS256.key.pub /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | - package-ecosystem: gomod 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | -------------------------------------------------------------------------------- /.github/workflows/bearer.yml: -------------------------------------------------------------------------------- 1 | name: Bearer PR Check 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | rule_check: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - uses: reviewdog/action-setup@v1 19 | with: 20 | reviewdog_version: latest 21 | 22 | - name: Run Report 23 | id: report 24 | uses: bearer/bearer-action@v2 25 | with: 26 | format: rdjson 27 | output: rd.json 28 | diff: true 29 | 30 | - name: Run reviewdog 31 | if: always() 32 | env: 33 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | cat rd.json | reviewdog -f=rdjson -reporter=github-pr-review 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [master] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [master] 20 | schedule: 21 | - cron: "41 23 * * 6" 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ["go"] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v5 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v7 27 | with: 28 | args: --verbose 29 | test: 30 | strategy: 31 | matrix: 32 | os: [ubuntu-latest] 33 | go: [1.23, 1.24] 34 | include: 35 | - os: ubuntu-latest 36 | go-build: ~/.cache/go-build 37 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 38 | runs-on: ${{ matrix.os }} 39 | env: 40 | GO111MODULE: on 41 | GOPROXY: https://proxy.golang.org 42 | steps: 43 | - name: Checkout Code 44 | uses: actions/checkout@v4 45 | with: 46 | ref: ${{ github.ref }} 47 | 48 | - name: Set up Go ${{ matrix.go }} 49 | uses: actions/setup-go@v5 50 | with: 51 | go-version: ${{ matrix.go }} 52 | 53 | - uses: actions/cache@v4 54 | with: 55 | path: | 56 | ${{ matrix.go-build }} 57 | ~/go/pkg/mod 58 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 59 | restore-keys: | 60 | ${{ runner.os }}-go- 61 | - name: Run Tests 62 | run: | 63 | go test -v -covermode=atomic -coverprofile=coverage.out 64 | 65 | - name: Upload coverage to Codecov 66 | uses: codecov/codecov-action@v5 67 | with: 68 | flags: ${{ matrix.os }},go-${{ matrix.go }} 69 | -------------------------------------------------------------------------------- /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: Goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: go.mod 21 | check-latest: true 22 | - name: Run GoReleaser 23 | uses: goreleaser/goreleaser-action@v6 24 | with: 25 | # either 'goreleaser' (default) or 'goreleaser-pro' 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | .DS_Store 26 | .vscode 27 | coverage.out 28 | .idea 29 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | builds: 2 | - # If true, skip the build. 3 | # Useful for library projects. 4 | # Default is false 5 | skip: true 6 | 7 | changelog: 8 | use: github 9 | groups: 10 | - title: Features 11 | regexp: "^.*feat[(\\w)]*:+.*$" 12 | order: 0 13 | - title: "Bug fixes" 14 | regexp: "^.*fix[(\\w)]*:+.*$" 15 | order: 1 16 | - title: "Enhancements" 17 | regexp: "^.*chore[(\\w)]*:+.*$" 18 | order: 2 19 | - title: "Refactor" 20 | regexp: "^.*refactor[(\\w)]*:+.*$" 21 | order: 3 22 | - title: "Build process updates" 23 | regexp: ^.*?(build|ci)(\(.+\))??!?:.+$ 24 | order: 4 25 | - title: "Documentation updates" 26 | regexp: ^.*?docs?(\(.+\))??!?:.+$ 27 | order: 4 28 | - title: Others 29 | order: 999 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Bo-Yi Wu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT Middleware for Gin Framework 2 | 3 | [![Run Tests](https://github.com/appleboy/gin-jwt/actions/workflows/go.yml/badge.svg)](https://github.com/appleboy/gin-jwt/actions/workflows/go.yml) 4 | [![GitHub tag](https://img.shields.io/github/tag/appleboy/gin-jwt.svg)](https://github.com/appleboy/gin-jwt/releases) 5 | [![GoDoc](https://godoc.org/github.com/appleboy/gin-jwt?status.svg)](https://godoc.org/github.com/appleboy/gin-jwt) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/gin-jwt)](https://goreportcard.com/report/github.com/appleboy/gin-jwt) 7 | [![codecov](https://codecov.io/gh/appleboy/gin-jwt/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/gin-jwt) 8 | [![codebeat badge](https://codebeat.co/badges/c4015f07-df23-4c7c-95ba-9193a12e14b1)](https://codebeat.co/projects/github-com-appleboy-gin-jwt) 9 | [![Sourcegraph](https://sourcegraph.com/github.com/appleboy/gin-jwt/-/badge.svg)](https://sourcegraph.com/github.com/appleboy/gin-jwt?badge) 10 | 11 | This is a middleware for [Gin](https://github.com/gin-gonic/gin) framework. 12 | 13 | It uses [jwt-go](https://github.com/golang-jwt/jwt) to provide a jwt authentication middleware. It provides additional handler functions to provide the `login` api that will generate the token and an additional `refresh` handler that can be used to refresh tokens. 14 | 15 | ## Security Issue 16 | 17 | Simple HS256 JWT token brute force cracker. Effective only to crack JWT tokens with weak secrets. **Recommendation**: Use strong long secrets or `RS256` tokens. See the [jwt-cracker repository](https://github.com/lmammino/jwt-cracker). 18 | 19 | ## Usage 20 | 21 | Download and install using [go module](https://blog.golang.org/using-go-modules): 22 | 23 | ```sh 24 | export GO111MODULE=on 25 | go get github.com/appleboy/gin-jwt/v2 26 | ``` 27 | 28 | Import it in your code: 29 | 30 | ```go 31 | import "github.com/appleboy/gin-jwt/v2" 32 | ``` 33 | 34 | Download and install without using [go module](https://blog.golang.org/using-go-modules): 35 | 36 | ```sh 37 | go get github.com/appleboy/gin-jwt 38 | ``` 39 | 40 | Import it in your code: 41 | 42 | ```go 43 | import "github.com/appleboy/gin-jwt" 44 | ``` 45 | 46 | ## Example 47 | 48 | Please see [the example file](_example/basic/server.go) and you can use `ExtractClaims` to fetch user data. 49 | 50 | ```go 51 | package main 52 | 53 | import ( 54 | "log" 55 | "net/http" 56 | "os" 57 | "time" 58 | 59 | jwt "github.com/appleboy/gin-jwt/v2" 60 | "github.com/gin-gonic/gin" 61 | ) 62 | 63 | type login struct { 64 | Username string `form:"username" json:"username" binding:"required"` 65 | Password string `form:"password" json:"password" binding:"required"` 66 | } 67 | 68 | var ( 69 | identityKey = "id" 70 | port string 71 | ) 72 | 73 | // User demo 74 | type User struct { 75 | UserName string 76 | FirstName string 77 | LastName string 78 | } 79 | 80 | func init() { 81 | port = os.Getenv("PORT") 82 | if port == "" { 83 | port = "8000" 84 | } 85 | } 86 | 87 | func main() { 88 | engine := gin.Default() 89 | // the jwt middleware 90 | authMiddleware, err := jwt.New(initParams()) 91 | if err != nil { 92 | log.Fatal("JWT Error:" + err.Error()) 93 | } 94 | 95 | // register middleware 96 | engine.Use(handlerMiddleware(authMiddleware)) 97 | 98 | // register route 99 | registerRoute(engine, authMiddleware) 100 | 101 | // start http server 102 | if err = http.ListenAndServe(":"+port, engine); err != nil { 103 | log.Fatal(err) 104 | } 105 | } 106 | 107 | func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) { 108 | r.POST("/login", handle.LoginHandler) 109 | r.NoRoute(handle.MiddlewareFunc(), handleNoRoute()) 110 | 111 | auth := r.Group("/auth", handle.MiddlewareFunc()) 112 | auth.GET("/refresh_token", handle.RefreshHandler) 113 | auth.GET("/hello", helloHandler) 114 | } 115 | 116 | func handlerMiddleware(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc { 117 | return func(context *gin.Context) { 118 | errInit := authMiddleware.MiddlewareInit() 119 | if errInit != nil { 120 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 121 | } 122 | } 123 | } 124 | 125 | func initParams() *jwt.GinJWTMiddleware { 126 | 127 | return &jwt.GinJWTMiddleware{ 128 | Realm: "test zone", 129 | Key: []byte("secret key"), 130 | Timeout: time.Hour, 131 | MaxRefresh: time.Hour, 132 | IdentityKey: identityKey, 133 | PayloadFunc: payloadFunc(), 134 | 135 | IdentityHandler: identityHandler(), 136 | Authenticator: authenticator(), 137 | Authorizator: authorizator(), 138 | Unauthorized: unauthorized(), 139 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 140 | // TokenLookup: "query:token", 141 | // TokenLookup: "cookie:token", 142 | TokenHeadName: "Bearer", 143 | TimeFunc: time.Now, 144 | } 145 | } 146 | 147 | func payloadFunc() func(data interface{}) jwt.MapClaims { 148 | return func(data interface{}) jwt.MapClaims { 149 | if v, ok := data.(*User); ok { 150 | return jwt.MapClaims{ 151 | identityKey: v.UserName, 152 | } 153 | } 154 | return jwt.MapClaims{} 155 | } 156 | } 157 | 158 | func identityHandler() func(c *gin.Context) interface{} { 159 | return func(c *gin.Context) interface{} { 160 | claims := jwt.ExtractClaims(c) 161 | return &User{ 162 | UserName: claims[identityKey].(string), 163 | } 164 | } 165 | } 166 | 167 | func authenticator() func(c *gin.Context) (interface{}, error) { 168 | return func(c *gin.Context) (interface{}, error) { 169 | var loginVals login 170 | if err := c.ShouldBind(&loginVals); err != nil { 171 | return "", jwt.ErrMissingLoginValues 172 | } 173 | userID := loginVals.Username 174 | password := loginVals.Password 175 | 176 | if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") { 177 | return &User{ 178 | UserName: userID, 179 | LastName: "Bo-Yi", 180 | FirstName: "Wu", 181 | }, nil 182 | } 183 | return nil, jwt.ErrFailedAuthentication 184 | } 185 | } 186 | 187 | func authorizator() func(data interface{}, c *gin.Context) bool { 188 | return func(data interface{}, c *gin.Context) bool { 189 | if v, ok := data.(*User); ok && v.UserName == "admin" { 190 | return true 191 | } 192 | return false 193 | } 194 | } 195 | 196 | func unauthorized() func(c *gin.Context, code int, message string) { 197 | return func(c *gin.Context, code int, message string) { 198 | c.JSON(code, gin.H{ 199 | "code": code, 200 | "message": message, 201 | }) 202 | } 203 | } 204 | 205 | func handleNoRoute() func(c *gin.Context) { 206 | return func(c *gin.Context) { 207 | claims := jwt.ExtractClaims(c) 208 | log.Printf("NoRoute claims: %#v\n", claims) 209 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 210 | } 211 | } 212 | 213 | func helloHandler(c *gin.Context) { 214 | claims := jwt.ExtractClaims(c) 215 | user, _ := c.Get(identityKey) 216 | c.JSON(200, gin.H{ 217 | "userID": claims[identityKey], 218 | "userName": user.(*User).UserName, 219 | "text": "Hello World.", 220 | }) 221 | } 222 | 223 | ``` 224 | 225 | ## Demo 226 | 227 | Please run _example/basic/server.go file and listen `8000` port. 228 | 229 | ```sh 230 | go run _example/basic/server.go 231 | ``` 232 | 233 | Download and install [httpie](https://github.com/jkbrzt/httpie) CLI HTTP client. 234 | 235 | ### Login API 236 | 237 | ```sh 238 | http -v --json POST localhost:8000/login username=admin password=admin 239 | ``` 240 | 241 | Output screenshot 242 | 243 | ![api screenshot](screenshot/login.png) 244 | 245 | ### Refresh token API 246 | 247 | ```bash 248 | http -v -f GET localhost:8000/auth/refresh_token "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json" 249 | ``` 250 | 251 | Output screenshot 252 | 253 | ![api screenshot](screenshot/refresh_token.png) 254 | 255 | ### Hello world 256 | 257 | Please login as `admin` and password as `admin` 258 | 259 | ```bash 260 | http -f GET localhost:8000/auth/hello "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json" 261 | ``` 262 | 263 | Response message `200 OK`: 264 | 265 | ```sh 266 | HTTP/1.1 200 OK 267 | Content-Length: 24 268 | Content-Type: application/json; charset=utf-8 269 | Date: Sat, 19 Mar 2016 03:02:57 GMT 270 | 271 | { 272 | "text": "Hello World.", 273 | "userID": "admin" 274 | } 275 | ``` 276 | 277 | ### Authorization 278 | 279 | Please login as `test` and password as `test` 280 | 281 | ```bash 282 | http -f GET localhost:8000/auth/hello "Authorization:Bearer xxxxxxxxx" "Content-Type: application/json" 283 | ``` 284 | 285 | Response message `403 Forbidden`: 286 | 287 | ```sh 288 | HTTP/1.1 403 Forbidden 289 | Content-Length: 62 290 | Content-Type: application/json; charset=utf-8 291 | Date: Sat, 19 Mar 2016 03:05:40 GMT 292 | Www-Authenticate: JWT realm=test zone 293 | 294 | { 295 | "code": 403, 296 | "message": "You don't have permission to access." 297 | } 298 | ``` 299 | 300 | ### Cookie Token 301 | 302 | 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. 303 | 304 | ```go 305 | SendCookie: true, 306 | SecureCookie: false, //non HTTPS dev environments 307 | CookieHTTPOnly: true, // JS can't modify 308 | CookieDomain: "localhost:8080", 309 | CookieName: "token", // default jwt 310 | TokenLookup: "cookie:token", 311 | CookieSameSite: http.SameSiteDefaultMode, //SameSiteDefaultMode, SameSiteLaxMode, SameSiteStrictMode, SameSiteNoneMode 312 | ``` 313 | 314 | ### Login request flow (using the LoginHandler) 315 | 316 | PROVIDED: `LoginHandler` 317 | 318 | This is a provided function to be called on any login endpoint, which will trigger the flow described below. 319 | 320 | REQUIRED: `Authenticator` 321 | 322 | This function should verify the user credentials given the gin context (i.e. password matches hashed password for a given user email, and any other authentication logic). Then the authenticator should return a struct or map that contains the user data that will be embedded in the jwt token. This might be something like an account id, role, is_verified, etc. After having successfully authenticated, the data returned from the authenticator is passed in as a parameter into the `PayloadFunc`, which is used to embed the user identifiers mentioned above into the jwt token. If an error is returned, the `Unauthorized` function is used (explained below). 323 | 324 | OPTIONAL: `PayloadFunc` 325 | 326 | This function is called after having successfully authenticated (logged in). It should take whatever was returned from `Authenticator` and convert it into `MapClaims` (i.e. map[string]interface{}). A typical use case of this function is for when `Authenticator` returns a struct which holds the user identifiers, and that struct needs to be converted into a map. `MapClaims` should include one element that is [`IdentityKey` (default is "identity"): some_user_identity]. The elements of `MapClaims` returned in `PayloadFunc` will be embedded within the jwt token (as token claims). When users pass in their token on subsequent requests, you can get these claims back by using `ExtractClaims`. 327 | 328 | OPTIONAL: `LoginResponse` 329 | 330 | After having successfully authenticated with `Authenticator`, created the jwt token using the identifiers from map returned from `PayloadFunc`, and set it as a cookie if `SendCookie` is enabled, this function is called. It is used to handle any post-login logic. This might look something like using the gin context to return a JSON of the token back to the user. 331 | 332 | ### Subsequent requests on endpoints requiring jwt token (using MiddlewareFunc) 333 | 334 | PROVIDED: `MiddlewareFunc` 335 | 336 | This is gin middleware that should be used within any endpoints that require the jwt token to be present. This middleware will parse the request headers for the token if it exists, and check that the jwt token is valid (not expired, correct signature). Then it will call `IdentityHandler` followed by `Authorizator`. If `Authorizator` passes and all of the previous token validity checks passed, the middleware will continue the request. If any of these checks fail, the `Unauthorized` function is used (explained below). 337 | 338 | OPTIONAL: `IdentityHandler` 339 | 340 | The default of this function is likely sufficient for your needs. The purpose of this function is to fetch the user identity from claims embedded within the jwt token, and pass this identity value to `Authorizator`. This function assumes [`IdentityKey`: some_user_identity] is one of the attributes embedded within the claims of the jwt token (determined by `PayloadFunc`). 341 | 342 | OPTIONAL: `Authorizator` 343 | 344 | Given the user identity value (`data` parameter) and the gin context, this function should check if the user is authorized to be reaching this endpoint (on the endpoints where the `MiddlewareFunc` applies). This function should likely use `ExtractClaims` to check if the user has the sufficient permissions to reach this endpoint, as opposed to hitting the database on every request. This function should return true if the user is authorized to continue through with the request, or false if they are not authorized (where `Unauthorized` will be called). 345 | 346 | ### Logout Request flow (using LogoutHandler) 347 | 348 | PROVIDED: `LogoutHandler` 349 | 350 | This is a provided function to be called on any logout endpoint, which will clear any cookies if `SendCookie` is set, and then call `LogoutResponse`. 351 | 352 | OPTIONAL: `LogoutResponse` 353 | 354 | This should likely just return back to the user the http status code, if logout was successful or not. 355 | 356 | ### Refresh Request flow (using RefreshHandler) 357 | 358 | PROVIDED: `RefreshHandler`: 359 | 360 | 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` 361 | 362 | OPTIONAL: `RefreshResponse`: 363 | 364 | This should likely return a JSON of the token back to the user, similar to `LoginResponse` 365 | 366 | ### Failures with logging in, bad tokens, or lacking privileges 367 | 368 | OPTIONAL `Unauthorized`: 369 | 370 | On any error logging in, authorizing the user, or when there was no token or a invalid token passed in with the request, the following will happen. The gin context will be aborted depending on `DisabledAbort`, then `HTTPStatusMessageFunc` is called which by default converts the error into a string. Finally the `Unauthorized` function will be called. This function should likely return a JSON containing the http error code and error message to the user. 371 | -------------------------------------------------------------------------------- /_example/basic/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt/v2 v2.10.2 7 | github.com/gin-gonic/gin v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/sonic v1.12.9 // indirect 12 | github.com/bytedance/sonic/loader v0.2.3 // indirect 13 | github.com/cloudwego/base64x v0.1.5 // indirect 14 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 15 | github.com/gin-contrib/sse v1.0.0 // indirect 16 | github.com/go-playground/locales v0.14.1 // indirect 17 | github.com/go-playground/universal-translator v0.18.1 // indirect 18 | github.com/go-playground/validator/v10 v10.25.0 // indirect 19 | github.com/goccy/go-json v0.10.5 // indirect 20 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 23 | github.com/kr/text v0.2.0 // indirect 24 | github.com/leodido/go-urn v1.4.0 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 27 | github.com/modern-go/reflect2 v1.0.2 // indirect 28 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 29 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 30 | github.com/ugorji/go/codec v1.2.12 // indirect 31 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 32 | golang.org/x/arch v0.14.0 // indirect 33 | golang.org/x/crypto v0.36.0 // indirect 34 | golang.org/x/net v0.37.0 // indirect 35 | golang.org/x/sys v0.31.0 // indirect 36 | golang.org/x/text v0.23.0 // indirect 37 | google.golang.org/protobuf v1.36.5 // indirect 38 | gopkg.in/yaml.v3 v3.0.1 // indirect 39 | ) 40 | 41 | replace github.com/appleboy/gin-jwt/v2 => ../../ 42 | -------------------------------------------------------------------------------- /_example/basic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 2 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 3 | github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ= 4 | github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= 7 | github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 8 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 9 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 16 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 17 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 18 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 19 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 20 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= 28 | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 29 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 30 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 31 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 32 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 33 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 34 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 36 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 37 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 38 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 39 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 40 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 41 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 42 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 43 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 44 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 46 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 47 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 48 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 49 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 54 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 55 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 56 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 59 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 60 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 63 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 64 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 65 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 66 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 69 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 70 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 71 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 72 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 73 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 74 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 75 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 76 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 77 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 78 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 79 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 80 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 81 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 82 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 83 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 84 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 85 | golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= 86 | golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 87 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 88 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 89 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 90 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 91 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 93 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 94 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 95 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 96 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 97 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 98 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 99 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 100 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 101 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 102 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 103 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 105 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 106 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 107 | -------------------------------------------------------------------------------- /_example/basic/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "time" 8 | 9 | jwt "github.com/appleboy/gin-jwt/v2" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type login struct { 14 | Username string `form:"username" json:"username" binding:"required"` 15 | Password string `form:"password" json:"password" binding:"required"` 16 | } 17 | 18 | var ( 19 | identityKey = "id" 20 | port string 21 | ) 22 | 23 | // User demo 24 | type User struct { 25 | UserName string 26 | FirstName string 27 | LastName string 28 | } 29 | 30 | func init() { 31 | port = os.Getenv("PORT") 32 | if port == "" { 33 | port = "8000" 34 | } 35 | } 36 | 37 | func main() { 38 | engine := gin.Default() 39 | // the jwt middleware 40 | authMiddleware, err := jwt.New(initParams()) 41 | if err != nil { 42 | log.Fatal("JWT Error:" + err.Error()) 43 | } 44 | 45 | // register middleware 46 | engine.Use(handlerMiddleWare(authMiddleware)) 47 | 48 | // register route 49 | registerRoute(engine, authMiddleware) 50 | 51 | // start http server 52 | if err = http.ListenAndServe(":"+port, engine); err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | 57 | func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) { 58 | r.POST("/login", handle.LoginHandler) 59 | r.NoRoute(handle.MiddlewareFunc(), handleNoRoute()) 60 | 61 | auth := r.Group("/auth", handle.MiddlewareFunc()) 62 | auth.GET("/refresh_token", handle.RefreshHandler) 63 | auth.GET("/hello", helloHandler) 64 | } 65 | 66 | func handlerMiddleWare(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc { 67 | return func(context *gin.Context) { 68 | errInit := authMiddleware.MiddlewareInit() 69 | if errInit != nil { 70 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 71 | } 72 | } 73 | } 74 | 75 | func initParams() *jwt.GinJWTMiddleware { 76 | return &jwt.GinJWTMiddleware{ 77 | Realm: "test zone", 78 | Key: []byte("secret key"), 79 | Timeout: time.Hour, 80 | MaxRefresh: time.Hour, 81 | IdentityKey: identityKey, 82 | PayloadFunc: payloadFunc(), 83 | 84 | IdentityHandler: identityHandler(), 85 | Authenticator: authenticator(), 86 | Authorizator: authorizator(), 87 | Unauthorized: unauthorized(), 88 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 89 | // TokenLookup: "query:token", 90 | // TokenLookup: "cookie:token", 91 | TokenHeadName: "Bearer", 92 | TimeFunc: time.Now, 93 | } 94 | } 95 | 96 | func payloadFunc() func(data interface{}) jwt.MapClaims { 97 | return func(data interface{}) jwt.MapClaims { 98 | if v, ok := data.(*User); ok { 99 | return jwt.MapClaims{ 100 | identityKey: v.UserName, 101 | } 102 | } 103 | return jwt.MapClaims{} 104 | } 105 | } 106 | 107 | func identityHandler() func(c *gin.Context) interface{} { 108 | return func(c *gin.Context) interface{} { 109 | claims := jwt.ExtractClaims(c) 110 | return &User{ 111 | UserName: claims[identityKey].(string), 112 | } 113 | } 114 | } 115 | 116 | func authenticator() func(c *gin.Context) (interface{}, error) { 117 | return func(c *gin.Context) (interface{}, error) { 118 | var loginVals login 119 | if err := c.ShouldBind(&loginVals); err != nil { 120 | return "", jwt.ErrMissingLoginValues 121 | } 122 | userID := loginVals.Username 123 | password := loginVals.Password 124 | 125 | if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") { 126 | return &User{ 127 | UserName: userID, 128 | LastName: "Bo-Yi", 129 | FirstName: "Wu", 130 | }, nil 131 | } 132 | return nil, jwt.ErrFailedAuthentication 133 | } 134 | } 135 | 136 | func authorizator() func(data interface{}, c *gin.Context) bool { 137 | return func(data interface{}, c *gin.Context) bool { 138 | if v, ok := data.(*User); ok && v.UserName == "admin" { 139 | return true 140 | } 141 | return false 142 | } 143 | } 144 | 145 | func unauthorized() func(c *gin.Context, code int, message string) { 146 | return func(c *gin.Context, code int, message string) { 147 | c.JSON(code, gin.H{ 148 | "code": code, 149 | "message": message, 150 | }) 151 | } 152 | } 153 | 154 | func handleNoRoute() func(c *gin.Context) { 155 | return func(c *gin.Context) { 156 | claims := jwt.ExtractClaims(c) 157 | log.Printf("NoRoute claims: %#v\n", claims) 158 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 159 | } 160 | } 161 | 162 | func helloHandler(c *gin.Context) { 163 | claims := jwt.ExtractClaims(c) 164 | user, _ := c.Get(identityKey) 165 | c.JSON(200, gin.H{ 166 | "userID": claims[identityKey], 167 | "userName": user.(*User).UserName, 168 | "text": "Hello World.", 169 | }) 170 | } 171 | -------------------------------------------------------------------------------- /auth_jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "crypto/rsa" 5 | "encoding/json" 6 | "errors" 7 | "net/http" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/golang-jwt/jwt/v4" 14 | "github.com/youmark/pkcs8" 15 | ) 16 | 17 | type MapClaims jwt.MapClaims 18 | 19 | // GinJWTMiddleware provides a Json-Web-Token authentication implementation. On failure, a 401 HTTP response 20 | // is returned. On success, the wrapped middleware is called, and the userID is made available as 21 | // c.Get("userID").(string). 22 | // Users can get a token by posting a json request to LoginHandler. The token then needs to be passed in 23 | // the Authentication header. Example: Authorization:Bearer XXX_TOKEN_XXX 24 | type GinJWTMiddleware struct { 25 | // Realm name to display to the user. Required. 26 | Realm string 27 | 28 | // signing algorithm - possible values are HS256, HS384, HS512, RS256, RS384 or RS512 29 | // Optional, default is HS256. 30 | SigningAlgorithm string 31 | 32 | // Secret key used for signing. Required. 33 | Key []byte 34 | 35 | // Callback to retrieve key used for signing. Setting KeyFunc will bypass 36 | // all other key settings 37 | KeyFunc func(token *jwt.Token) (interface{}, error) 38 | 39 | // Duration that a jwt token is valid. Optional, defaults to one hour. 40 | Timeout time.Duration 41 | // Callback function that will override the default timeout duration. 42 | TimeoutFunc func(data interface{}) time.Duration 43 | 44 | // This field allows clients to refresh their token until MaxRefresh has passed. 45 | // Note that clients can refresh their token in the last moment of MaxRefresh. 46 | // This means that the maximum validity timespan for a token is TokenTime + MaxRefresh. 47 | // Optional, defaults to 0 meaning not refreshable. 48 | MaxRefresh time.Duration 49 | 50 | // Callback function that should perform the authentication of the user based on login info. 51 | // Must return user data as user identifier, it will be stored in Claim Array. Required. 52 | // Check error (e) to determine the appropriate error message. 53 | Authenticator func(c *gin.Context) (interface{}, error) 54 | 55 | // Callback function that should perform the authorization of the authenticated user. Called 56 | // only after an authentication success. Must return true on success, false on failure. 57 | // Optional, default to success. 58 | Authorizator func(data interface{}, c *gin.Context) bool 59 | 60 | // Callback function that will be called during login. 61 | // Using this function it is possible to add additional payload data to the webtoken. 62 | // The data is then made available during requests via c.Get("JWT_PAYLOAD"). 63 | // Note that the payload is not encrypted. 64 | // The attributes mentioned on jwt.io can't be used as keys for the map. 65 | // Optional, by default no additional data will be set. 66 | PayloadFunc func(data interface{}) MapClaims 67 | 68 | // User can define own Unauthorized func. 69 | Unauthorized func(c *gin.Context, code int, message string) 70 | 71 | // User can define own LoginResponse func. 72 | LoginResponse func(c *gin.Context, code int, message string, time time.Time) 73 | 74 | // User can define own LogoutResponse func. 75 | LogoutResponse func(c *gin.Context, code int) 76 | 77 | // User can define own RefreshResponse func. 78 | RefreshResponse func(c *gin.Context, code int, message string, time time.Time) 79 | 80 | // Set the identity handler function 81 | IdentityHandler func(*gin.Context) interface{} 82 | 83 | // Set the identity key 84 | IdentityKey string 85 | 86 | // TokenLookup is a string in the form of ":" that is used 87 | // to extract token from the request. 88 | // Optional. Default value "header:Authorization". 89 | // Possible values: 90 | // - "header:" 91 | // - "query:" 92 | // - "cookie:" 93 | TokenLookup string 94 | 95 | // TokenHeadName is a string in the header. Default value is "Bearer" 96 | TokenHeadName string 97 | 98 | // TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens. 99 | TimeFunc func() time.Time 100 | 101 | // HTTP Status messages for when something in the JWT middleware fails. 102 | // Check error (e) to determine the appropriate error message. 103 | HTTPStatusMessageFunc func(e error, c *gin.Context) string 104 | 105 | // Private key file for asymmetric algorithms 106 | PrivKeyFile string 107 | 108 | // Private Key bytes for asymmetric algorithms 109 | // 110 | // Note: PrivKeyFile takes precedence over PrivKeyBytes if both are set 111 | PrivKeyBytes []byte 112 | 113 | // Public key file for asymmetric algorithms 114 | PubKeyFile string 115 | 116 | // Private key passphrase 117 | PrivateKeyPassphrase string 118 | 119 | // Public key bytes for asymmetric algorithms. 120 | // 121 | // Note: PubKeyFile takes precedence over PubKeyBytes if both are set 122 | PubKeyBytes []byte 123 | 124 | // Private key 125 | privKey *rsa.PrivateKey 126 | 127 | // Public key 128 | pubKey *rsa.PublicKey 129 | 130 | // Optionally return the token as a cookie 131 | SendCookie bool 132 | 133 | // Duration that a cookie is valid. Optional, by default equals to Timeout value. 134 | CookieMaxAge time.Duration 135 | 136 | // Allow insecure cookies for development over http 137 | SecureCookie bool 138 | 139 | // Allow cookies to be accessed client side for development 140 | CookieHTTPOnly bool 141 | 142 | // Allow cookie domain change for development 143 | CookieDomain string 144 | 145 | // SendAuthorization allow return authorization header for every request 146 | SendAuthorization bool 147 | 148 | // Disable abort() of context. 149 | DisabledAbort bool 150 | 151 | // CookieName allow cookie name change for development 152 | CookieName string 153 | 154 | // CookieSameSite allow use http.SameSite cookie param 155 | CookieSameSite http.SameSite 156 | 157 | // ParseOptions allow to modify jwt's parser methods 158 | ParseOptions []jwt.ParserOption 159 | 160 | // Default vaule is "exp" 161 | ExpField string 162 | } 163 | 164 | var ( 165 | // ErrMissingSecretKey indicates Secret key is required 166 | ErrMissingSecretKey = errors.New("secret key is required") 167 | 168 | // ErrForbidden when HTTP status 403 is given 169 | ErrForbidden = errors.New("you don't have permission to access this resource") 170 | 171 | // ErrMissingAuthenticatorFunc indicates Authenticator is required 172 | ErrMissingAuthenticatorFunc = errors.New("ginJWTMiddleware.Authenticator func is undefined") 173 | 174 | // ErrMissingLoginValues indicates a user tried to authenticate without username or password 175 | ErrMissingLoginValues = errors.New("missing Username or Password") 176 | 177 | // ErrFailedAuthentication indicates authentication failed, could be faulty username or password 178 | ErrFailedAuthentication = errors.New("incorrect Username or Password") 179 | 180 | // ErrFailedTokenCreation indicates JWT Token failed to create, reason unknown 181 | ErrFailedTokenCreation = errors.New("failed to create JWT Token") 182 | 183 | // ErrExpiredToken indicates JWT token has expired. Can't refresh. 184 | ErrExpiredToken = errors.New("token is expired") // in practice, this is generated from the jwt library not by us 185 | 186 | // ErrEmptyAuthHeader can be thrown if authing with a HTTP header, the Auth header needs to be set 187 | ErrEmptyAuthHeader = errors.New("auth header is empty") 188 | 189 | // ErrMissingExpField missing exp field in token 190 | ErrMissingExpField = errors.New("missing exp field") 191 | 192 | // ErrWrongFormatOfExp field must be float64 format 193 | ErrWrongFormatOfExp = errors.New("exp must be float64 format") 194 | 195 | // ErrInvalidAuthHeader indicates auth header is invalid, could for example have the wrong Realm name 196 | ErrInvalidAuthHeader = errors.New("auth header is invalid") 197 | 198 | // ErrEmptyQueryToken can be thrown if authing with URL Query, the query token variable is empty 199 | ErrEmptyQueryToken = errors.New("query token is empty") 200 | 201 | // ErrEmptyCookieToken can be thrown if authing with a cookie, the token cookie is empty 202 | ErrEmptyCookieToken = errors.New("cookie token is empty") 203 | 204 | // ErrEmptyParamToken can be thrown if authing with parameter in path, the parameter in path is empty 205 | ErrEmptyParamToken = errors.New("parameter token is empty") 206 | 207 | // ErrInvalidSigningAlgorithm indicates signing algorithm is invalid, needs to be HS256, HS384, HS512, RS256, RS384 or RS512 208 | ErrInvalidSigningAlgorithm = errors.New("invalid signing algorithm") 209 | 210 | // ErrNoPrivKeyFile indicates that the given private key is unreadable 211 | ErrNoPrivKeyFile = errors.New("private key file unreadable") 212 | 213 | // ErrNoPubKeyFile indicates that the given public key is unreadable 214 | ErrNoPubKeyFile = errors.New("public key file unreadable") 215 | 216 | // ErrInvalidPrivKey indicates that the given private key is invalid 217 | ErrInvalidPrivKey = errors.New("private key invalid") 218 | 219 | // ErrInvalidPubKey indicates the the given public key is invalid 220 | ErrInvalidPubKey = errors.New("public key invalid") 221 | 222 | // IdentityKey default identity key 223 | IdentityKey = "identity" 224 | ) 225 | 226 | // New for check error with GinJWTMiddleware 227 | func New(m *GinJWTMiddleware) (*GinJWTMiddleware, error) { 228 | if err := m.MiddlewareInit(); err != nil { 229 | return nil, err 230 | } 231 | 232 | return m, nil 233 | } 234 | 235 | func (mw *GinJWTMiddleware) readKeys() error { 236 | err := mw.privateKey() 237 | if err != nil { 238 | return err 239 | } 240 | 241 | err = mw.publicKey() 242 | if err != nil { 243 | return err 244 | } 245 | return nil 246 | } 247 | 248 | func (mw *GinJWTMiddleware) privateKey() error { 249 | var keyData []byte 250 | var err error 251 | if mw.PrivKeyFile == "" { 252 | keyData = mw.PrivKeyBytes 253 | } else { 254 | var filecontent []byte 255 | filecontent, err = os.ReadFile(mw.PrivKeyFile) 256 | if err != nil { 257 | return ErrNoPrivKeyFile 258 | } 259 | keyData = filecontent 260 | } 261 | 262 | if mw.PrivateKeyPassphrase != "" { 263 | var key interface{} 264 | key, err = pkcs8.ParsePKCS8PrivateKey(keyData, []byte(mw.PrivateKeyPassphrase)) 265 | if err != nil { 266 | return ErrInvalidPrivKey 267 | } 268 | rsaKey, ok := key.(*rsa.PrivateKey) 269 | if !ok { 270 | return ErrInvalidPrivKey 271 | } 272 | mw.privKey = rsaKey 273 | return nil 274 | } 275 | 276 | var key *rsa.PrivateKey 277 | key, err = jwt.ParseRSAPrivateKeyFromPEM(keyData) 278 | if err != nil { 279 | return ErrInvalidPrivKey 280 | } 281 | mw.privKey = key 282 | return nil 283 | } 284 | 285 | func (mw *GinJWTMiddleware) publicKey() error { 286 | var keyData []byte 287 | if mw.PubKeyFile == "" { 288 | keyData = mw.PubKeyBytes 289 | } else { 290 | filecontent, err := os.ReadFile(mw.PubKeyFile) 291 | if err != nil { 292 | return ErrNoPubKeyFile 293 | } 294 | keyData = filecontent 295 | } 296 | 297 | key, err := jwt.ParseRSAPublicKeyFromPEM(keyData) 298 | if err != nil { 299 | return ErrInvalidPubKey 300 | } 301 | mw.pubKey = key 302 | return nil 303 | } 304 | 305 | func (mw *GinJWTMiddleware) usingPublicKeyAlgo() bool { 306 | switch mw.SigningAlgorithm { 307 | case "RS256", "RS512", "RS384": 308 | return true 309 | } 310 | return false 311 | } 312 | 313 | // MiddlewareInit initialize jwt configs. 314 | func (mw *GinJWTMiddleware) MiddlewareInit() error { 315 | if mw.TokenLookup == "" { 316 | mw.TokenLookup = "header:Authorization" 317 | } 318 | 319 | if mw.SigningAlgorithm == "" { 320 | mw.SigningAlgorithm = "HS256" 321 | } 322 | 323 | if mw.Timeout == 0 { 324 | mw.Timeout = time.Hour 325 | } 326 | 327 | if mw.TimeoutFunc == nil { 328 | mw.TimeoutFunc = func(data interface{}) time.Duration { 329 | return mw.Timeout 330 | } 331 | } 332 | 333 | if mw.TimeFunc == nil { 334 | mw.TimeFunc = time.Now 335 | } 336 | 337 | mw.TokenHeadName = strings.TrimSpace(mw.TokenHeadName) 338 | if len(mw.TokenHeadName) == 0 { 339 | mw.TokenHeadName = "Bearer" 340 | } 341 | 342 | if mw.Authorizator == nil { 343 | mw.Authorizator = func(data interface{}, c *gin.Context) bool { 344 | return true 345 | } 346 | } 347 | 348 | if mw.Unauthorized == nil { 349 | mw.Unauthorized = func(c *gin.Context, code int, message string) { 350 | c.JSON(code, gin.H{ 351 | "code": code, 352 | "message": message, 353 | }) 354 | } 355 | } 356 | 357 | if mw.LoginResponse == nil { 358 | mw.LoginResponse = func(c *gin.Context, code int, token string, expire time.Time) { 359 | c.JSON(http.StatusOK, gin.H{ 360 | "code": http.StatusOK, 361 | "token": token, 362 | "expire": expire.Format(time.RFC3339), 363 | }) 364 | } 365 | } 366 | 367 | if mw.LogoutResponse == nil { 368 | mw.LogoutResponse = func(c *gin.Context, code int) { 369 | c.JSON(http.StatusOK, gin.H{ 370 | "code": http.StatusOK, 371 | }) 372 | } 373 | } 374 | 375 | if mw.RefreshResponse == nil { 376 | mw.RefreshResponse = func(c *gin.Context, code int, token string, expire time.Time) { 377 | c.JSON(http.StatusOK, gin.H{ 378 | "code": http.StatusOK, 379 | "token": token, 380 | "expire": expire.Format(time.RFC3339), 381 | }) 382 | } 383 | } 384 | 385 | if mw.IdentityKey == "" { 386 | mw.IdentityKey = IdentityKey 387 | } 388 | 389 | if mw.IdentityHandler == nil { 390 | mw.IdentityHandler = func(c *gin.Context) interface{} { 391 | claims := ExtractClaims(c) 392 | return claims[mw.IdentityKey] 393 | } 394 | } 395 | 396 | if mw.HTTPStatusMessageFunc == nil { 397 | mw.HTTPStatusMessageFunc = func(e error, c *gin.Context) string { 398 | return e.Error() 399 | } 400 | } 401 | 402 | if mw.Realm == "" { 403 | mw.Realm = "gin jwt" 404 | } 405 | 406 | if mw.CookieMaxAge == 0 { 407 | mw.CookieMaxAge = mw.Timeout 408 | } 409 | 410 | if mw.CookieName == "" { 411 | mw.CookieName = "jwt" 412 | } 413 | 414 | if mw.ExpField == "" { 415 | mw.ExpField = "exp" 416 | } 417 | 418 | // bypass other key settings if KeyFunc is set 419 | if mw.KeyFunc != nil { 420 | return nil 421 | } 422 | 423 | if mw.usingPublicKeyAlgo() { 424 | return mw.readKeys() 425 | } 426 | 427 | if mw.Key == nil { 428 | return ErrMissingSecretKey 429 | } 430 | 431 | return nil 432 | } 433 | 434 | // MiddlewareFunc makes GinJWTMiddleware implement the Middleware interface. 435 | func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc { 436 | return func(c *gin.Context) { 437 | mw.middlewareImpl(c) 438 | } 439 | } 440 | 441 | func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) { 442 | claims, err := mw.GetClaimsFromJWT(c) 443 | if err != nil { 444 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c)) 445 | return 446 | } 447 | 448 | switch v := claims[mw.ExpField].(type) { 449 | case nil: 450 | mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c)) 451 | return 452 | case float64: 453 | if int64(v) < mw.TimeFunc().Unix() { 454 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c)) 455 | return 456 | } 457 | case json.Number: 458 | n, err := v.Int64() 459 | if err != nil { 460 | mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c)) 461 | return 462 | } 463 | if n < mw.TimeFunc().Unix() { 464 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c)) 465 | return 466 | } 467 | default: 468 | mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c)) 469 | return 470 | } 471 | 472 | c.Set("JWT_PAYLOAD", claims) 473 | identity := mw.IdentityHandler(c) 474 | 475 | if identity != nil { 476 | c.Set(mw.IdentityKey, identity) 477 | } 478 | 479 | if !mw.Authorizator(identity, c) { 480 | mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c)) 481 | return 482 | } 483 | 484 | c.Next() 485 | } 486 | 487 | // GetClaimsFromJWT get claims from JWT token 488 | func (mw *GinJWTMiddleware) GetClaimsFromJWT(c *gin.Context) (MapClaims, error) { 489 | token, err := mw.ParseToken(c) 490 | if err != nil { 491 | return nil, err 492 | } 493 | 494 | if mw.SendAuthorization { 495 | if v, ok := c.Get("JWT_TOKEN"); ok { 496 | c.Header("Authorization", mw.TokenHeadName+" "+v.(string)) 497 | } 498 | } 499 | 500 | claims := MapClaims{} 501 | for key, value := range token.Claims.(jwt.MapClaims) { 502 | claims[key] = value 503 | } 504 | 505 | return claims, nil 506 | } 507 | 508 | // LoginHandler can be used by clients to get a jwt token. 509 | // Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}. 510 | // Reply will be of the form {"token": "TOKEN"}. 511 | func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) { 512 | if mw.Authenticator == nil { 513 | mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c)) 514 | return 515 | } 516 | 517 | data, err := mw.Authenticator(c) 518 | if err != nil { 519 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c)) 520 | return 521 | } 522 | 523 | // Create the token 524 | token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm)) 525 | claims := token.Claims.(jwt.MapClaims) 526 | 527 | if mw.PayloadFunc != nil { 528 | for key, value := range mw.PayloadFunc(data) { 529 | claims[key] = value 530 | } 531 | } 532 | 533 | expire := mw.TimeFunc().Add(mw.TimeoutFunc(claims)) 534 | claims[mw.ExpField] = expire.Unix() 535 | claims["orig_iat"] = mw.TimeFunc().Unix() 536 | tokenString, err := mw.signedString(token) 537 | if err != nil { 538 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c)) 539 | return 540 | } 541 | 542 | mw.SetCookie(c, tokenString) 543 | 544 | mw.LoginResponse(c, http.StatusOK, tokenString, expire) 545 | } 546 | 547 | // LogoutHandler can be used by clients to remove the jwt cookie (if set) 548 | func (mw *GinJWTMiddleware) LogoutHandler(c *gin.Context) { 549 | // delete auth cookie 550 | if mw.SendCookie { 551 | if mw.CookieSameSite != 0 { 552 | c.SetSameSite(mw.CookieSameSite) 553 | } 554 | 555 | c.SetCookie( 556 | mw.CookieName, 557 | "", 558 | -1, 559 | "/", 560 | mw.CookieDomain, 561 | mw.SecureCookie, 562 | mw.CookieHTTPOnly, 563 | ) 564 | } 565 | 566 | mw.LogoutResponse(c, http.StatusOK) 567 | } 568 | 569 | func (mw *GinJWTMiddleware) signedString(token *jwt.Token) (string, error) { 570 | var tokenString string 571 | var err error 572 | if mw.usingPublicKeyAlgo() { 573 | tokenString, err = token.SignedString(mw.privKey) 574 | } else { 575 | tokenString, err = token.SignedString(mw.Key) 576 | } 577 | return tokenString, err 578 | } 579 | 580 | // RefreshHandler can be used to refresh a token. The token still needs to be valid on refresh. 581 | // Shall be put under an endpoint that is using the GinJWTMiddleware. 582 | // Reply will be of the form {"token": "TOKEN"}. 583 | func (mw *GinJWTMiddleware) RefreshHandler(c *gin.Context) { 584 | tokenString, expire, err := mw.RefreshToken(c) 585 | if err != nil { 586 | mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c)) 587 | return 588 | } 589 | 590 | mw.RefreshResponse(c, http.StatusOK, tokenString, expire) 591 | } 592 | 593 | // RefreshToken refresh token and check if token is expired 594 | func (mw *GinJWTMiddleware) RefreshToken(c *gin.Context) (string, time.Time, error) { 595 | claims, err := mw.CheckIfTokenExpire(c) 596 | if err != nil { 597 | return "", time.Now(), err 598 | } 599 | 600 | // Create the token 601 | newToken := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm)) 602 | newClaims := newToken.Claims.(jwt.MapClaims) 603 | 604 | for key := range claims { 605 | newClaims[key] = claims[key] 606 | } 607 | 608 | expire := mw.TimeFunc().Add(mw.TimeoutFunc(claims)) 609 | newClaims[mw.ExpField] = expire.Unix() 610 | newClaims["orig_iat"] = mw.TimeFunc().Unix() 611 | tokenString, err := mw.signedString(newToken) 612 | if err != nil { 613 | return "", time.Now(), err 614 | } 615 | 616 | mw.SetCookie(c, tokenString) 617 | 618 | return tokenString, expire, nil 619 | } 620 | 621 | // CheckIfTokenExpire check if token expire 622 | func (mw *GinJWTMiddleware) CheckIfTokenExpire(c *gin.Context) (jwt.MapClaims, error) { 623 | token, err := mw.ParseToken(c) 624 | if err != nil { 625 | // If we receive an error, and the error is anything other than a single 626 | // ValidationErrorExpired, we want to return the error. 627 | // If the error is just ValidationErrorExpired, we want to continue, as we can still 628 | // refresh the token if it's within the MaxRefresh time. 629 | // (see https://github.com/appleboy/gin-jwt/issues/176) 630 | validationErr, ok := err.(*jwt.ValidationError) 631 | if !ok || validationErr.Errors != jwt.ValidationErrorExpired { 632 | return nil, err 633 | } 634 | } 635 | 636 | claims := token.Claims.(jwt.MapClaims) 637 | 638 | origIat := int64(claims["orig_iat"].(float64)) 639 | 640 | if origIat < mw.TimeFunc().Add(-mw.MaxRefresh).Unix() { 641 | return nil, ErrExpiredToken 642 | } 643 | 644 | return claims, nil 645 | } 646 | 647 | // TokenGenerator method that clients can use to get a jwt token. 648 | func (mw *GinJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time, error) { 649 | token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm)) 650 | claims := token.Claims.(jwt.MapClaims) 651 | 652 | if mw.PayloadFunc != nil { 653 | for key, value := range mw.PayloadFunc(data) { 654 | claims[key] = value 655 | } 656 | } 657 | 658 | expire := mw.TimeFunc().Add(mw.TimeoutFunc(claims)) 659 | claims[mw.ExpField] = expire.Unix() 660 | claims["orig_iat"] = mw.TimeFunc().Unix() 661 | tokenString, err := mw.signedString(token) 662 | if err != nil { 663 | return "", time.Time{}, err 664 | } 665 | 666 | return tokenString, expire, nil 667 | } 668 | 669 | func (mw *GinJWTMiddleware) jwtFromHeader(c *gin.Context, key string) (string, error) { 670 | authHeader := c.Request.Header.Get(key) 671 | 672 | if authHeader == "" { 673 | return "", ErrEmptyAuthHeader 674 | } 675 | 676 | parts := strings.SplitN(authHeader, " ", 2) 677 | if len(parts) != 2 || parts[0] != mw.TokenHeadName { 678 | return "", ErrInvalidAuthHeader 679 | } 680 | 681 | return parts[1], nil 682 | } 683 | 684 | func (mw *GinJWTMiddleware) jwtFromQuery(c *gin.Context, key string) (string, error) { 685 | token := c.Query(key) 686 | 687 | if token == "" { 688 | return "", ErrEmptyQueryToken 689 | } 690 | 691 | return token, nil 692 | } 693 | 694 | func (mw *GinJWTMiddleware) jwtFromCookie(c *gin.Context, key string) (string, error) { 695 | cookie, _ := c.Cookie(key) 696 | 697 | if cookie == "" { 698 | return "", ErrEmptyCookieToken 699 | } 700 | 701 | return cookie, nil 702 | } 703 | 704 | func (mw *GinJWTMiddleware) jwtFromParam(c *gin.Context, key string) (string, error) { 705 | token := c.Param(key) 706 | 707 | if token == "" { 708 | return "", ErrEmptyParamToken 709 | } 710 | 711 | return token, nil 712 | } 713 | 714 | func (mw *GinJWTMiddleware) jwtFromForm(c *gin.Context, key string) (string, error) { 715 | token := c.PostForm(key) 716 | 717 | if token == "" { 718 | return "", ErrEmptyParamToken 719 | } 720 | 721 | return token, nil 722 | } 723 | 724 | // ParseToken parse jwt token from gin context 725 | func (mw *GinJWTMiddleware) ParseToken(c *gin.Context) (*jwt.Token, error) { 726 | var token string 727 | var err error 728 | 729 | methods := strings.Split(mw.TokenLookup, ",") 730 | for _, method := range methods { 731 | if len(token) > 0 { 732 | break 733 | } 734 | parts := strings.Split(strings.TrimSpace(method), ":") 735 | k := strings.TrimSpace(parts[0]) 736 | v := strings.TrimSpace(parts[1]) 737 | switch k { 738 | case "header": 739 | token, err = mw.jwtFromHeader(c, v) 740 | case "query": 741 | token, err = mw.jwtFromQuery(c, v) 742 | case "cookie": 743 | token, err = mw.jwtFromCookie(c, v) 744 | case "param": 745 | token, err = mw.jwtFromParam(c, v) 746 | case "form": 747 | token, err = mw.jwtFromForm(c, v) 748 | } 749 | } 750 | 751 | if err != nil { 752 | return nil, err 753 | } 754 | 755 | if mw.KeyFunc != nil { 756 | return jwt.Parse(token, mw.KeyFunc, mw.ParseOptions...) 757 | } 758 | 759 | return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { 760 | if jwt.GetSigningMethod(mw.SigningAlgorithm) != t.Method { 761 | return nil, ErrInvalidSigningAlgorithm 762 | } 763 | if mw.usingPublicKeyAlgo() { 764 | return mw.pubKey, nil 765 | } 766 | 767 | // save token string if valid 768 | c.Set("JWT_TOKEN", token) 769 | 770 | return mw.Key, nil 771 | }, mw.ParseOptions...) 772 | } 773 | 774 | // ParseTokenString parse jwt token string 775 | func (mw *GinJWTMiddleware) ParseTokenString(token string) (*jwt.Token, error) { 776 | if mw.KeyFunc != nil { 777 | return jwt.Parse(token, mw.KeyFunc, mw.ParseOptions...) 778 | } 779 | 780 | return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { 781 | if jwt.GetSigningMethod(mw.SigningAlgorithm) != t.Method { 782 | return nil, ErrInvalidSigningAlgorithm 783 | } 784 | if mw.usingPublicKeyAlgo() { 785 | return mw.pubKey, nil 786 | } 787 | 788 | return mw.Key, nil 789 | }, mw.ParseOptions...) 790 | } 791 | 792 | func (mw *GinJWTMiddleware) unauthorized(c *gin.Context, code int, message string) { 793 | c.Header("WWW-Authenticate", "JWT realm="+mw.Realm) 794 | if !mw.DisabledAbort { 795 | c.Abort() 796 | } 797 | 798 | mw.Unauthorized(c, code, message) 799 | } 800 | 801 | // ExtractClaims help to extract the JWT claims 802 | func ExtractClaims(c *gin.Context) MapClaims { 803 | claims, exists := c.Get("JWT_PAYLOAD") 804 | if !exists { 805 | return make(MapClaims) 806 | } 807 | 808 | return claims.(MapClaims) 809 | } 810 | 811 | // ExtractClaimsFromToken help to extract the JWT claims from token 812 | func ExtractClaimsFromToken(token *jwt.Token) MapClaims { 813 | if token == nil { 814 | return make(MapClaims) 815 | } 816 | 817 | claims := MapClaims{} 818 | for key, value := range token.Claims.(jwt.MapClaims) { 819 | claims[key] = value 820 | } 821 | 822 | return claims 823 | } 824 | 825 | // GetToken help to get the JWT token string 826 | func GetToken(c *gin.Context) string { 827 | token, exists := c.Get("JWT_TOKEN") 828 | if !exists { 829 | return "" 830 | } 831 | 832 | return token.(string) 833 | } 834 | 835 | // SetCookie help to set the token in the cookie 836 | func (mw *GinJWTMiddleware) SetCookie(c *gin.Context, token string) { 837 | // set cookie 838 | if mw.SendCookie { 839 | expireCookie := mw.TimeFunc().Add(mw.CookieMaxAge) 840 | maxage := int(expireCookie.Unix() - mw.TimeFunc().Unix()) 841 | 842 | if mw.CookieSameSite != 0 { 843 | c.SetSameSite(mw.CookieSameSite) 844 | } 845 | 846 | c.SetCookie( 847 | mw.CookieName, 848 | token, 849 | maxage, 850 | "/", 851 | mw.CookieDomain, 852 | mw.SecureCookie, 853 | mw.CookieHTTPOnly, 854 | ) 855 | } 856 | } 857 | -------------------------------------------------------------------------------- /auth_jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/appleboy/gofight/v2" 16 | "github.com/gin-gonic/gin" 17 | "github.com/golang-jwt/jwt/v4" 18 | "github.com/stretchr/testify/assert" 19 | "github.com/tidwall/gjson" 20 | ) 21 | 22 | // Login form structure. 23 | type Login struct { 24 | Username string `form:"username" json:"username" binding:"required"` 25 | Password string `form:"password" json:"password" binding:"required"` 26 | } 27 | 28 | var ( 29 | key = []byte("secret key") 30 | defaultAuthenticator = func(c *gin.Context) (interface{}, error) { 31 | var loginVals Login 32 | userID := loginVals.Username 33 | password := loginVals.Password 34 | 35 | if userID == "admin" && password == "admin" { 36 | return userID, nil 37 | } 38 | 39 | return userID, ErrFailedAuthentication 40 | } 41 | ) 42 | 43 | func makeTokenString(SigningAlgorithm string, username string) string { 44 | if SigningAlgorithm == "" { 45 | SigningAlgorithm = "HS256" 46 | } 47 | 48 | token := jwt.New(jwt.GetSigningMethod(SigningAlgorithm)) 49 | claims := token.Claims.(jwt.MapClaims) 50 | claims["identity"] = username 51 | claims["exp"] = time.Now().Add(time.Hour).Unix() 52 | claims["orig_iat"] = time.Now().Unix() 53 | var tokenString string 54 | if SigningAlgorithm == "RS256" { 55 | keyData, _ := os.ReadFile("testdata/jwtRS256.key") 56 | signKey, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) 57 | tokenString, _ = token.SignedString(signKey) 58 | } else { 59 | tokenString, _ = token.SignedString(key) 60 | } 61 | 62 | return tokenString 63 | } 64 | 65 | func keyFunc(token *jwt.Token) (interface{}, error) { 66 | cert, err := os.ReadFile("testdata/jwtRS256.key.pub") 67 | if err != nil { 68 | return nil, err 69 | } 70 | return jwt.ParseRSAPublicKeyFromPEM(cert) 71 | } 72 | 73 | func TestMissingKey(t *testing.T) { 74 | _, err := New(&GinJWTMiddleware{ 75 | Realm: "test zone", 76 | Timeout: time.Hour, 77 | MaxRefresh: time.Hour * 24, 78 | Authenticator: defaultAuthenticator, 79 | }) 80 | 81 | assert.Error(t, err) 82 | assert.Equal(t, ErrMissingSecretKey, err) 83 | } 84 | 85 | func TestMissingPrivKey(t *testing.T) { 86 | _, err := New(&GinJWTMiddleware{ 87 | Realm: "zone", 88 | SigningAlgorithm: "RS256", 89 | PrivKeyFile: "nonexisting", 90 | }) 91 | 92 | assert.Error(t, err) 93 | assert.Equal(t, ErrNoPrivKeyFile, err) 94 | } 95 | 96 | func TestMissingPubKey(t *testing.T) { 97 | _, err := New(&GinJWTMiddleware{ 98 | Realm: "zone", 99 | SigningAlgorithm: "RS256", 100 | PrivKeyFile: "testdata/jwtRS256.key", 101 | PubKeyFile: "nonexisting", 102 | }) 103 | 104 | assert.Error(t, err) 105 | assert.Equal(t, ErrNoPubKeyFile, err) 106 | } 107 | 108 | func TestInvalidPrivKey(t *testing.T) { 109 | _, err := New(&GinJWTMiddleware{ 110 | Realm: "zone", 111 | SigningAlgorithm: "RS256", 112 | PrivKeyFile: "testdata/invalidprivkey.key", 113 | PubKeyFile: "testdata/jwtRS256.key.pub", 114 | }) 115 | 116 | assert.Error(t, err) 117 | assert.Equal(t, ErrInvalidPrivKey, err) 118 | } 119 | 120 | func TestInvalidPrivKeyBytes(t *testing.T) { 121 | _, err := New(&GinJWTMiddleware{ 122 | Realm: "zone", 123 | SigningAlgorithm: "RS256", 124 | PrivKeyBytes: []byte("Invalid_Private_Key"), 125 | PubKeyFile: "testdata/jwtRS256.key.pub", 126 | }) 127 | 128 | assert.Error(t, err) 129 | assert.Equal(t, ErrInvalidPrivKey, err) 130 | } 131 | 132 | func TestInvalidPubKey(t *testing.T) { 133 | _, err := New(&GinJWTMiddleware{ 134 | Realm: "zone", 135 | SigningAlgorithm: "RS256", 136 | PrivKeyFile: "testdata/jwtRS256.key", 137 | PubKeyFile: "testdata/invalidpubkey.key", 138 | }) 139 | 140 | assert.Error(t, err) 141 | assert.Equal(t, ErrInvalidPubKey, err) 142 | } 143 | 144 | func TestInvalidPubKeyBytes(t *testing.T) { 145 | _, err := New(&GinJWTMiddleware{ 146 | Realm: "zone", 147 | SigningAlgorithm: "RS256", 148 | PrivKeyFile: "testdata/jwtRS256.key", 149 | PubKeyBytes: []byte("Invalid_Private_Key"), 150 | }) 151 | 152 | assert.Error(t, err) 153 | assert.Equal(t, ErrInvalidPubKey, err) 154 | } 155 | 156 | func TestMissingTimeOut(t *testing.T) { 157 | authMiddleware, err := New(&GinJWTMiddleware{ 158 | Realm: "test zone", 159 | Key: key, 160 | Authenticator: defaultAuthenticator, 161 | }) 162 | 163 | assert.NoError(t, err) 164 | assert.Equal(t, time.Hour, authMiddleware.Timeout) 165 | } 166 | 167 | func TestMissingTokenLookup(t *testing.T) { 168 | authMiddleware, err := New(&GinJWTMiddleware{ 169 | Realm: "test zone", 170 | Key: key, 171 | Authenticator: defaultAuthenticator, 172 | }) 173 | 174 | assert.NoError(t, err) 175 | assert.Equal(t, "header:Authorization", authMiddleware.TokenLookup) 176 | } 177 | 178 | func helloHandler(c *gin.Context) { 179 | c.JSON(200, gin.H{ 180 | "text": "Hello World.", 181 | "token": GetToken(c), 182 | }) 183 | } 184 | 185 | func ginHandler(auth *GinJWTMiddleware) *gin.Engine { 186 | gin.SetMode(gin.TestMode) 187 | r := gin.New() 188 | 189 | r.POST("/login", auth.LoginHandler) 190 | r.POST("/logout", auth.LogoutHandler) 191 | // test token in path 192 | r.GET("/g/:token/refresh_token", auth.RefreshHandler) 193 | 194 | group := r.Group("/auth") 195 | // Refresh time can be longer than token timeout 196 | group.GET("/refresh_token", auth.RefreshHandler) 197 | group.Use(auth.MiddlewareFunc()) 198 | { 199 | group.GET("/hello", helloHandler) 200 | } 201 | 202 | return r 203 | } 204 | 205 | func TestMissingAuthenticatorForLoginHandler(t *testing.T) { 206 | authMiddleware, err := New(&GinJWTMiddleware{ 207 | Realm: "test zone", 208 | Key: key, 209 | Timeout: time.Hour, 210 | MaxRefresh: time.Hour * 24, 211 | }) 212 | 213 | assert.NoError(t, err) 214 | 215 | handler := ginHandler(authMiddleware) 216 | r := gofight.New() 217 | 218 | r.POST("/login"). 219 | SetJSON(gofight.D{ 220 | "username": "admin", 221 | "password": "admin", 222 | }). 223 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 224 | message := gjson.Get(r.Body.String(), "message") 225 | 226 | assert.Equal(t, ErrMissingAuthenticatorFunc.Error(), message.String()) 227 | assert.Equal(t, http.StatusInternalServerError, r.Code) 228 | }) 229 | } 230 | 231 | func TestLoginHandler(t *testing.T) { 232 | // the middleware to test 233 | cookieName := "jwt" 234 | cookieDomain := "example.com" 235 | authMiddleware, err := New(&GinJWTMiddleware{ 236 | Realm: "test zone", 237 | Key: key, 238 | PayloadFunc: func(data interface{}) MapClaims { 239 | // Set custom claim, to be checked in Authorizator method 240 | return MapClaims{"testkey": "testval", "exp": 0} 241 | }, 242 | Authenticator: func(c *gin.Context) (interface{}, error) { 243 | var loginVals Login 244 | if binderr := c.ShouldBind(&loginVals); binderr != nil { 245 | return "", ErrMissingLoginValues 246 | } 247 | userID := loginVals.Username 248 | password := loginVals.Password 249 | if userID == "admin" && password == "admin" { 250 | return userID, nil 251 | } 252 | return "", ErrFailedAuthentication 253 | }, 254 | Authorizator: func(user interface{}, c *gin.Context) bool { 255 | return true 256 | }, 257 | LoginResponse: func(c *gin.Context, code int, token string, t time.Time) { 258 | cookie, err := c.Cookie("jwt") 259 | if err != nil { 260 | log.Println(err) 261 | } 262 | 263 | c.JSON(http.StatusOK, gin.H{ 264 | "code": http.StatusOK, 265 | "token": token, 266 | "expire": t.Format(time.RFC3339), 267 | "message": "login successfully", 268 | "cookie": cookie, 269 | }) 270 | }, 271 | SendCookie: true, 272 | CookieName: cookieName, 273 | CookieDomain: cookieDomain, 274 | TimeFunc: func() time.Time { return time.Now().Add(time.Duration(5) * time.Minute) }, 275 | }) 276 | 277 | assert.NoError(t, err) 278 | 279 | handler := ginHandler(authMiddleware) 280 | 281 | r := gofight.New() 282 | 283 | r.POST("/login"). 284 | SetJSON(gofight.D{ 285 | "username": "admin", 286 | }). 287 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 288 | message := gjson.Get(r.Body.String(), "message") 289 | 290 | assert.Equal(t, ErrMissingLoginValues.Error(), message.String()) 291 | assert.Equal(t, http.StatusUnauthorized, r.Code) 292 | //nolint:staticcheck 293 | assert.Equal(t, "application/json; charset=utf-8", r.HeaderMap.Get("Content-Type")) 294 | }) 295 | 296 | r.POST("/login"). 297 | SetJSON(gofight.D{ 298 | "username": "admin", 299 | "password": "test", 300 | }). 301 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 302 | message := gjson.Get(r.Body.String(), "message") 303 | assert.Equal(t, ErrFailedAuthentication.Error(), message.String()) 304 | assert.Equal(t, http.StatusUnauthorized, r.Code) 305 | }) 306 | 307 | r.POST("/login"). 308 | SetJSON(gofight.D{ 309 | "username": "admin", 310 | "password": "admin", 311 | }). 312 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 313 | message := gjson.Get(r.Body.String(), "message") 314 | assert.Equal(t, "login successfully", message.String()) 315 | assert.Equal(t, http.StatusOK, r.Code) 316 | //nolint:staticcheck 317 | assert.True(t, strings.HasPrefix(r.HeaderMap.Get("Set-Cookie"), "jwt=")) 318 | //nolint:staticcheck 319 | assert.True(t, strings.HasSuffix(r.HeaderMap.Get("Set-Cookie"), "; Path=/; Domain=example.com; Max-Age=3600")) 320 | }) 321 | } 322 | 323 | func TestParseToken(t *testing.T) { 324 | // the middleware to test 325 | authMiddleware, _ := New(&GinJWTMiddleware{ 326 | Realm: "test zone", 327 | Key: key, 328 | Timeout: time.Hour, 329 | MaxRefresh: time.Hour * 24, 330 | Authenticator: defaultAuthenticator, 331 | }) 332 | 333 | handler := ginHandler(authMiddleware) 334 | 335 | r := gofight.New() 336 | 337 | r.GET("/auth/hello"). 338 | SetHeader(gofight.H{ 339 | "Authorization": "", 340 | }). 341 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 342 | assert.Equal(t, http.StatusUnauthorized, r.Code) 343 | }) 344 | 345 | r.GET("/auth/hello"). 346 | SetHeader(gofight.H{ 347 | "Authorization": "Test 1234", 348 | }). 349 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 350 | assert.Equal(t, http.StatusUnauthorized, r.Code) 351 | }) 352 | 353 | r.GET("/auth/hello"). 354 | SetHeader(gofight.H{ 355 | "Authorization": "Bearer " + makeTokenString("HS384", "admin"), 356 | }). 357 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 358 | assert.Equal(t, http.StatusUnauthorized, r.Code) 359 | }) 360 | 361 | r.GET("/auth/hello"). 362 | SetHeader(gofight.H{ 363 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 364 | }). 365 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 366 | assert.Equal(t, http.StatusOK, r.Code) 367 | }) 368 | } 369 | 370 | func TestParseTokenRS256(t *testing.T) { 371 | // the middleware to test 372 | authMiddleware, _ := New(&GinJWTMiddleware{ 373 | Realm: "test zone", 374 | Key: key, 375 | Timeout: time.Hour, 376 | MaxRefresh: time.Hour * 24, 377 | SigningAlgorithm: "RS256", 378 | PrivKeyFile: "testdata/jwtRS256.key", 379 | PubKeyFile: "testdata/jwtRS256.key.pub", 380 | Authenticator: defaultAuthenticator, 381 | }) 382 | 383 | handler := ginHandler(authMiddleware) 384 | 385 | r := gofight.New() 386 | 387 | r.GET("/auth/hello"). 388 | SetHeader(gofight.H{ 389 | "Authorization": "", 390 | }). 391 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 392 | assert.Equal(t, http.StatusUnauthorized, r.Code) 393 | }) 394 | 395 | r.GET("/auth/hello"). 396 | SetHeader(gofight.H{ 397 | "Authorization": "Test 1234", 398 | }). 399 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 400 | assert.Equal(t, http.StatusUnauthorized, r.Code) 401 | }) 402 | 403 | r.GET("/auth/hello"). 404 | SetHeader(gofight.H{ 405 | "Authorization": "Bearer " + makeTokenString("HS384", "admin"), 406 | }). 407 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 408 | assert.Equal(t, http.StatusUnauthorized, r.Code) 409 | }) 410 | 411 | r.GET("/auth/hello"). 412 | SetHeader(gofight.H{ 413 | "Authorization": "Bearer " + makeTokenString("RS256", "admin"), 414 | }). 415 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 416 | assert.Equal(t, http.StatusOK, r.Code) 417 | }) 418 | } 419 | 420 | func TestParseTokenKeyFunc(t *testing.T) { 421 | // the middleware to test 422 | authMiddleware, _ := New(&GinJWTMiddleware{ 423 | Realm: "test zone", 424 | KeyFunc: keyFunc, 425 | Timeout: time.Hour, 426 | MaxRefresh: time.Hour * 24, 427 | Authenticator: defaultAuthenticator, 428 | // make sure it skips these settings 429 | Key: []byte(""), 430 | SigningAlgorithm: "RS256", 431 | PrivKeyFile: "", 432 | PubKeyFile: "", 433 | }) 434 | 435 | handler := ginHandler(authMiddleware) 436 | 437 | r := gofight.New() 438 | 439 | r.GET("/auth/hello"). 440 | SetHeader(gofight.H{ 441 | "Authorization": "", 442 | }). 443 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 444 | assert.Equal(t, http.StatusUnauthorized, r.Code) 445 | }) 446 | 447 | r.GET("/auth/hello"). 448 | SetHeader(gofight.H{ 449 | "Authorization": "Test 1234", 450 | }). 451 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 452 | assert.Equal(t, http.StatusUnauthorized, r.Code) 453 | }) 454 | 455 | r.GET("/auth/hello"). 456 | SetHeader(gofight.H{ 457 | "Authorization": "Bearer " + makeTokenString("HS384", "admin"), 458 | }). 459 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 460 | assert.Equal(t, http.StatusUnauthorized, r.Code) 461 | }) 462 | 463 | r.GET("/auth/hello"). 464 | SetHeader(gofight.H{ 465 | "Authorization": "Bearer " + makeTokenString("RS256", "admin"), 466 | }). 467 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 468 | assert.Equal(t, http.StatusOK, r.Code) 469 | }) 470 | } 471 | 472 | func TestRefreshHandlerRS256(t *testing.T) { 473 | // the middleware to test 474 | authMiddleware, _ := New(&GinJWTMiddleware{ 475 | Realm: "test zone", 476 | Key: key, 477 | Timeout: time.Hour, 478 | MaxRefresh: time.Hour * 24, 479 | SigningAlgorithm: "RS256", 480 | PrivKeyFile: "testdata/jwtRS256.key", 481 | PubKeyFile: "testdata/jwtRS256.key.pub", 482 | SendCookie: true, 483 | CookieName: "jwt", 484 | Authenticator: defaultAuthenticator, 485 | RefreshResponse: func(c *gin.Context, code int, token string, t time.Time) { 486 | cookie, err := c.Cookie("jwt") 487 | if err != nil { 488 | log.Println(err) 489 | } 490 | 491 | c.JSON(http.StatusOK, gin.H{ 492 | "code": http.StatusOK, 493 | "token": token, 494 | "expire": t.Format(time.RFC3339), 495 | "message": "refresh successfully", 496 | "cookie": cookie, 497 | }) 498 | }, 499 | }) 500 | 501 | handler := ginHandler(authMiddleware) 502 | 503 | r := gofight.New() 504 | 505 | r.GET("/auth/refresh_token"). 506 | SetHeader(gofight.H{ 507 | "Authorization": "", 508 | }). 509 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 510 | assert.Equal(t, http.StatusUnauthorized, r.Code) 511 | }) 512 | 513 | r.GET("/auth/refresh_token"). 514 | SetHeader(gofight.H{ 515 | "Authorization": "Test 1234", 516 | }). 517 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 518 | assert.Equal(t, http.StatusUnauthorized, r.Code) 519 | }) 520 | r.GET("/auth/refresh_token"). 521 | SetHeader(gofight.H{ 522 | "Authorization": "Bearer " + makeTokenString("RS256", "admin"), 523 | }). 524 | SetCookie(gofight.H{ 525 | "jwt": makeTokenString("RS256", "admin"), 526 | }). 527 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 528 | message := gjson.Get(r.Body.String(), "message") 529 | cookie := gjson.Get(r.Body.String(), "cookie") 530 | assert.Equal(t, "refresh successfully", message.String()) 531 | assert.Equal(t, http.StatusOK, r.Code) 532 | assert.Equal(t, makeTokenString("RS256", "admin"), cookie.String()) 533 | }) 534 | } 535 | 536 | func TestRefreshHandler(t *testing.T) { 537 | // the middleware to test 538 | authMiddleware, _ := New(&GinJWTMiddleware{ 539 | Realm: "test zone", 540 | Key: key, 541 | Timeout: time.Hour, 542 | MaxRefresh: time.Hour * 24, 543 | Authenticator: defaultAuthenticator, 544 | }) 545 | 546 | handler := ginHandler(authMiddleware) 547 | 548 | r := gofight.New() 549 | 550 | r.GET("/auth/refresh_token"). 551 | SetHeader(gofight.H{ 552 | "Authorization": "", 553 | }). 554 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 555 | assert.Equal(t, http.StatusUnauthorized, r.Code) 556 | }) 557 | 558 | r.GET("/auth/refresh_token"). 559 | SetHeader(gofight.H{ 560 | "Authorization": "Test 1234", 561 | }). 562 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 563 | assert.Equal(t, http.StatusUnauthorized, r.Code) 564 | }) 565 | 566 | r.GET("/auth/refresh_token"). 567 | SetHeader(gofight.H{ 568 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 569 | }). 570 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 571 | assert.Equal(t, http.StatusOK, r.Code) 572 | }) 573 | } 574 | 575 | func TestExpiredTokenWithinMaxRefreshOnRefreshHandler(t *testing.T) { 576 | // the middleware to test 577 | authMiddleware, _ := New(&GinJWTMiddleware{ 578 | Realm: "test zone", 579 | Key: key, 580 | Timeout: time.Hour, 581 | MaxRefresh: 2 * time.Hour, 582 | Authenticator: defaultAuthenticator, 583 | }) 584 | 585 | handler := ginHandler(authMiddleware) 586 | 587 | r := gofight.New() 588 | 589 | token := jwt.New(jwt.GetSigningMethod("HS256")) 590 | claims := token.Claims.(jwt.MapClaims) 591 | claims["identity"] = "admin" 592 | claims["exp"] = time.Now().Add(-time.Minute).Unix() 593 | claims["orig_iat"] = time.Now().Add(-time.Hour).Unix() 594 | tokenString, _ := token.SignedString(key) 595 | 596 | // We should be able to refresh a token that has expired but is within the MaxRefresh time 597 | r.GET("/auth/refresh_token"). 598 | SetHeader(gofight.H{ 599 | "Authorization": "Bearer " + tokenString, 600 | }). 601 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 602 | assert.Equal(t, http.StatusOK, r.Code) 603 | }) 604 | } 605 | 606 | func TestExpiredTokenOnRefreshHandler(t *testing.T) { 607 | // the middleware to test 608 | authMiddleware, _ := New(&GinJWTMiddleware{ 609 | Realm: "test zone", 610 | Key: key, 611 | Timeout: time.Hour, 612 | Authenticator: defaultAuthenticator, 613 | }) 614 | 615 | handler := ginHandler(authMiddleware) 616 | 617 | r := gofight.New() 618 | 619 | token := jwt.New(jwt.GetSigningMethod("HS256")) 620 | claims := token.Claims.(jwt.MapClaims) 621 | claims["identity"] = "admin" 622 | claims["exp"] = time.Now().Add(time.Hour).Unix() 623 | claims["orig_iat"] = 0 624 | tokenString, _ := token.SignedString(key) 625 | 626 | r.GET("/auth/refresh_token"). 627 | SetHeader(gofight.H{ 628 | "Authorization": "Bearer " + tokenString, 629 | }). 630 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 631 | assert.Equal(t, http.StatusUnauthorized, r.Code) 632 | }) 633 | } 634 | 635 | func TestAuthorizator(t *testing.T) { 636 | // the middleware to test 637 | authMiddleware, _ := New(&GinJWTMiddleware{ 638 | Realm: "test zone", 639 | Key: key, 640 | Timeout: time.Hour, 641 | MaxRefresh: time.Hour * 24, 642 | Authenticator: defaultAuthenticator, 643 | Authorizator: func(data interface{}, c *gin.Context) bool { 644 | return data.(string) == "admin" 645 | }, 646 | }) 647 | 648 | handler := ginHandler(authMiddleware) 649 | 650 | r := gofight.New() 651 | 652 | r.GET("/auth/hello"). 653 | SetHeader(gofight.H{ 654 | "Authorization": "Bearer " + makeTokenString("HS256", "test"), 655 | }). 656 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 657 | assert.Equal(t, http.StatusForbidden, r.Code) 658 | }) 659 | 660 | r.GET("/auth/hello"). 661 | SetHeader(gofight.H{ 662 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 663 | }). 664 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 665 | assert.Equal(t, http.StatusOK, r.Code) 666 | }) 667 | } 668 | 669 | func TestParseTokenWithJsonNumber(t *testing.T) { 670 | authMiddleware, _ := New(&GinJWTMiddleware{ 671 | Realm: "test zone", 672 | Key: key, 673 | Timeout: time.Hour, 674 | MaxRefresh: time.Hour * 24, 675 | Authenticator: defaultAuthenticator, 676 | Unauthorized: func(c *gin.Context, code int, message string) { 677 | c.String(code, message) 678 | }, 679 | ParseOptions: []jwt.ParserOption{jwt.WithJSONNumber()}, 680 | }) 681 | 682 | handler := ginHandler(authMiddleware) 683 | 684 | r := gofight.New() 685 | 686 | r.GET("/auth/hello"). 687 | SetHeader(gofight.H{ 688 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 689 | }). 690 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 691 | assert.Equal(t, http.StatusOK, r.Code) 692 | }) 693 | } 694 | 695 | func TestClaimsDuringAuthorization(t *testing.T) { 696 | // the middleware to test 697 | authMiddleware, _ := New(&GinJWTMiddleware{ 698 | Realm: "test zone", 699 | Key: key, 700 | Timeout: time.Hour, 701 | MaxRefresh: time.Hour * 24, 702 | PayloadFunc: func(data interface{}) MapClaims { 703 | if v, ok := data.(MapClaims); ok { 704 | return v 705 | } 706 | 707 | if reflect.TypeOf(data).String() != "string" { 708 | return MapClaims{} 709 | } 710 | 711 | var testkey string 712 | switch data.(string) { 713 | case "admin": 714 | testkey = "1234" 715 | case "test": 716 | testkey = "5678" 717 | case "Guest": 718 | testkey = "" 719 | } 720 | // Set custom claim, to be checked in Authorizator method 721 | return MapClaims{"identity": data.(string), "testkey": testkey, "exp": 0} 722 | }, 723 | Authenticator: func(c *gin.Context) (interface{}, error) { 724 | var loginVals Login 725 | 726 | if err := c.BindJSON(&loginVals); err != nil { 727 | return "", ErrMissingLoginValues 728 | } 729 | 730 | userID := loginVals.Username 731 | password := loginVals.Password 732 | 733 | if userID == "admin" && password == "admin" { 734 | return userID, nil 735 | } 736 | 737 | if userID == "test" && password == "test" { 738 | return userID, nil 739 | } 740 | 741 | return "Guest", ErrFailedAuthentication 742 | }, 743 | Authorizator: func(user interface{}, c *gin.Context) bool { 744 | jwtClaims := ExtractClaims(c) 745 | 746 | if jwtClaims["identity"] == "administrator" { 747 | return true 748 | } 749 | 750 | if jwtClaims["testkey"] == "1234" && jwtClaims["identity"] == "admin" { 751 | return true 752 | } 753 | 754 | if jwtClaims["testkey"] == "5678" && jwtClaims["identity"] == "test" { 755 | return true 756 | } 757 | 758 | return false 759 | }, 760 | }) 761 | 762 | r := gofight.New() 763 | handler := ginHandler(authMiddleware) 764 | 765 | userToken, _, _ := authMiddleware.TokenGenerator(MapClaims{ 766 | "identity": "administrator", 767 | }) 768 | 769 | r.GET("/auth/hello"). 770 | SetHeader(gofight.H{ 771 | "Authorization": "Bearer " + userToken, 772 | }). 773 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 774 | assert.Equal(t, http.StatusOK, r.Code) 775 | }) 776 | 777 | r.POST("/login"). 778 | SetJSON(gofight.D{ 779 | "username": "admin", 780 | "password": "admin", 781 | }). 782 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 783 | token := gjson.Get(r.Body.String(), "token") 784 | userToken = token.String() 785 | assert.Equal(t, http.StatusOK, r.Code) 786 | }) 787 | 788 | r.GET("/auth/hello"). 789 | SetHeader(gofight.H{ 790 | "Authorization": "Bearer " + userToken, 791 | }). 792 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 793 | assert.Equal(t, http.StatusOK, r.Code) 794 | }) 795 | 796 | r.POST("/login"). 797 | SetJSON(gofight.D{ 798 | "username": "test", 799 | "password": "test", 800 | }). 801 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 802 | token := gjson.Get(r.Body.String(), "token") 803 | userToken = token.String() 804 | assert.Equal(t, http.StatusOK, r.Code) 805 | }) 806 | 807 | r.GET("/auth/hello"). 808 | SetHeader(gofight.H{ 809 | "Authorization": "Bearer " + userToken, 810 | }). 811 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 812 | assert.Equal(t, http.StatusOK, r.Code) 813 | }) 814 | } 815 | 816 | func ConvertClaims(claims MapClaims) map[string]interface{} { 817 | return map[string]interface{}{} 818 | } 819 | 820 | func TestEmptyClaims(t *testing.T) { 821 | // the middleware to test 822 | authMiddleware, _ := New(&GinJWTMiddleware{ 823 | Realm: "test zone", 824 | Key: key, 825 | Timeout: time.Hour, 826 | MaxRefresh: time.Hour * 24, 827 | Authenticator: func(c *gin.Context) (interface{}, error) { 828 | var loginVals Login 829 | userID := loginVals.Username 830 | password := loginVals.Password 831 | 832 | if userID == "admin" && password == "admin" { 833 | return "", nil 834 | } 835 | 836 | if userID == "test" && password == "test" { 837 | return "Administrator", nil 838 | } 839 | 840 | return userID, ErrFailedAuthentication 841 | }, 842 | Unauthorized: func(c *gin.Context, code int, message string) { 843 | assert.Empty(t, ExtractClaims(c)) 844 | assert.Empty(t, ConvertClaims(ExtractClaims(c))) 845 | c.String(code, message) 846 | }, 847 | }) 848 | 849 | r := gofight.New() 850 | handler := ginHandler(authMiddleware) 851 | 852 | r.GET("/auth/hello"). 853 | SetHeader(gofight.H{ 854 | "Authorization": "Bearer 1234", 855 | }). 856 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 857 | assert.Equal(t, http.StatusUnauthorized, r.Code) 858 | }) 859 | 860 | assert.Empty(t, MapClaims{}) 861 | } 862 | 863 | func TestUnauthorized(t *testing.T) { 864 | // the middleware to test 865 | authMiddleware, _ := New(&GinJWTMiddleware{ 866 | Realm: "test zone", 867 | Key: key, 868 | Timeout: time.Hour, 869 | MaxRefresh: time.Hour * 24, 870 | Authenticator: defaultAuthenticator, 871 | Unauthorized: func(c *gin.Context, code int, message string) { 872 | c.String(code, message) 873 | }, 874 | }) 875 | 876 | handler := ginHandler(authMiddleware) 877 | 878 | r := gofight.New() 879 | 880 | r.GET("/auth/hello"). 881 | SetHeader(gofight.H{ 882 | "Authorization": "Bearer 1234", 883 | }). 884 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 885 | assert.Equal(t, http.StatusUnauthorized, r.Code) 886 | }) 887 | } 888 | 889 | func TestTokenExpire(t *testing.T) { 890 | // the middleware to test 891 | authMiddleware, _ := New(&GinJWTMiddleware{ 892 | Realm: "test zone", 893 | Key: key, 894 | Timeout: time.Hour, 895 | MaxRefresh: -time.Second, 896 | Authenticator: defaultAuthenticator, 897 | Unauthorized: func(c *gin.Context, code int, message string) { 898 | c.String(code, message) 899 | }, 900 | }) 901 | 902 | handler := ginHandler(authMiddleware) 903 | 904 | r := gofight.New() 905 | 906 | userToken, _, _ := authMiddleware.TokenGenerator(jwt.MapClaims{ 907 | "identity": "admin", 908 | }) 909 | 910 | r.GET("/auth/refresh_token"). 911 | SetHeader(gofight.H{ 912 | "Authorization": "Bearer " + userToken, 913 | }). 914 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 915 | assert.Equal(t, http.StatusUnauthorized, r.Code) 916 | }) 917 | } 918 | 919 | func TestTokenFromQueryString(t *testing.T) { 920 | // the middleware to test 921 | authMiddleware, _ := New(&GinJWTMiddleware{ 922 | Realm: "test zone", 923 | Key: key, 924 | Timeout: time.Hour, 925 | Authenticator: defaultAuthenticator, 926 | Unauthorized: func(c *gin.Context, code int, message string) { 927 | c.String(code, message) 928 | }, 929 | TokenLookup: "query:token", 930 | }) 931 | 932 | handler := ginHandler(authMiddleware) 933 | 934 | r := gofight.New() 935 | 936 | userToken, _, _ := authMiddleware.TokenGenerator(jwt.MapClaims{ 937 | "identity": "admin", 938 | }) 939 | 940 | r.GET("/auth/refresh_token"). 941 | SetHeader(gofight.H{ 942 | "Authorization": "Bearer " + userToken, 943 | }). 944 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 945 | assert.Equal(t, http.StatusUnauthorized, r.Code) 946 | }) 947 | 948 | r.GET("/auth/refresh_token?token="+userToken). 949 | SetHeader(gofight.H{ 950 | "Authorization": "Bearer " + userToken, 951 | }). 952 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 953 | assert.Equal(t, http.StatusOK, r.Code) 954 | }) 955 | } 956 | 957 | func TestTokenFromParamPath(t *testing.T) { 958 | // the middleware to test 959 | authMiddleware, _ := New(&GinJWTMiddleware{ 960 | Realm: "test zone", 961 | Key: key, 962 | Timeout: time.Hour, 963 | Authenticator: defaultAuthenticator, 964 | Unauthorized: func(c *gin.Context, code int, message string) { 965 | c.String(code, message) 966 | }, 967 | TokenLookup: "param:token", 968 | }) 969 | 970 | handler := ginHandler(authMiddleware) 971 | 972 | r := gofight.New() 973 | 974 | userToken, _, _ := authMiddleware.TokenGenerator(jwt.MapClaims{ 975 | "identity": "admin", 976 | }) 977 | 978 | r.GET("/auth/refresh_token"). 979 | SetHeader(gofight.H{ 980 | "Authorization": "Bearer " + userToken, 981 | }). 982 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 983 | assert.Equal(t, http.StatusUnauthorized, r.Code) 984 | }) 985 | 986 | r.GET("/g/"+userToken+"/refresh_token"). 987 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 988 | assert.Equal(t, http.StatusOK, r.Code) 989 | }) 990 | } 991 | 992 | func TestTokenFromCookieString(t *testing.T) { 993 | // the middleware to test 994 | authMiddleware, _ := New(&GinJWTMiddleware{ 995 | Realm: "test zone", 996 | Key: key, 997 | Timeout: time.Hour, 998 | Authenticator: defaultAuthenticator, 999 | Unauthorized: func(c *gin.Context, code int, message string) { 1000 | c.String(code, message) 1001 | }, 1002 | TokenLookup: "cookie:token", 1003 | }) 1004 | 1005 | handler := ginHandler(authMiddleware) 1006 | 1007 | r := gofight.New() 1008 | 1009 | userToken, _, _ := authMiddleware.TokenGenerator(jwt.MapClaims{ 1010 | "identity": "admin", 1011 | }) 1012 | 1013 | r.GET("/auth/refresh_token"). 1014 | SetHeader(gofight.H{ 1015 | "Authorization": "Bearer " + userToken, 1016 | }). 1017 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1018 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1019 | }) 1020 | 1021 | r.GET("/auth/hello"). 1022 | SetHeader(gofight.H{ 1023 | "Authorization": "Bearer " + userToken, 1024 | }). 1025 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1026 | token := gjson.Get(r.Body.String(), "token") 1027 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1028 | assert.Equal(t, "", token.String()) 1029 | }) 1030 | 1031 | r.GET("/auth/refresh_token"). 1032 | SetCookie(gofight.H{ 1033 | "token": userToken, 1034 | }). 1035 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1036 | assert.Equal(t, http.StatusOK, r.Code) 1037 | }) 1038 | 1039 | r.GET("/auth/hello"). 1040 | SetCookie(gofight.H{ 1041 | "token": userToken, 1042 | }). 1043 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1044 | token := gjson.Get(r.Body.String(), "token") 1045 | assert.Equal(t, http.StatusOK, r.Code) 1046 | assert.Equal(t, userToken, token.String()) 1047 | }) 1048 | } 1049 | 1050 | func TestDefineTokenHeadName(t *testing.T) { 1051 | // the middleware to test 1052 | authMiddleware, _ := New(&GinJWTMiddleware{ 1053 | Realm: "test zone", 1054 | Key: key, 1055 | Timeout: time.Hour, 1056 | TokenHeadName: "JWTTOKEN ", 1057 | Authenticator: defaultAuthenticator, 1058 | }) 1059 | 1060 | handler := ginHandler(authMiddleware) 1061 | 1062 | r := gofight.New() 1063 | 1064 | r.GET("/auth/hello"). 1065 | SetHeader(gofight.H{ 1066 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 1067 | }). 1068 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1069 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1070 | }) 1071 | 1072 | r.GET("/auth/hello"). 1073 | SetHeader(gofight.H{ 1074 | "Authorization": "JWTTOKEN " + makeTokenString("HS256", "admin"), 1075 | }). 1076 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1077 | assert.Equal(t, http.StatusOK, r.Code) 1078 | }) 1079 | } 1080 | 1081 | func TestHTTPStatusMessageFunc(t *testing.T) { 1082 | successError := errors.New("Successful test error") 1083 | failedError := errors.New("Failed test error") 1084 | successMessage := "Overwrite error message." 1085 | 1086 | authMiddleware, _ := New(&GinJWTMiddleware{ 1087 | Key: key, 1088 | Timeout: time.Hour, 1089 | MaxRefresh: time.Hour * 24, 1090 | Authenticator: defaultAuthenticator, 1091 | 1092 | HTTPStatusMessageFunc: func(e error, c *gin.Context) string { 1093 | if e == successError { 1094 | return successMessage 1095 | } 1096 | 1097 | return e.Error() 1098 | }, 1099 | }) 1100 | 1101 | successString := authMiddleware.HTTPStatusMessageFunc(successError, nil) 1102 | failedString := authMiddleware.HTTPStatusMessageFunc(failedError, nil) 1103 | 1104 | assert.Equal(t, successMessage, successString) 1105 | assert.NotEqual(t, successMessage, failedString) 1106 | } 1107 | 1108 | func TestSendAuthorizationBool(t *testing.T) { 1109 | // the middleware to test 1110 | authMiddleware, _ := New(&GinJWTMiddleware{ 1111 | Realm: "test zone", 1112 | Key: key, 1113 | Timeout: time.Hour, 1114 | MaxRefresh: time.Hour * 24, 1115 | Authenticator: defaultAuthenticator, 1116 | SendAuthorization: true, 1117 | Authorizator: func(data interface{}, c *gin.Context) bool { 1118 | return data.(string) == "admin" 1119 | }, 1120 | }) 1121 | 1122 | handler := ginHandler(authMiddleware) 1123 | 1124 | r := gofight.New() 1125 | 1126 | r.GET("/auth/hello"). 1127 | SetHeader(gofight.H{ 1128 | "Authorization": "Bearer " + makeTokenString("HS256", "test"), 1129 | }). 1130 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1131 | assert.Equal(t, http.StatusForbidden, r.Code) 1132 | }) 1133 | 1134 | r.GET("/auth/hello"). 1135 | SetHeader(gofight.H{ 1136 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 1137 | }). 1138 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1139 | //nolint:staticcheck 1140 | token := r.HeaderMap.Get("Authorization") 1141 | assert.Equal(t, "Bearer "+makeTokenString("HS256", "admin"), token) 1142 | assert.Equal(t, http.StatusOK, r.Code) 1143 | }) 1144 | } 1145 | 1146 | func TestExpiredTokenOnAuth(t *testing.T) { 1147 | // the middleware to test 1148 | authMiddleware, _ := New(&GinJWTMiddleware{ 1149 | Realm: "test zone", 1150 | Key: key, 1151 | Timeout: time.Hour, 1152 | MaxRefresh: time.Hour * 24, 1153 | Authenticator: defaultAuthenticator, 1154 | SendAuthorization: true, 1155 | Authorizator: func(data interface{}, c *gin.Context) bool { 1156 | return data.(string) == "admin" 1157 | }, 1158 | TimeFunc: func() time.Time { 1159 | return time.Now().AddDate(0, 0, 1) 1160 | }, 1161 | }) 1162 | 1163 | handler := ginHandler(authMiddleware) 1164 | 1165 | r := gofight.New() 1166 | 1167 | r.GET("/auth/hello"). 1168 | SetHeader(gofight.H{ 1169 | "Authorization": "Bearer " + makeTokenString("HS256", "admin"), 1170 | }). 1171 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1172 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1173 | }) 1174 | } 1175 | 1176 | func TestBadTokenOnRefreshHandler(t *testing.T) { 1177 | // the middleware to test 1178 | authMiddleware, _ := New(&GinJWTMiddleware{ 1179 | Realm: "test zone", 1180 | Key: key, 1181 | Timeout: time.Hour, 1182 | Authenticator: defaultAuthenticator, 1183 | }) 1184 | 1185 | handler := ginHandler(authMiddleware) 1186 | 1187 | r := gofight.New() 1188 | 1189 | r.GET("/auth/refresh_token"). 1190 | SetHeader(gofight.H{ 1191 | "Authorization": "Bearer " + "BadToken", 1192 | }). 1193 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1194 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1195 | }) 1196 | } 1197 | 1198 | func TestExpiredField(t *testing.T) { 1199 | // the middleware to test 1200 | authMiddleware, _ := New(&GinJWTMiddleware{ 1201 | Realm: "test zone", 1202 | Key: key, 1203 | Timeout: time.Hour, 1204 | Authenticator: defaultAuthenticator, 1205 | }) 1206 | 1207 | handler := ginHandler(authMiddleware) 1208 | 1209 | r := gofight.New() 1210 | 1211 | token := jwt.New(jwt.GetSigningMethod("HS256")) 1212 | claims := token.Claims.(jwt.MapClaims) 1213 | claims["identity"] = "admin" 1214 | claims["orig_iat"] = 0 1215 | tokenString, _ := token.SignedString(key) 1216 | 1217 | r.GET("/auth/hello"). 1218 | SetHeader(gofight.H{ 1219 | "Authorization": "Bearer " + tokenString, 1220 | }). 1221 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1222 | message := gjson.Get(r.Body.String(), "message") 1223 | 1224 | assert.Equal(t, ErrMissingExpField.Error(), message.String()) 1225 | assert.Equal(t, http.StatusBadRequest, r.Code) 1226 | }) 1227 | 1228 | // wrong format 1229 | claims["exp"] = "wrongFormatForExpiryIgnoredByJwtLibrary" 1230 | tokenString, _ = token.SignedString(key) 1231 | 1232 | r.GET("/auth/hello"). 1233 | SetHeader(gofight.H{ 1234 | "Authorization": "Bearer " + tokenString, 1235 | }). 1236 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1237 | message := gjson.Get(r.Body.String(), "message") 1238 | 1239 | assert.Equal(t, ErrExpiredToken.Error(), strings.ToLower(message.String())) 1240 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1241 | }) 1242 | } 1243 | 1244 | func TestCheckTokenString(t *testing.T) { 1245 | // the middleware to test 1246 | authMiddleware, _ := New(&GinJWTMiddleware{ 1247 | Realm: "test zone", 1248 | Key: key, 1249 | Timeout: 1 * time.Second, 1250 | Authenticator: defaultAuthenticator, 1251 | Unauthorized: func(c *gin.Context, code int, message string) { 1252 | c.String(code, message) 1253 | }, 1254 | PayloadFunc: func(data interface{}) MapClaims { 1255 | if v, ok := data.(MapClaims); ok { 1256 | return v 1257 | } 1258 | 1259 | return nil 1260 | }, 1261 | }) 1262 | 1263 | handler := ginHandler(authMiddleware) 1264 | 1265 | r := gofight.New() 1266 | 1267 | userToken, _, _ := authMiddleware.TokenGenerator(MapClaims{ 1268 | "identity": "admin", 1269 | }) 1270 | 1271 | r.GET("/auth/hello"). 1272 | SetHeader(gofight.H{ 1273 | "Authorization": "Bearer " + userToken, 1274 | }). 1275 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1276 | assert.Equal(t, http.StatusOK, r.Code) 1277 | }) 1278 | 1279 | token, err := authMiddleware.ParseTokenString(userToken) 1280 | assert.NoError(t, err) 1281 | claims := ExtractClaimsFromToken(token) 1282 | assert.Equal(t, "admin", claims["identity"]) 1283 | 1284 | time.Sleep(2 * time.Second) 1285 | 1286 | r.GET("/auth/hello"). 1287 | SetHeader(gofight.H{ 1288 | "Authorization": "Bearer " + userToken, 1289 | }). 1290 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1291 | assert.Equal(t, http.StatusUnauthorized, r.Code) 1292 | }) 1293 | 1294 | _, err = authMiddleware.ParseTokenString(userToken) 1295 | assert.Error(t, err) 1296 | assert.Equal(t, MapClaims{}, ExtractClaimsFromToken(nil)) 1297 | } 1298 | 1299 | func TestLogout(t *testing.T) { 1300 | cookieName := "jwt" 1301 | cookieDomain := "example.com" 1302 | // the middleware to test 1303 | authMiddleware, _ := New(&GinJWTMiddleware{ 1304 | Realm: "test zone", 1305 | Key: key, 1306 | Timeout: time.Hour, 1307 | Authenticator: defaultAuthenticator, 1308 | SendCookie: true, 1309 | CookieName: cookieName, 1310 | CookieDomain: cookieDomain, 1311 | }) 1312 | 1313 | handler := ginHandler(authMiddleware) 1314 | 1315 | r := gofight.New() 1316 | 1317 | r.POST("/logout"). 1318 | Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) { 1319 | assert.Equal(t, http.StatusOK, r.Code) 1320 | //nolint:staticcheck 1321 | assert.Equal(t, fmt.Sprintf("%s=; Path=/; Domain=%s; Max-Age=0", cookieName, cookieDomain), r.HeaderMap.Get("Set-Cookie")) 1322 | }) 1323 | } 1324 | 1325 | func TestSetCookie(t *testing.T) { 1326 | w := httptest.NewRecorder() 1327 | c, _ := gin.CreateTestContext(w) 1328 | 1329 | mw, _ := New(&GinJWTMiddleware{ 1330 | Realm: "test zone", 1331 | Key: key, 1332 | Timeout: time.Hour, 1333 | Authenticator: defaultAuthenticator, 1334 | SendCookie: true, 1335 | CookieName: "jwt", 1336 | CookieMaxAge: time.Hour, 1337 | CookieDomain: "example.com", 1338 | SecureCookie: false, 1339 | CookieHTTPOnly: true, 1340 | TimeFunc: func() time.Time { 1341 | return time.Now() 1342 | }, 1343 | }) 1344 | 1345 | token := makeTokenString("HS384", "admin") 1346 | 1347 | mw.SetCookie(c, token) 1348 | 1349 | cookies := w.Result().Cookies() 1350 | 1351 | assert.Len(t, cookies, 1) 1352 | 1353 | cookie := cookies[0] 1354 | assert.Equal(t, "jwt", cookie.Name) 1355 | assert.Equal(t, token, cookie.Value) 1356 | assert.Equal(t, "/", cookie.Path) 1357 | assert.Equal(t, "example.com", cookie.Domain) 1358 | assert.Equal(t, true, cookie.HttpOnly) 1359 | } 1360 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/appleboy/gin-jwt/v2 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.1.2 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/golang-jwt/jwt/v4 v4.5.2 9 | github.com/stretchr/testify v1.10.0 10 | github.com/tidwall/gjson v1.17.1 11 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.12.9 // indirect 16 | github.com/bytedance/sonic/loader v0.2.3 // indirect 17 | github.com/cloudwego/base64x v0.1.5 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 20 | github.com/gin-contrib/sse v1.0.0 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/go-playground/validator/v10 v10.25.0 // indirect 24 | github.com/goccy/go-json v0.10.5 // indirect 25 | github.com/json-iterator/go v1.1.12 // indirect 26 | github.com/klauspost/cpuid/v2 v2.2.9 // indirect 27 | github.com/kr/pretty v0.3.0 // indirect 28 | github.com/leodido/go-urn v1.4.0 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/rogpeppe/go-internal v1.8.0 // indirect 35 | github.com/tidwall/match v1.1.1 // indirect 36 | github.com/tidwall/pretty v1.2.0 // indirect 37 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 38 | github.com/ugorji/go/codec v1.2.12 // indirect 39 | golang.org/x/arch v0.14.0 // indirect 40 | golang.org/x/crypto v0.36.0 // indirect 41 | golang.org/x/net v0.37.0 // indirect 42 | golang.org/x/sys v0.31.0 // indirect 43 | golang.org/x/text v0.23.0 // indirect 44 | google.golang.org/protobuf v1.36.5 // indirect 45 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 46 | gopkg.in/yaml.v3 v3.0.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 2 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 3 | github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ= 4 | github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= 7 | github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 8 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 9 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= 16 | github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 17 | github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= 18 | github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= 19 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 20 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= 28 | github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 29 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 30 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 31 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 32 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 33 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 34 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 35 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 36 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 37 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 38 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 39 | github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= 40 | github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 41 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 42 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 43 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 44 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 45 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 46 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 47 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 48 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 49 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 50 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 51 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 52 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 53 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 54 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 58 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 59 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 60 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 61 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 62 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 63 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 64 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 65 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 66 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 67 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 68 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 69 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 70 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 71 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 72 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 75 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 76 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 77 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 78 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 79 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 80 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 81 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 82 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 83 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 84 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 85 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 86 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 87 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 88 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 89 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 90 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 91 | golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= 92 | golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 93 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 94 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 95 | golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= 96 | golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 97 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 98 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 99 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 100 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 101 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 102 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 103 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 104 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 105 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 106 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 107 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 108 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 109 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 110 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 111 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 112 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 113 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 114 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 115 | -------------------------------------------------------------------------------- /screenshot/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/dfa65975a0d79fd981cf5641275e81ce81d2a4fc/screenshot/login.png -------------------------------------------------------------------------------- /screenshot/refresh_token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/dfa65975a0d79fd981cf5641275e81ce81d2a4fc/screenshot/refresh_token.png -------------------------------------------------------------------------------- /testdata/invalidprivkey.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/dfa65975a0d79fd981cf5641275e81ce81d2a4fc/testdata/invalidprivkey.key -------------------------------------------------------------------------------- /testdata/invalidpubkey.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/dfa65975a0d79fd981cf5641275e81ce81d2a4fc/testdata/invalidpubkey.key -------------------------------------------------------------------------------- /testdata/jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAyRUuEd6LczbWCnLy3dUvU7stxcuKcCEetQiAF2T0LLEPL6M1 3 | DBz6hzU7Roeznn7RtJFOAEbcyetVS1VolSliw97wlIWcfJWtx2uv+0PLPAGW0dUQ 4 | rrBoWeOExhysB7NWhtChAbeICGNcHoWWrGyqdAvNKLvH6uHfyJqM5cMIrDvA/eoB 5 | Gm/31b5sLA+pf+lNwm5Lktss/FhCcHTtZpy/sp5iG2KfKU3EEDQ0FkqCsBKbizkv 6 | 5dmDWxke5XpSpBWIlV0Zqz7lhiWvRclG3qek6mxNQqjl3c1DssdvJ7ruV8UORpMp 7 | DphFlZKqHk/6a6oafqNJ4BuXOEo9nZ/oOGty2QIDAQABAoIBAFDZETLiFZN3YsvE 8 | t911T5gM1DSIx9qZlm0XQ9kkIACwF/kBV9zM8fXW80RCX3fEabB+E6yM0UzmL98g 9 | MfJ3N1ylkHlG10pILBzYMWOHOHmh8e/gCNsT1oD9t26oLIrUEmAWFgZIsosc1/b1 10 | o0UkU8xgylYsWg8YTg+sBCaFKkGE9XivjbayIQHZgh3y4FGl41mXzHsbyec28dnV 11 | JqK/QtiX1dGXhPb/Uc3Ro3zHEtjwVItNgc7aywajz2N9V9ttCyrSoJli0fUPpRaA 12 | Qlk4vftIJCIH4jvaqgyBY/CpdYL8OoGgb3dwZnqxrxhdCa1H/PJ6a2Eze2UmrN7a 13 | bZGCB2ECgYEA/AnfVKIiuwfOmRHPGDIRDn1Q6jI2wXBjsFgLw4AmmuXYapc2EAne 14 | woHz5MdXdAE9tJxD4nyl0ugxfW3LvpjrAYiXbP0m3CLujzX77vkIJrz+52qHqDLl 15 | sNFxUmnF/e4uN62iDCc/gk4G9Hqu62pWp0cAlT+l4UOMMRqnHRsRlS0CgYEAzD5G 16 | ztcqAzFH+3IvpAy1+yad5QMPqv9si48fdVTUQDLbFYXe93rzG57jq7eKJ4jkMvgv 17 | 8/e9b1eJx6zWW/91xuuHNddtwIYADpyLmdScS2eaDivMZ1ldTjhVT7Bccen9dPqS 18 | eX4Nhxx8Uoq8sFDq5Br7O8B5KeSiNFRsMECVN90CgYEAo3iXxOIAmsR+iKOXaf8X 19 | Nwmq0KvO/foyfm8s+hmFcJRBoSkAZLiyJgB5u1pb657eceWk1iK4vyng55SuQKoY 20 | Sv9YD9XGPaPejT6bcC1PzyhoQJrE8CBLADtoP+bhB0lT6sMQxscyFwca1bk4+PIY 21 | 0BhqVWNZ6NiR9ktuNp+W8OUCgYBzYjteXu+9HfosczW21/d3CznoRvJzCBmqPhDn 22 | mCTQn+plHlv4M91jnT/Bos7JxuwkX1G34h2C6VFNHLd9AbTny+d24119hjZCCu5S 23 | 2Wnyr3S4zMWNHU85AVowytFvCWHG1EgrmqrJya3yc65lbVFFzHhiKTpKEIASUB9O 24 | oy2pgQKBgDRIauxpBY2LfK6BVihqwJobIqDkbwHf2e/kxFATaD5aAKufogg0kXGY 25 | BgMli9iTK/xD/M+yZATWq12oYmeKsl6YLawhs9XPENmlDaFgLYysDy3vdpbCKQAC 26 | 09wD0hUEtLJSIN6JkRAwH9lVi7eB/i1JqdQRIEwFFCtbQKz6jhpH 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /testdata/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyRUuEd6LczbWCnLy3dUv 3 | U7stxcuKcCEetQiAF2T0LLEPL6M1DBz6hzU7Roeznn7RtJFOAEbcyetVS1VolSli 4 | w97wlIWcfJWtx2uv+0PLPAGW0dUQrrBoWeOExhysB7NWhtChAbeICGNcHoWWrGyq 5 | dAvNKLvH6uHfyJqM5cMIrDvA/eoBGm/31b5sLA+pf+lNwm5Lktss/FhCcHTtZpy/ 6 | sp5iG2KfKU3EEDQ0FkqCsBKbizkv5dmDWxke5XpSpBWIlV0Zqz7lhiWvRclG3qek 7 | 6mxNQqjl3c1DssdvJ7ruV8UORpMpDphFlZKqHk/6a6oafqNJ4BuXOEo9nZ/oOGty 8 | 2QIDAQAB 9 | -----END PUBLIC KEY----- 10 | --------------------------------------------------------------------------------