├── .gitignore ├── README.md ├── .github ├── workflows │ ├── security.yml │ ├── linter.yml │ ├── auto-labeler.yml │ ├── release-drafter.yml │ ├── gotidy.yml │ ├── dependabot_automerge.yml │ └── test.yml ├── dependabot.yml ├── labeler.yml └── release-drafter.yml ├── LICENSE ├── go.mod ├── main.go ├── crypto.go ├── jwt.go ├── config_test.go ├── go.sum ├── config.go └── main_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore JetBrains IDE files. 2 | .idea/ 3 | vendor 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Deprecated repository 2 | 3 | This middleware is no longer maintained, it is available within [Fiber Contrib](https://github.com/gofiber/contrib/tree/main/jwt). 4 | -------------------------------------------------------------------------------- /.github/workflows/security.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - main 6 | pull_request: 7 | name: Security 8 | jobs: 9 | Gosec: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Fetch Repository 13 | uses: actions/checkout@v3.5.2 14 | - name: Run Gosec 15 | uses: securego/gosec@master 16 | with: 17 | args: ./... 18 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | - main 6 | pull_request: 7 | name: Linter 8 | jobs: 9 | Golint: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Fetch Repository 13 | uses: actions/checkout@v3.5.2 14 | - name: Run Golint 15 | uses: reviewdog/action-golangci-lint@v2 16 | with: 17 | golangci_lint_flags: "--tests=false" 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" # Location of package manifests 7 | labels: 8 | - "🤖 Dependencies" 9 | schedule: 10 | interval: daily 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | labels: 16 | - "🤖 Dependencies" 17 | -------------------------------------------------------------------------------- /.github/workflows/auto-labeler.yml: -------------------------------------------------------------------------------- 1 | name: Auto labeler 2 | on: 3 | issues: 4 | types: [ opened, edited, milestoned ] 5 | pull_request_target: 6 | types: [ opened ] 7 | permissions: 8 | contents: read 9 | issues: write 10 | pull-requests: write 11 | statuses: write 12 | checks: write 13 | jobs: 14 | labeler: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check Labels 18 | id: labeler 19 | uses: fuxingloh/multi-labeler@v2 20 | with: 21 | github-token: ${{secrets.GITHUB_TOKEN}} 22 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | version: v1 2 | labels: 3 | - label: '📒 Documentation' 4 | matcher: 5 | title: '\b(docs|doc:|\[doc\]|README|typos|comment|documentation)\b' 6 | - label: '☢️ Bug' 7 | matcher: 8 | title: '\b(fix|race|bug|missing|correct)\b' 9 | - label: '🧹 Updates' 10 | matcher: 11 | title: '\b(improve|update|refactor|deprecated|remove|unused|test)\b' 12 | - label: '🤖 Dependencies' 13 | matcher: 14 | title: '\b(bumb|bdependencies)\b' 15 | - label: '✏️ Feature' 16 | matcher: 17 | title: '\b(feature|feat|create|implement|add)\b' 18 | - label: '🤔 Question' 19 | matcher: 20 | title: '\b(question|how)\b' 21 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | - main 9 | 10 | jobs: 11 | update_release_draft: 12 | runs-on: ubuntu-latest 13 | steps: 14 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 15 | #- name: Set GHE_HOST 16 | # run: | 17 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 18 | 19 | # Drafts your next Release notes as Pull Requests are merged into "master" 20 | - uses: release-drafter/release-drafter@v5 21 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 22 | # with: 23 | # config-name: my-config.yml 24 | # disable-autolabeler: true 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fiber 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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // Deprecated: Now part of the fiber contrib repo: 2 | // github.com/gofiber/contrib/jwt 3 | module github.com/gofiber/jwt/v4 4 | 5 | go 1.18 6 | 7 | require ( 8 | github.com/MicahParks/keyfunc/v2 v2.0.3 9 | github.com/gofiber/fiber/v2 v2.46.0 10 | github.com/golang-jwt/jwt/v5 v5.0.0 11 | ) 12 | 13 | require ( 14 | github.com/andybalholm/brotli v1.0.5 // indirect 15 | github.com/google/uuid v1.3.0 // indirect 16 | github.com/klauspost/compress v1.16.3 // indirect 17 | github.com/mattn/go-colorable v0.1.13 // indirect 18 | github.com/mattn/go-isatty v0.0.18 // indirect 19 | github.com/mattn/go-runewidth v0.0.14 // indirect 20 | github.com/philhofer/fwd v1.1.2 // indirect 21 | github.com/rivo/uniseg v0.2.0 // indirect 22 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 23 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 24 | github.com/tinylib/msgp v1.1.8 // indirect 25 | github.com/valyala/bytebufferpool v1.0.0 // indirect 26 | github.com/valyala/fasthttp v1.47.0 // indirect 27 | github.com/valyala/tcplisten v1.0.0 // indirect 28 | golang.org/x/sys v0.8.0 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | categories: 4 | - title: '🚀 New' 5 | labels: 6 | - '✏️ Feature' 7 | - title: '🧹 Updates' 8 | labels: 9 | - '🧹 Updates' 10 | - '🤖 Dependencies' 11 | - title: '🐛 Fixes' 12 | labels: 13 | - '☢️ Bug' 14 | - title: '📚 Documentation' 15 | labels: 16 | - '📒 Documentation' 17 | change-template: '- $TITLE (#$NUMBER)' 18 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 19 | version-resolver: 20 | major: 21 | labels: 22 | - 'major' 23 | minor: 24 | labels: 25 | - 'minor' 26 | - '✏️ Feature' 27 | patch: 28 | labels: 29 | - 'patch' 30 | - '📒 Documentation' 31 | - '☢️ Bug' 32 | - '🤖 Dependencies' 33 | - '🧹 Updates' 34 | default: patch 35 | template: | 36 | $CHANGES 37 | 38 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 39 | 40 | Thank you $CONTRIBUTORS for making this update possible. 41 | -------------------------------------------------------------------------------- /.github/workflows/gotidy.yml: -------------------------------------------------------------------------------- 1 | name: Tidy 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths: 8 | - '.github/workflows/gotidy.yml' 9 | - 'go.mod' 10 | - 'go.sum' 11 | 12 | jobs: 13 | fix: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - 17 | name: Checkout 18 | uses: actions/checkout@v3.5.2 19 | - 20 | name: Set up Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: 1.18 24 | - 25 | name: Tidy 26 | run: | 27 | rm -f go.sum 28 | go mod tidy 29 | - 30 | name: Set up Git 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | run: | 34 | git config user.name GitHub 35 | git config user.email noreply@github.com 36 | git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 37 | - 38 | name: Commit and push changes 39 | run: | 40 | git add . 41 | if output=$(git status --porcelain) && [ ! -z "$output" ]; then 42 | git commit --author "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" --message "Fix go modules" 43 | git push 44 | fi 45 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: 3 | pull_request 4 | 5 | permissions: 6 | contents: write 7 | pull-requests: write 8 | 9 | jobs: 10 | wait_for_checks: 11 | runs-on: ubuntu-latest 12 | if: ${{ github.actor == 'dependabot[bot]' }} 13 | steps: 14 | - name: Wait for check is finished 15 | uses: lewagon/wait-on-check-action@v1.3.1 16 | id: wait_for_checks 17 | with: 18 | ref: ${{ github.event.pull_request.head.sha || github.sha }} 19 | running-workflow-name: wait_for_checks 20 | check-regexp: Tests 21 | repo-token: ${{ secrets.PR_TOKEN }} 22 | wait-interval: 10 23 | dependabot: 24 | needs: [wait_for_checks] 25 | name: Dependabot auto-merge 26 | runs-on: ubuntu-latest 27 | if: ${{ github.actor == 'dependabot[bot]' }} 28 | steps: 29 | - name: Dependabot metadata 30 | id: metadata 31 | uses: dependabot/fetch-metadata@v1.5.1 32 | with: 33 | github-token: "${{ secrets.PR_TOKEN }}" 34 | - name: Enable auto-merge for Dependabot PRs 35 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}} 36 | run: | 37 | gh pr review --approve "$PR_URL" 38 | gh pr merge --auto --merge "$PR_URL" 39 | env: 40 | PR_URL: ${{github.event.pull_request.html_url}} 41 | GITHUB_TOKEN: ${{secrets.PR_TOKEN}} 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: Tests 7 | jobs: 8 | Tests: 9 | name: Tests 10 | strategy: 11 | matrix: 12 | go-version: 13 | - 1.18.x 14 | - 1.20.x 15 | platform: 16 | - ubuntu-latest 17 | - windows-latest 18 | runs-on: '${{ matrix.platform }}' 19 | steps: 20 | - name: Install Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | - name: Setup Golang caches 25 | uses: actions/cache@v3 26 | with: 27 | # In order: 28 | # * Module download cache 29 | # * Build cache (Linux) 30 | # * Build cache (Mac) 31 | # * Build cache (Windows) 32 | path: | 33 | ~/go/pkg/mod 34 | ~/.cache/go-build 35 | ~/Library/Caches/go-build 36 | ~\AppData\Local\go-build 37 | key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} 38 | restore-keys: | 39 | ${{ runner.os }}-go-${{ matrix.go-version }}- 40 | - name: Fetch Repository 41 | uses: actions/checkout@v3.5.2 42 | - name: Run Test 43 | run: go test ./... -v -race 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 🚀 Fiber is an Express inspired web framework written in Go with 💖 2 | // 📌 API Documentation: https://fiber.wiki 3 | // 📝 Github Repository: https://github.com/gofiber/fiber 4 | // Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/jwt.go 5 | 6 | package jwtware 7 | 8 | import ( 9 | "reflect" 10 | 11 | "github.com/gofiber/fiber/v2" 12 | "github.com/golang-jwt/jwt/v5" 13 | ) 14 | 15 | var ( 16 | defaultTokenLookup = "header:" + fiber.HeaderAuthorization 17 | ) 18 | 19 | // New ... 20 | func New(config ...Config) fiber.Handler { 21 | cfg := makeCfg(config) 22 | 23 | extractors := cfg.getExtractors() 24 | 25 | // Return middleware handler 26 | return func(c *fiber.Ctx) error { 27 | // Filter request to skip middleware 28 | if cfg.Filter != nil && cfg.Filter(c) { 29 | return c.Next() 30 | } 31 | var auth string 32 | var err error 33 | 34 | for _, extractor := range extractors { 35 | auth, err = extractor(c) 36 | if auth != "" && err == nil { 37 | break 38 | } 39 | } 40 | if err != nil { 41 | return cfg.ErrorHandler(c, err) 42 | } 43 | var token *jwt.Token 44 | 45 | if _, ok := cfg.Claims.(jwt.MapClaims); ok { 46 | token, err = jwt.Parse(auth, cfg.KeyFunc) 47 | } else { 48 | t := reflect.ValueOf(cfg.Claims).Type().Elem() 49 | claims := reflect.New(t).Interface().(jwt.Claims) 50 | token, err = jwt.ParseWithClaims(auth, claims, cfg.KeyFunc) 51 | } 52 | if err == nil && token.Valid { 53 | // Store user information from token into context. 54 | c.Locals(cfg.ContextKey, token) 55 | return cfg.SuccessHandler(c) 56 | } 57 | return cfg.ErrorHandler(c, err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crypto.go: -------------------------------------------------------------------------------- 1 | package jwtware 2 | 3 | const ( 4 | // HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm. 5 | HS256 = "HS256" 6 | 7 | // HS384 represents a public cryptography key generated by a 384 bit HMAC algorithm. 8 | HS384 = "HS384" 9 | 10 | // HS512 represents a public cryptography key generated by a 512 bit HMAC algorithm. 11 | HS512 = "HS512" 12 | 13 | // ES256 represents a public cryptography key generated by a 256 bit ECDSA algorithm. 14 | ES256 = "ES256" 15 | 16 | // ES384 represents a public cryptography key generated by a 384 bit ECDSA algorithm. 17 | ES384 = "ES384" 18 | 19 | // ES512 represents a public cryptography key generated by a 512 bit ECDSA algorithm. 20 | ES512 = "ES512" 21 | 22 | // P256 represents a cryptographic elliptical curve type. 23 | P256 = "P-256" 24 | 25 | // P384 represents a cryptographic elliptical curve type. 26 | P384 = "P-384" 27 | 28 | // P521 represents a cryptographic elliptical curve type. 29 | P521 = "P-521" 30 | 31 | // RS256 represents a public cryptography key generated by a 256 bit RSA algorithm. 32 | RS256 = "RS256" 33 | 34 | // RS384 represents a public cryptography key generated by a 384 bit RSA algorithm. 35 | RS384 = "RS384" 36 | 37 | // RS512 represents a public cryptography key generated by a 512 bit RSA algorithm. 38 | RS512 = "RS512" 39 | 40 | // PS256 represents a public cryptography key generated by a 256 bit RSA algorithm. 41 | PS256 = "PS256" 42 | 43 | // PS384 represents a public cryptography key generated by a 384 bit RSA algorithm. 44 | PS384 = "PS384" 45 | 46 | // PS512 represents a public cryptography key generated by a 512 bit RSA algorithm. 47 | PS512 = "PS512" 48 | ) 49 | -------------------------------------------------------------------------------- /jwt.go: -------------------------------------------------------------------------------- 1 | package jwtware 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/gofiber/fiber/v2" 8 | ) 9 | 10 | var ( 11 | // ErrJWTMissingOrMalformed is returned when the JWT is missing or malformed. 12 | ErrJWTMissingOrMalformed = errors.New("missing or malformed JWT") 13 | ) 14 | 15 | type jwtExtractor func(c *fiber.Ctx) (string, error) 16 | 17 | // jwtFromHeader returns a function that extracts token from the request header. 18 | func jwtFromHeader(header string, authScheme string) func(c *fiber.Ctx) (string, error) { 19 | return func(c *fiber.Ctx) (string, error) { 20 | auth := c.Get(header) 21 | l := len(authScheme) 22 | if len(auth) > l+1 && strings.EqualFold(auth[:l], authScheme) { 23 | return strings.TrimSpace(auth[l:]), nil 24 | } 25 | return "", ErrJWTMissingOrMalformed 26 | } 27 | } 28 | 29 | // jwtFromQuery returns a function that extracts token from the query string. 30 | func jwtFromQuery(param string) func(c *fiber.Ctx) (string, error) { 31 | return func(c *fiber.Ctx) (string, error) { 32 | token := c.Query(param) 33 | if token == "" { 34 | return "", ErrJWTMissingOrMalformed 35 | } 36 | return token, nil 37 | } 38 | } 39 | 40 | // jwtFromParam returns a function that extracts token from the url param string. 41 | func jwtFromParam(param string) func(c *fiber.Ctx) (string, error) { 42 | return func(c *fiber.Ctx) (string, error) { 43 | token := c.Params(param) 44 | if token == "" { 45 | return "", ErrJWTMissingOrMalformed 46 | } 47 | return token, nil 48 | } 49 | } 50 | 51 | // jwtFromCookie returns a function that extracts token from the named cookie. 52 | func jwtFromCookie(name string) func(c *fiber.Ctx) (string, error) { 53 | return func(c *fiber.Ctx) (string, error) { 54 | token := c.Cookies(name) 55 | if token == "" { 56 | return "", ErrJWTMissingOrMalformed 57 | } 58 | return token, nil 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package jwtware 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPanicOnMissingConfiguration(t *testing.T) { 8 | t.Parallel() 9 | 10 | defer func() { 11 | // Assert 12 | if err := recover(); err == nil { 13 | t.Fatalf("Middleware should panic on missing configuration") 14 | } 15 | }() 16 | 17 | // Arrange 18 | config := make([]Config, 0) 19 | 20 | // Act 21 | makeCfg(config) 22 | } 23 | 24 | func TestDefaultConfiguration(t *testing.T) { 25 | t.Parallel() 26 | 27 | defer func() { 28 | // Assert 29 | if err := recover(); err != nil { 30 | t.Fatalf("Middleware should not panic") 31 | } 32 | }() 33 | 34 | // Arrange 35 | config := append(make([]Config, 0), Config{ 36 | SigningKey: SigningKey{Key: []byte("")}, 37 | }) 38 | 39 | // Act 40 | cfg := makeCfg(config) 41 | 42 | // Assert 43 | if cfg.ContextKey != "user" { 44 | t.Fatalf("Default context key should be 'user'") 45 | } 46 | if cfg.Claims == nil { 47 | t.Fatalf("Default claims should not be 'nil'") 48 | } 49 | 50 | if cfg.TokenLookup != defaultTokenLookup { 51 | t.Fatalf("Default token lookup should be '%v'", defaultTokenLookup) 52 | } 53 | if cfg.AuthScheme != "Bearer" { 54 | t.Fatalf("Default auth scheme should be 'Bearer'") 55 | } 56 | } 57 | 58 | func TestExtractorsInitialization(t *testing.T) { 59 | t.Parallel() 60 | 61 | defer func() { 62 | // Assert 63 | if err := recover(); err != nil { 64 | t.Fatalf("Middleware should not panic") 65 | } 66 | }() 67 | 68 | // Arrange 69 | cfg := Config{ 70 | SigningKey: SigningKey{Key: []byte("")}, 71 | TokenLookup: defaultTokenLookup + ",query:token,param:token,cookie:token,something:something", 72 | } 73 | 74 | // Act 75 | extractors := cfg.getExtractors() 76 | 77 | // Assert 78 | if len(extractors) != 4 { 79 | t.Fatalf("Extractors should not be created for invalid lookups") 80 | } 81 | if cfg.AuthScheme != "" { 82 | t.Fatal("AuthScheme should be \"\"") 83 | } 84 | } 85 | 86 | func TestCustomTokenLookup(t *testing.T) { 87 | t.Parallel() 88 | 89 | defer func() { 90 | // Assert 91 | if err := recover(); err != nil { 92 | t.Fatalf("Middleware should not panic") 93 | } 94 | }() 95 | 96 | // Arrange 97 | lookup := `header:X-Auth` 98 | scheme := "Token" 99 | cfg := Config{ 100 | SigningKey: SigningKey{Key: []byte("")}, 101 | TokenLookup: lookup, 102 | AuthScheme: scheme, 103 | } 104 | 105 | if cfg.TokenLookup != lookup { 106 | t.Fatalf("TokenLookup should be %s", lookup) 107 | } 108 | if cfg.AuthScheme != scheme { 109 | t.Fatalf("AuthScheme should be %s", scheme) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/MicahParks/keyfunc/v2 v2.0.3 h1:uKUEOc+knRO0UoucONisgNPiT85V2s/W5c0FQYsd9kc= 2 | github.com/MicahParks/keyfunc/v2 v2.0.3/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k= 3 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 4 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 5 | github.com/gofiber/fiber/v2 v2.46.0 h1:wkkWotblsGVlLjXj2dpgKQAYHtXumsK/HyFugQM68Ns= 6 | github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc= 7 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= 8 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 9 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 12 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 13 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 14 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 15 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 16 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 17 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 18 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 19 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 20 | github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 21 | github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 22 | github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= 23 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 24 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 25 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4= 26 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8= 27 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4= 28 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk= 29 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g= 30 | github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw= 31 | github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 32 | github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= 33 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 34 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 35 | github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c= 36 | github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 37 | github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= 38 | github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 39 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 40 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 41 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 42 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 43 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 44 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 45 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 46 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 47 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 51 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 52 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 53 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 54 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 69 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 71 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 72 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 76 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 78 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 79 | golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 80 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 81 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 82 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 83 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package jwtware 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/MicahParks/keyfunc/v2" 11 | "github.com/gofiber/fiber/v2" 12 | "github.com/golang-jwt/jwt/v5" 13 | ) 14 | 15 | var ( 16 | // ErrJWTAlg is returned when the JWT header did not contain the expected algorithm. 17 | ErrJWTAlg = errors.New("the JWT header did not contain the expected algorithm") 18 | ) 19 | 20 | // Config defines the config for JWT middleware 21 | type Config struct { 22 | // Filter defines a function to skip middleware. 23 | // Optional. Default: nil 24 | Filter func(*fiber.Ctx) bool 25 | 26 | // SuccessHandler defines a function which is executed for a valid token. 27 | // Optional. Default: nil 28 | SuccessHandler fiber.Handler 29 | 30 | // ErrorHandler defines a function which is executed for an invalid token. 31 | // It may be used to define a custom JWT error. 32 | // Optional. Default: 401 Invalid or expired JWT 33 | ErrorHandler fiber.ErrorHandler 34 | 35 | // Signing key to validate token. Used as fallback if SigningKeys has length 0. 36 | // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. 37 | // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. 38 | SigningKey SigningKey 39 | 40 | // Map of signing keys to validate token with kid field usage. 41 | // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. 42 | // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. 43 | SigningKeys map[string]SigningKey 44 | 45 | // Context key to store user information from the token into context. 46 | // Optional. Default: "user". 47 | ContextKey string 48 | 49 | // Claims are extendable claims data defining token content. 50 | // Optional. Default value jwt.MapClaims 51 | Claims jwt.Claims 52 | 53 | // TokenLookup is a string in the form of ":" that is used 54 | // to extract token from the request. 55 | // Optional. Default value "header:Authorization". 56 | // Possible values: 57 | // - "header:" 58 | // - "query:" 59 | // - "param:" 60 | // - "cookie:" 61 | TokenLookup string 62 | 63 | // AuthScheme to be used in the Authorization header. 64 | // Optional. Default: "Bearer". 65 | AuthScheme string 66 | 67 | // KeyFunc is a function that supplies the public key for JWT cryptographic verification. 68 | // The function shall take care of verifying the signing algorithm and selecting the proper key. 69 | // Internally, github.com/MicahParks/keyfunc/v2 package is used project defaults. If you need more customization, 70 | // you can provide a jwt.Keyfunc using that package or make your own implementation. 71 | // 72 | // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. 73 | // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. 74 | KeyFunc jwt.Keyfunc 75 | 76 | // JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of 77 | // JWTs. Use of HTTPS is recommended. The presence of the "kid" field in the JWT header and JWKs is mandatory for 78 | // this feature. 79 | // 80 | // By default, all JWK Sets in this slice will: 81 | // * Refresh every hour. 82 | // * Refresh automatically if a new "kid" is seen in a JWT being verified. 83 | // * Rate limit refreshes to once every 5 minutes. 84 | // * Timeout refreshes after 10 seconds. 85 | // 86 | // At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey. 87 | // The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey. 88 | JWKSetURLs []string 89 | } 90 | 91 | // SigningKey holds information about the recognized cryptographic keys used to sign JWTs by this program. 92 | type SigningKey struct { 93 | // JWTAlg is the algorithm used to sign JWTs. If this value is a non-empty string, this will be checked against the 94 | // "alg" value in the JWT header. 95 | // 96 | // https://www.rfc-editor.org/rfc/rfc7518#section-3.1 97 | JWTAlg string 98 | // Key is the cryptographic key used to sign JWTs. For supported types, please see 99 | // https://github.com/golang-jwt/jwt. 100 | Key interface{} 101 | } 102 | 103 | // makeCfg function will check correctness of supplied configuration 104 | // and will complement it with default values instead of missing ones 105 | func makeCfg(config []Config) (cfg Config) { 106 | if len(config) > 0 { 107 | cfg = config[0] 108 | } 109 | if cfg.SuccessHandler == nil { 110 | cfg.SuccessHandler = func(c *fiber.Ctx) error { 111 | return c.Next() 112 | } 113 | } 114 | if cfg.ErrorHandler == nil { 115 | cfg.ErrorHandler = func(c *fiber.Ctx, err error) error { 116 | if err.Error() == "Missing or malformed JWT" { 117 | return c.Status(fiber.StatusBadRequest).SendString("Missing or malformed JWT") 118 | } 119 | return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT") 120 | } 121 | } 122 | if cfg.SigningKey.Key == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil { 123 | panic("Fiber: JWT middleware configuration: At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.") 124 | } 125 | if cfg.ContextKey == "" { 126 | cfg.ContextKey = "user" 127 | } 128 | if cfg.Claims == nil { 129 | cfg.Claims = jwt.MapClaims{} 130 | } 131 | if cfg.TokenLookup == "" { 132 | cfg.TokenLookup = defaultTokenLookup 133 | // set AuthScheme as "Bearer" only if TokenLookup is set to default. 134 | if cfg.AuthScheme == "" { 135 | cfg.AuthScheme = "Bearer" 136 | } 137 | } 138 | 139 | if cfg.KeyFunc == nil { 140 | if len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 { 141 | var givenKeys map[string]keyfunc.GivenKey 142 | if cfg.SigningKeys != nil { 143 | givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys)) 144 | for kid, key := range cfg.SigningKeys { 145 | givenKeys[kid] = keyfunc.NewGivenCustom(key, keyfunc.GivenKeyOptions{ 146 | Algorithm: key.JWTAlg, 147 | }) 148 | } 149 | } 150 | if len(cfg.JWKSetURLs) > 0 { 151 | var err error 152 | cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs) 153 | if err != nil { 154 | panic("Failed to create keyfunc from JWK Set URL: " + err.Error()) 155 | } 156 | } else { 157 | cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc 158 | } 159 | } else { 160 | cfg.KeyFunc = signingKeyFunc(cfg.SigningKey) 161 | } 162 | } 163 | 164 | return cfg 165 | } 166 | 167 | func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) { 168 | opts := keyfuncOptions(givenKeys) 169 | multiple := make(map[string]keyfunc.Options, len(jwkSetURLs)) 170 | for _, url := range jwkSetURLs { 171 | multiple[url] = opts 172 | } 173 | multiOpts := keyfunc.MultipleOptions{ 174 | KeySelector: keyfunc.KeySelectorFirst, 175 | } 176 | multi, err := keyfunc.GetMultiple(multiple, multiOpts) 177 | if err != nil { 178 | return nil, fmt.Errorf("failed to get multiple JWK Set URLs: %w", err) 179 | } 180 | return multi.Keyfunc, nil 181 | } 182 | 183 | func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options { 184 | return keyfunc.Options{ 185 | GivenKeys: givenKeys, 186 | RefreshErrorHandler: func(err error) { 187 | log.Printf("Failed to perform background refresh of JWK Set: %s.", err) 188 | }, 189 | RefreshInterval: time.Hour, 190 | RefreshRateLimit: time.Minute * 5, 191 | RefreshTimeout: time.Second * 10, 192 | RefreshUnknownKID: true, 193 | } 194 | } 195 | 196 | // getExtractors function will create a slice of functions which will be used 197 | // for token sarch and will perform extraction of the value 198 | func (cfg *Config) getExtractors() []jwtExtractor { 199 | // Initialize 200 | extractors := make([]jwtExtractor, 0) 201 | rootParts := strings.Split(cfg.TokenLookup, ",") 202 | for _, rootPart := range rootParts { 203 | parts := strings.Split(strings.TrimSpace(rootPart), ":") 204 | 205 | switch parts[0] { 206 | case "header": 207 | extractors = append(extractors, jwtFromHeader(parts[1], cfg.AuthScheme)) 208 | case "query": 209 | extractors = append(extractors, jwtFromQuery(parts[1])) 210 | case "param": 211 | extractors = append(extractors, jwtFromParam(parts[1])) 212 | case "cookie": 213 | extractors = append(extractors, jwtFromCookie(parts[1])) 214 | } 215 | } 216 | return extractors 217 | } 218 | 219 | func signingKeyFunc(key SigningKey) jwt.Keyfunc { 220 | return func(token *jwt.Token) (interface{}, error) { 221 | if key.JWTAlg != "" { 222 | alg, ok := token.Header["alg"].(string) 223 | if !ok { 224 | return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: missing or unexpected JSON type", key.JWTAlg) 225 | } 226 | if alg != key.JWTAlg { 227 | return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: %q", key.JWTAlg, alg) 228 | } 229 | } 230 | return key.Key, nil 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package jwtware_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/gofiber/fiber/v2" 12 | "github.com/gofiber/fiber/v2/utils" 13 | "github.com/golang-jwt/jwt/v5" 14 | 15 | jwtware "github.com/gofiber/jwt/v4" 16 | ) 17 | 18 | type TestToken struct { 19 | SigningMethod string 20 | Token string 21 | } 22 | 23 | var ( 24 | hamac = []TestToken{ 25 | { 26 | SigningMethod: jwtware.HS256, 27 | Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o", 28 | }, 29 | { 30 | SigningMethod: jwtware.HS384, 31 | Token: "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hO2sthNQUSfvI9ylUdMKDxcrm8jB3KL6Rtkd3FOskL-jVqYh2CK1es8FKCQO8_tW", 32 | }, 33 | { 34 | SigningMethod: jwtware.HS512, 35 | Token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.wUVS6tazE2N98_J4SH_djkEe1igXPu0qILAvVXCiO6O20gdf5vZ2sYFWX3c-Hy6L4TD47b3DSAAO9XjSqpJfag", 36 | }, 37 | } 38 | 39 | rsa = []TestToken{ 40 | { 41 | SigningMethod: jwtware.RS256, 42 | Token: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.gvWLzl1sYUXdYqAPqFYLEJYtqPce8YxrV6LPiyWX2147llj1YfquFySnC8KOUTykCAxZHe6tFkyyZOp35HOqV3P-jxW2rw05mpNhld79f-O2sAFEzV7qxJXuYi4TL-Qn1gaLWP7i9B6B9c-0xLzYUmtLdrmlM2pxfPkXwG0oSao", 43 | }, 44 | { 45 | SigningMethod: jwtware.RS384, 46 | Token: "eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.IIFu5jNRT5fIe91we3ARLTpE8hGu4tK6gsWtrJ1lAWzCxUYsVE02yOi3ya9RJsh-37GN8LdfVw74ZQzr4dwuq8SorycVatA2bc_OfkWpioOoPCqGMBFgsEdue0qtL1taflA-YSNG-Qntpqx_ciCGfI1DhiqikLaL-LSe8H9YOWk", 47 | }, 48 | { 49 | SigningMethod: jwtware.RS512, 50 | Token: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcnNhIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DKY-VXa6JJUZpupEUcmXETwaV2jfLydyeBfhSP8pIEW9g52fQ3g5hrHCNstxG2yy9yU68yrFqrBDetDX_yJ6qSHAOInwGWYot8W4D0lJvqsHJe0W0IPi03xiaWjwKO26xENCUzNNLvSPKPox5DPcg31gzCFBrIUgVX-TkpajuSE", 51 | }, 52 | } 53 | 54 | ecdsa = []TestToken{ 55 | { 56 | SigningMethod: jwtware.ES256, 57 | Token: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0yNTYifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.n6iJptkq2i6Y6gbuc92f2ExT9oXbg7hdMlR5MvkCZjayxBAyfpIGGoQAjMriwEs4rjF5F-DSU8T6eUcDxNhonA", 58 | }, 59 | { 60 | SigningMethod: jwtware.ES384, 61 | Token: "eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC0zODQifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.WYGFC6NTSzD1E3Zv7Lyy3m_1l0zoF2tZqvDBxQBXqJN-bStTBzNYnpWZDMN6XMI7OqFbPGlh_Jff4Z4dlf0bieEfenURdtpoLIQI1zPNXoIfaY7TH8BTAXQKtoBk89Ed", 62 | }, 63 | { 64 | SigningMethod: jwtware.ES512, 65 | Token: "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCIsImtpZCI6ImdvZmliZXItcC01MjEifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ADwlteggILiCM_oCkxsyJTRK6BpQyH2FBQD_Tw_ph0vpLPRrpAkyh_CZIY9uZqqpb3J_eohscCzj5Vo9jrhP9DFRAdvLZCgehLj6N8P9aro2uy9jAl7kowxe0nEErv1SrD9qlyLWJh80jJVHRBVHXXysQ2WUD0KiRBq4x1p8jdEw5vHy", 66 | }, 67 | } 68 | ) 69 | 70 | const ( 71 | defaultSigningKey = "secret" 72 | defaultKeySet = ` 73 | { 74 | "keys":[ 75 | { 76 | "e": "AQAB", 77 | "kid": "gofiber-rsa", 78 | "kty": "RSA", 79 | "n": "2IPZysef6KVySrb_RPopuwWy1C7KRfE96zQ9jIRwPghlvs0yfj9VK4rqeYbuHp5k9ghbjm1Bn2LMLR-JzqYWbchxzVrV58ay4nRHYUSjyzdbNcG0J4W-NxHnVqK0UUOl59uikRDqGHh3eRen_jVO_B8lvhqM57HQhA-czHbsmeU" 80 | }, 81 | { 82 | "crv": "P-256", 83 | "kid": "gofiber-p-256", 84 | "kty": "EC", 85 | "x": "nLZJMz-8B6p2A1-owmTrCZqZx87_Y5soNPW74dQ8EDw", 86 | "y": "RvuLyi0tS-Tcx35IMy6aL_ID0K-cJFXmkFR8t9XJ4pc" 87 | }, 88 | { 89 | "crv": "P-384", 90 | "kid": "gofiber-p-384", 91 | "kty": "EC", 92 | "x": "wvSt-v7az1qbz493ToTSvNcXgdIGqTtlcLzW7B1Ko3QWVgmtBYWQr_Q311_QX9DY", 93 | "y": "DvvBgCVjsDyttGAF8cmTP5maV46PrxACZFLvC1OEiZh-Ul0obSGXqG2xu8ulINPy" 94 | }, 95 | { 96 | "crv": "P-521", 97 | "kid": "gofiber-p-521", 98 | "kty": "EC", 99 | "x": "AZhzdsnk9Dx5fLdPDnYJOI3ClkghbyFvpSq2ExzyPNgjZz_7iBUjyyLtr6QDn9BAaeFvSQFHvhZUylIQZ9wdIinq", 100 | "y": "AC2Me0tRqydVv7d23_0xdjiDndGuk0XpSZL5jeDWQ1_Tuty28-pJrFx38QQmWnosC0lBEdOUjxq-71YP7e4TzRMR" 101 | } 102 | ] 103 | } 104 | ` 105 | ) 106 | 107 | func TestJwtFromHeader(t *testing.T) { 108 | t.Parallel() 109 | 110 | defer func() { 111 | // Assert 112 | if err := recover(); err != nil { 113 | t.Fatalf("Middleware should not panic") 114 | } 115 | }() 116 | 117 | for _, test := range hamac { 118 | // Arrange 119 | app := fiber.New() 120 | 121 | app.Use(jwtware.New(jwtware.Config{ 122 | SigningKey: jwtware.SigningKey{ 123 | JWTAlg: test.SigningMethod, 124 | Key: []byte(defaultSigningKey), 125 | }, 126 | })) 127 | 128 | app.Get("/ok", func(c *fiber.Ctx) error { 129 | return c.SendString("OK") 130 | }) 131 | 132 | req := httptest.NewRequest("GET", "/ok", nil) 133 | req.Header.Add("Authorization", "Bearer "+test.Token) 134 | 135 | // Act 136 | resp, err := app.Test(req) 137 | 138 | // Assert 139 | utils.AssertEqual(t, nil, err) 140 | utils.AssertEqual(t, 200, resp.StatusCode) 141 | } 142 | } 143 | 144 | func TestJwtFromCookie(t *testing.T) { 145 | t.Parallel() 146 | 147 | defer func() { 148 | // Assert 149 | if err := recover(); err != nil { 150 | t.Fatalf("Middleware should not panic") 151 | } 152 | }() 153 | 154 | for _, test := range hamac { 155 | // Arrange 156 | app := fiber.New() 157 | 158 | app.Use(jwtware.New(jwtware.Config{ 159 | SigningKey: jwtware.SigningKey{ 160 | JWTAlg: test.SigningMethod, 161 | Key: []byte(defaultSigningKey), 162 | }, 163 | TokenLookup: "cookie:Token", 164 | })) 165 | 166 | app.Get("/ok", func(c *fiber.Ctx) error { 167 | return c.SendString("OK") 168 | }) 169 | 170 | req := httptest.NewRequest("GET", "/ok", nil) 171 | cookie := &http.Cookie{ 172 | Name: "Token", 173 | Value: test.Token, 174 | } 175 | req.AddCookie(cookie) 176 | 177 | // Act 178 | resp, err := app.Test(req) 179 | 180 | // Assert 181 | utils.AssertEqual(t, nil, err) 182 | utils.AssertEqual(t, 200, resp.StatusCode) 183 | } 184 | } 185 | 186 | // TestJWKs performs a table test on the JWKs code. 187 | // deprecated 188 | func TestJwkFromServer(t *testing.T) { 189 | // Could add a test with an invalid JWKs endpoint. 190 | // Create a temporary directory to serve the JWKs from. 191 | tempDir, err := os.MkdirTemp("", "*") 192 | if err != nil { 193 | t.Errorf("Failed to create a temporary directory.\nError:%s\n", err.Error()) 194 | t.FailNow() 195 | } 196 | defer func() { 197 | if err = os.RemoveAll(tempDir); err != nil { 198 | t.Errorf("Failed to remove temporary directory.\nError:%s\n", err.Error()) 199 | t.FailNow() 200 | } 201 | }() 202 | 203 | // Create the JWKs file path. 204 | jwksFile := filepath.Join(tempDir, "jwks.json") 205 | 206 | // Write the empty JWKs. 207 | if err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil { 208 | t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) 209 | t.FailNow() 210 | } 211 | 212 | // Create the HTTP test server. 213 | server := httptest.NewServer(http.FileServer(http.Dir(tempDir))) 214 | defer server.Close() 215 | 216 | // Iterate through the test cases. 217 | for _, test := range append(rsa, ecdsa...) { 218 | // Arrange 219 | app := fiber.New() 220 | 221 | app.Use(jwtware.New(jwtware.Config{ 222 | JWKSetURLs: []string{server.URL + "/jwks.json"}, 223 | })) 224 | 225 | app.Get("/ok", func(c *fiber.Ctx) error { 226 | return c.SendString("OK") 227 | }) 228 | 229 | req := httptest.NewRequest("GET", "/ok", nil) 230 | req.Header.Add("Authorization", "Bearer "+test.Token) 231 | 232 | // Act 233 | resp, err := app.Test(req) 234 | 235 | // Assert 236 | utils.AssertEqual(t, nil, err) 237 | utils.AssertEqual(t, 200, resp.StatusCode) 238 | } 239 | } 240 | 241 | // TestJWKs performs a table test on the JWKs code. 242 | func TestJwkFromServers(t *testing.T) { 243 | // Could add a test with an invalid JWKs endpoint. 244 | // Create a temporary directory to serve the JWKs from. 245 | tempDir, err := os.MkdirTemp("", "*") 246 | if err != nil { 247 | t.Errorf("Failed to create a temporary directory.\nError:%s\n", err.Error()) 248 | t.FailNow() 249 | } 250 | defer func() { 251 | if err = os.RemoveAll(tempDir); err != nil { 252 | t.Errorf("Failed to remove temporary directory.\nError:%s\n", err.Error()) 253 | t.FailNow() 254 | } 255 | }() 256 | 257 | // Create the JWKs file path. 258 | jwksFile := filepath.Join(tempDir, "jwks.json") 259 | jwksFile2 := filepath.Join(tempDir, "jwks2.json") 260 | 261 | // Write the empty JWKs. 262 | if err = os.WriteFile(jwksFile, []byte(defaultKeySet), 0600); err != nil { 263 | t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) 264 | t.FailNow() 265 | } 266 | 267 | // Write the empty JWKs 2. 268 | if err = os.WriteFile(jwksFile2, []byte(defaultKeySet), 0600); err != nil { 269 | t.Errorf("Failed to write JWKs file to temporary directory.\nError:%s\n", err.Error()) 270 | t.FailNow() 271 | } 272 | 273 | // Create the HTTP test server. 274 | server := httptest.NewServer(http.FileServer(http.Dir(tempDir))) 275 | defer server.Close() 276 | 277 | // Iterate through the test cases. 278 | for _, test := range append(rsa, ecdsa...) { 279 | // Arrange 280 | app := fiber.New() 281 | 282 | app.Use(jwtware.New(jwtware.Config{ 283 | JWKSetURLs: []string{server.URL + "/jwks.json", server.URL + "/jwks2.json"}, 284 | })) 285 | 286 | app.Get("/ok", func(c *fiber.Ctx) error { 287 | return c.SendString("OK") 288 | }) 289 | 290 | req := httptest.NewRequest("GET", "/ok", nil) 291 | req.Header.Add("Authorization", "Bearer "+test.Token) 292 | 293 | // Act 294 | resp, err := app.Test(req) 295 | 296 | // Assert 297 | utils.AssertEqual(t, nil, err) 298 | utils.AssertEqual(t, 200, resp.StatusCode) 299 | } 300 | } 301 | 302 | func TestCustomKeyfunc(t *testing.T) { 303 | t.Parallel() 304 | 305 | defer func() { 306 | // Assert 307 | if err := recover(); err != nil { 308 | t.Fatalf("Middleware should not panic") 309 | } 310 | }() 311 | 312 | test := hamac[0] 313 | // Arrange 314 | app := fiber.New() 315 | 316 | app.Use(jwtware.New(jwtware.Config{ 317 | KeyFunc: customKeyfunc(), 318 | })) 319 | 320 | app.Get("/ok", func(c *fiber.Ctx) error { 321 | return c.SendString("OK") 322 | }) 323 | 324 | req := httptest.NewRequest("GET", "/ok", nil) 325 | req.Header.Add("Authorization", "Bearer "+test.Token) 326 | 327 | // Act 328 | resp, err := app.Test(req) 329 | 330 | // Assert 331 | utils.AssertEqual(t, nil, err) 332 | utils.AssertEqual(t, 200, resp.StatusCode) 333 | } 334 | 335 | func customKeyfunc() jwt.Keyfunc { 336 | return func(t *jwt.Token) (interface{}, error) { 337 | // Always check the signing method 338 | if t.Method.Alg() != jwtware.HS256 { 339 | return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) 340 | } 341 | 342 | return []byte(defaultSigningKey), nil 343 | } 344 | } 345 | --------------------------------------------------------------------------------