├── testdata ├── invalidprivkey.key ├── invalidpubkey.key ├── jwtRS256.key.pub └── jwtRS256.key ├── screenshot ├── login.png └── refresh.png ├── .github ├── dependabot.yml └── workflows │ ├── goreleaser.yml │ ├── trivy-scan.yml │ ├── go.yml │ └── codeql.yml ├── .gitignore ├── store ├── store.go ├── factory_test.go ├── factory.go ├── memory.go ├── redis.go ├── redis_test.go └── memory_test.go ├── .goreleaser.yaml ├── LICENSE ├── .golangci.yml ├── core ├── token_test.go └── store.go ├── _example ├── basic │ ├── go.mod │ ├── server.go │ └── go.sum ├── redis_store │ ├── go.mod │ ├── main.go │ └── go.sum ├── authorization │ ├── go.mod │ ├── main.go │ └── go.sum ├── token_generator │ ├── go.mod │ ├── main.go │ ├── README.md │ └── go.sum ├── redis_simple │ ├── README.md │ └── main.go └── redis_tls │ └── main.go ├── auth_jwt_redis.go ├── go.mod ├── auth_jwt_methods_test.go └── auth_jwt_redis_test.go /testdata/invalidprivkey.key: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/invalidpubkey.key: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/HEAD/screenshot/login.png -------------------------------------------------------------------------------- /screenshot/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appleboy/gin-jwt/HEAD/screenshot/refresh.png -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | // Package store provides implementations for refresh token storage 2 | package store 3 | 4 | import ( 5 | "github.com/appleboy/gin-jwt/v3/core" 6 | ) 7 | 8 | // Re-export types from core for backward compatibility 9 | type ( 10 | RefreshTokenStorer = core.TokenStore 11 | RefreshTokenData = core.RefreshTokenData 12 | ) 13 | 14 | // Re-export errors from core for backward compatibility 15 | var ( 16 | ErrRefreshTokenNotFound = core.ErrRefreshTokenNotFound 17 | ErrRefreshTokenExpired = core.ErrRefreshTokenExpired 18 | ) 19 | 20 | // Default creates a default memory-based token store 21 | // This is the recommended way to create a store with sensible defaults 22 | func Default() core.TokenStore { 23 | return NewMemoryStore() 24 | } 25 | -------------------------------------------------------------------------------- /.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@v6 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Setup go 18 | uses: actions/setup-go@v6 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: none 4 | enable: 5 | - bodyclose 6 | - dogsled 7 | - dupl 8 | - errcheck 9 | - exhaustive 10 | - goconst 11 | - gocritic 12 | - gocyclo 13 | - goprintffuncname 14 | - gosec 15 | - govet 16 | - ineffassign 17 | - misspell 18 | - nakedret 19 | - noctx 20 | - nolintlint 21 | - rowserrcheck 22 | - staticcheck 23 | - unconvert 24 | - unparam 25 | - unused 26 | - whitespace 27 | - copyloopvar 28 | - predeclared 29 | exclusions: 30 | generated: lax 31 | presets: 32 | - comments 33 | - common-false-positives 34 | - legacy 35 | - std-error-handling 36 | paths: 37 | - internal/mocks 38 | - third_party$ 39 | - builtin$ 40 | - examples$ 41 | formatters: 42 | enable: 43 | - gofmt 44 | - gofumpt 45 | - goimports 46 | - golines 47 | exclusions: 48 | generated: lax 49 | paths: 50 | - third_party$ 51 | - builtin$ 52 | - examples$ 53 | 54 | settings: 55 | gofmt: 56 | simplify: true 57 | gofumpt: 58 | # Module path which contains the source code being formatted. 59 | # Default: "" 60 | module-path: github/appleboy/go-jira 61 | # Choose whether to use the extra rules. 62 | # Default: false 63 | extra-rules: true 64 | -------------------------------------------------------------------------------- /.github/workflows/trivy-scan.yml: -------------------------------------------------------------------------------- 1 | name: Trivy Security Scan 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | # Run daily at 00:00 UTC 12 | - cron: "0 0 * * *" 13 | workflow_dispatch: # Allow manual trigger 14 | 15 | permissions: 16 | contents: read 17 | security-events: write # Required for uploading SARIF results 18 | 19 | jobs: 20 | trivy-scan: 21 | name: Trivy Security Scan 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v6 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Run Trivy vulnerability scanner (source code) 30 | uses: aquasecurity/trivy-action@0.33.1 31 | with: 32 | scan-type: "fs" 33 | scan-ref: "." 34 | scanners: "vuln,secret,misconfig" 35 | format: "sarif" 36 | output: "trivy-results.sarif" 37 | severity: "CRITICAL,HIGH,MEDIUM" 38 | ignore-unfixed: true 39 | 40 | - name: Upload Trivy results to GitHub Security tab 41 | uses: github/codeql-action/upload-sarif@v4 42 | if: always() 43 | with: 44 | sarif_file: "trivy-results.sarif" 45 | 46 | - name: Run Trivy scanner (table output for logs) 47 | uses: aquasecurity/trivy-action@0.33.1 48 | if: always() 49 | with: 50 | scan-type: "fs" 51 | scan-ref: "." 52 | scanners: "vuln,secret,misconfig" 53 | format: "table" 54 | severity: "CRITICAL,HIGH,MEDIUM" 55 | ignore-unfixed: true 56 | exit-code: "1" 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /store/factory_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFactory_CreateStore_Memory(t *testing.T) { 10 | factory := NewFactory() 11 | 12 | config := NewMemoryConfig() 13 | store, err := factory.CreateStore(config) 14 | 15 | assert.NoError(t, err) 16 | assert.NotNil(t, store) 17 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 18 | } 19 | 20 | func TestFactory_CreateStore_DefaultConfig(t *testing.T) { 21 | factory := NewFactory() 22 | 23 | store, err := factory.CreateStore(nil) 24 | 25 | assert.NoError(t, err) 26 | assert.NotNil(t, store) 27 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 28 | } 29 | 30 | func TestNewStore_Memory(t *testing.T) { 31 | config := NewMemoryConfig() 32 | store, err := NewStore(config) 33 | 34 | assert.NoError(t, err) 35 | assert.NotNil(t, store) 36 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 37 | } 38 | 39 | func TestNewMemoryStore(t *testing.T) { 40 | store := NewMemoryStore() 41 | 42 | assert.NotNil(t, store) 43 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 44 | } 45 | 46 | func TestMustNewMemoryStore(t *testing.T) { 47 | store := MustNewMemoryStore() 48 | 49 | assert.NotNil(t, store) 50 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 51 | } 52 | 53 | func TestDefault(t *testing.T) { 54 | store := Default() 55 | 56 | assert.NotNil(t, store) 57 | assert.IsType(t, &InMemoryRefreshTokenStore{}, store) 58 | } 59 | 60 | func TestFactory_CreateStore_UnsupportedType(t *testing.T) { 61 | factory := NewFactory() 62 | 63 | config := &Config{ 64 | Type: "unsupported", 65 | } 66 | 67 | store, err := factory.CreateStore(config) 68 | 69 | assert.Error(t, err) 70 | assert.Nil(t, store) 71 | assert.Contains(t, err.Error(), "unsupported store type") 72 | } 73 | -------------------------------------------------------------------------------- /core/token_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestToken(t *testing.T) { 11 | now := time.Now() 12 | expiresAt := now.Add(time.Hour).Unix() 13 | createdAt := now.Unix() 14 | 15 | token := &Token{ 16 | AccessToken: "test.access.token", 17 | TokenType: "Bearer", 18 | RefreshToken: "test-refresh-token", 19 | ExpiresAt: expiresAt, 20 | CreatedAt: createdAt, 21 | } 22 | 23 | // Test basic fields 24 | assert.Equal(t, "test.access.token", token.AccessToken) 25 | assert.Equal(t, "Bearer", token.TokenType) 26 | assert.Equal(t, "test-refresh-token", token.RefreshToken) 27 | assert.Equal(t, expiresAt, token.ExpiresAt) 28 | assert.Equal(t, createdAt, token.CreatedAt) 29 | 30 | // Test ExpiresIn method 31 | expiresIn := token.ExpiresIn() 32 | assert.True(t, expiresIn > 3500) // Should be close to 3600 (1 hour) 33 | assert.True(t, expiresIn <= 3600) 34 | } 35 | 36 | func TestTokenExpiresIn(t *testing.T) { 37 | testCases := []struct { 38 | name string 39 | expiresAt int64 40 | expected int64 41 | }{ 42 | { 43 | name: "Future expiry", 44 | expiresAt: time.Now().Add(30 * time.Minute).Unix(), 45 | expected: 30 * 60, // approximately 30 minutes 46 | }, 47 | { 48 | name: "Past expiry", 49 | expiresAt: time.Now().Add(-30 * time.Minute).Unix(), 50 | expected: -30 * 60, // approximately -30 minutes 51 | }, 52 | } 53 | 54 | for _, tc := range testCases { 55 | t.Run(tc.name, func(t *testing.T) { 56 | token := &Token{ExpiresAt: tc.expiresAt} 57 | expiresIn := token.ExpiresIn() 58 | 59 | // Allow for some time difference due to test execution 60 | diff := expiresIn - tc.expected 61 | assert.True( 62 | t, 63 | diff >= -5 && diff <= 5, 64 | "ExpiresIn should be within 5 seconds of expected", 65 | ) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /_example/basic/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt/v3 v3.0.0 7 | github.com/gin-gonic/gin v1.11.0 8 | github.com/golang-jwt/jwt/v5 v5.3.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/gopkg v0.1.3 // indirect 13 | github.com/bytedance/sonic v1.14.1 // indirect 14 | github.com/bytedance/sonic/loader v0.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.6 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 17 | github.com/gin-contrib/sse v1.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.28.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/goccy/go-yaml v1.18.0 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/quic-go/qpack v0.6.0 // indirect 31 | github.com/quic-go/quic-go v0.57.1 // indirect 32 | github.com/redis/rueidis v1.0.66 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.3.0 // indirect 35 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 36 | golang.org/x/arch v0.22.0 // indirect 37 | golang.org/x/crypto v0.45.0 // indirect 38 | golang.org/x/net v0.47.0 // indirect 39 | golang.org/x/sys v0.38.0 // indirect 40 | golang.org/x/text v0.31.0 // indirect 41 | google.golang.org/protobuf v1.36.10 // indirect 42 | ) 43 | 44 | replace github.com/appleboy/gin-jwt/v3 => ../../ 45 | -------------------------------------------------------------------------------- /_example/redis_store/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.25.1 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt/v3 v3.0.0 7 | github.com/gin-gonic/gin v1.11.0 8 | github.com/golang-jwt/jwt/v5 v5.3.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/gopkg v0.1.3 // indirect 13 | github.com/bytedance/sonic v1.14.1 // indirect 14 | github.com/bytedance/sonic/loader v0.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.6 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 17 | github.com/gin-contrib/sse v1.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.28.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/goccy/go-yaml v1.18.0 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/quic-go/qpack v0.6.0 // indirect 31 | github.com/quic-go/quic-go v0.57.1 // indirect 32 | github.com/redis/rueidis v1.0.66 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.3.0 // indirect 35 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 36 | golang.org/x/arch v0.22.0 // indirect 37 | golang.org/x/crypto v0.45.0 // indirect 38 | golang.org/x/net v0.47.0 // indirect 39 | golang.org/x/sys v0.38.0 // indirect 40 | golang.org/x/text v0.31.0 // indirect 41 | google.golang.org/protobuf v1.36.10 // indirect 42 | ) 43 | 44 | replace github.com/appleboy/gin-jwt/v3 => ../../ 45 | -------------------------------------------------------------------------------- /_example/authorization/go.mod: -------------------------------------------------------------------------------- 1 | module authorization-example 2 | 3 | go 1.25.1 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt/v3 v3.0.0 7 | github.com/gin-gonic/gin v1.11.0 8 | github.com/golang-jwt/jwt/v5 v5.3.0 9 | ) 10 | 11 | require ( 12 | github.com/bytedance/gopkg v0.1.3 // indirect 13 | github.com/bytedance/sonic v1.14.1 // indirect 14 | github.com/bytedance/sonic/loader v0.3.0 // indirect 15 | github.com/cloudwego/base64x v0.1.6 // indirect 16 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 17 | github.com/gin-contrib/sse v1.1.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.28.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/goccy/go-yaml v1.18.0 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/quic-go/qpack v0.6.0 // indirect 31 | github.com/quic-go/quic-go v0.57.1 // indirect 32 | github.com/redis/rueidis v1.0.66 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.3.0 // indirect 35 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 36 | golang.org/x/arch v0.22.0 // indirect 37 | golang.org/x/crypto v0.45.0 // indirect 38 | golang.org/x/net v0.47.0 // indirect 39 | golang.org/x/sys v0.38.0 // indirect 40 | golang.org/x/text v0.31.0 // indirect 41 | google.golang.org/protobuf v1.36.10 // indirect 42 | ) 43 | 44 | replace github.com/appleboy/gin-jwt/v3 => ../../ 45 | -------------------------------------------------------------------------------- /.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@v6 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Setup go 21 | uses: actions/setup-go@v6 22 | with: 23 | go-version-file: go.mod 24 | check-latest: true 25 | - name: Setup golangci-lint 26 | uses: golangci/golangci-lint-action@v9 27 | with: 28 | version: v2.6 29 | args: --verbose 30 | test: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | go: [1.24, 1.25] 35 | include: 36 | - os: ubuntu-latest 37 | go-build: ~/.cache/go-build 38 | name: ${{ matrix.os }} @ Go ${{ matrix.go }} 39 | runs-on: ${{ matrix.os }} 40 | env: 41 | GO111MODULE: on 42 | GOPROXY: https://proxy.golang.org 43 | steps: 44 | - name: Checkout Code 45 | uses: actions/checkout@v6 46 | with: 47 | ref: ${{ github.ref }} 48 | 49 | - name: Set up Go ${{ matrix.go }} 50 | uses: actions/setup-go@v6 51 | with: 52 | go-version: ${{ matrix.go }} 53 | 54 | - uses: actions/cache@v4 55 | with: 56 | path: | 57 | ${{ matrix.go-build }} 58 | ~/go/pkg/mod 59 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 60 | restore-keys: | 61 | ${{ runner.os }}-go- 62 | - name: Run Tests 63 | run: | 64 | go test -v -covermode=atomic -coverprofile=coverage.out 65 | 66 | - name: Upload coverage to Codecov 67 | uses: codecov/codecov-action@v5 68 | with: 69 | flags: ${{ matrix.os }},go-${{ matrix.go }} 70 | -------------------------------------------------------------------------------- /_example/token_generator/go.mod: -------------------------------------------------------------------------------- 1 | module token_generator_example 2 | 3 | go 1.25.1 4 | 5 | require ( 6 | github.com/appleboy/gin-jwt/v3 v3.0.0 7 | github.com/golang-jwt/jwt/v5 v5.3.0 8 | ) 9 | 10 | require ( 11 | github.com/bytedance/gopkg v0.1.3 // indirect 12 | github.com/bytedance/sonic v1.14.1 // indirect 13 | github.com/bytedance/sonic/loader v0.3.0 // indirect 14 | github.com/cloudwego/base64x v0.1.6 // indirect 15 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 16 | github.com/gin-contrib/sse v1.1.0 // indirect 17 | github.com/gin-gonic/gin v1.11.0 // indirect 18 | github.com/go-playground/locales v0.14.1 // indirect 19 | github.com/go-playground/universal-translator v0.18.1 // indirect 20 | github.com/go-playground/validator/v10 v10.28.0 // indirect 21 | github.com/goccy/go-json v0.10.5 // indirect 22 | github.com/goccy/go-yaml v1.18.0 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 25 | github.com/leodido/go-urn v1.4.0 // indirect 26 | github.com/mattn/go-isatty v0.0.20 // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 30 | github.com/quic-go/qpack v0.6.0 // indirect 31 | github.com/quic-go/quic-go v0.57.1 // indirect 32 | github.com/redis/rueidis v1.0.66 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.3.0 // indirect 35 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 36 | golang.org/x/arch v0.22.0 // indirect 37 | golang.org/x/crypto v0.45.0 // indirect 38 | golang.org/x/net v0.47.0 // indirect 39 | golang.org/x/sys v0.38.0 // indirect 40 | golang.org/x/text v0.31.0 // indirect 41 | google.golang.org/protobuf v1.36.10 // indirect 42 | ) 43 | 44 | replace github.com/appleboy/gin-jwt/v3 => ../../ 45 | -------------------------------------------------------------------------------- /.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@v6 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v4 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@v4 55 | -------------------------------------------------------------------------------- /core/store.go: -------------------------------------------------------------------------------- 1 | // Package core provides core interfaces and types for gin-jwt 2 | package core 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // ErrRefreshTokenNotFound indicates the refresh token was not found in storage 12 | ErrRefreshTokenNotFound = errors.New("refresh token not found") 13 | 14 | // ErrRefreshTokenExpired indicates the refresh token has expired 15 | ErrRefreshTokenExpired = errors.New("refresh token expired") 16 | ) 17 | 18 | // TokenStore defines the interface for storing and retrieving refresh tokens 19 | type TokenStore interface { 20 | // Set stores a refresh token with associated user data and expiration 21 | // Returns an error if the operation fails 22 | Set(ctx context.Context, token string, userData any, expiry time.Time) error 23 | 24 | // Get retrieves user data associated with a refresh token 25 | // Returns ErrRefreshTokenNotFound if token doesn't exist or is expired 26 | Get(ctx context.Context, token string) (any, error) 27 | 28 | // Delete removes a refresh token from storage 29 | // Returns an error if the operation fails, but should not error if token doesn't exist 30 | Delete(ctx context.Context, token string) error 31 | 32 | // Cleanup removes expired tokens (optional, for cleanup routines) 33 | // Returns the number of tokens cleaned up and any error encountered 34 | Cleanup(ctx context.Context) (int, error) 35 | 36 | // Count returns the total number of active refresh tokens 37 | // Useful for monitoring and debugging 38 | Count(ctx context.Context) (int, error) 39 | } 40 | 41 | // RefreshTokenData holds the data stored with each refresh token 42 | type RefreshTokenData struct { 43 | UserData any `json:"user_data"` 44 | Expiry time.Time `json:"expiry"` 45 | Created time.Time `json:"created"` 46 | } 47 | 48 | // IsExpired checks if the token data has expired 49 | func (r *RefreshTokenData) IsExpired() bool { 50 | return time.Now().After(r.Expiry) 51 | } 52 | 53 | // Token represents a complete JWT token pair with metadata 54 | type Token struct { 55 | AccessToken string `json:"access_token"` 56 | TokenType string `json:"token_type"` 57 | RefreshToken string `json:"refresh_token,omitempty"` 58 | ExpiresAt int64 `json:"expires_at"` 59 | CreatedAt int64 `json:"created_at"` 60 | } 61 | 62 | // ExpiresIn returns the number of seconds until the access token expires 63 | func (t *Token) ExpiresIn() int64 { 64 | return t.ExpiresAt - time.Now().Unix() 65 | } 66 | -------------------------------------------------------------------------------- /_example/token_generator/main.go: -------------------------------------------------------------------------------- 1 | // Example demonstrating the TokenGenerator functionality 2 | package main 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | jwt "github.com/appleboy/gin-jwt/v3" 11 | gojwt "github.com/golang-jwt/jwt/v5" 12 | ) 13 | 14 | func main() { 15 | // Initialize the middleware 16 | authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ 17 | Realm: "example zone", 18 | Key: []byte("secret key"), 19 | Timeout: time.Hour, 20 | MaxRefresh: time.Hour * 24, 21 | PayloadFunc: func(data any) gojwt.MapClaims { 22 | return gojwt.MapClaims{ 23 | "user_id": data, 24 | } 25 | }, 26 | }) 27 | if err != nil { 28 | log.Fatal("JWT Error:" + err.Error()) 29 | } 30 | 31 | // Example user data 32 | userData := "user123" 33 | 34 | // Create context for token operations 35 | ctx := context.Background() 36 | 37 | // Generate a complete token pair (access + refresh tokens) 38 | fmt.Println("=== Generating Token Pair ===") 39 | tokenPair, err := authMiddleware.TokenGenerator(ctx, userData) 40 | if err != nil { 41 | log.Fatal("Failed to generate token pair:", err) 42 | } 43 | 44 | fmt.Printf("Access Token: %s\n", tokenPair.AccessToken[:50]+"...") 45 | fmt.Printf("Token Type: %s\n", tokenPair.TokenType) 46 | fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken) 47 | fmt.Printf("Expires At: %d (%s)\n", tokenPair.ExpiresAt, time.Unix(tokenPair.ExpiresAt, 0)) 48 | fmt.Printf("Created At: %d (%s)\n", tokenPair.CreatedAt, time.Unix(tokenPair.CreatedAt, 0)) 49 | fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn()) 50 | 51 | // Simulate refresh token usage 52 | fmt.Println("\n=== Refreshing Token Pair ===") 53 | newTokenPair, err := authMiddleware.TokenGeneratorWithRevocation( 54 | ctx, 55 | userData, 56 | tokenPair.RefreshToken, 57 | ) 58 | if err != nil { 59 | log.Fatal("Failed to refresh token pair:", err) 60 | } 61 | 62 | fmt.Printf("New Access Token: %s\n", newTokenPair.AccessToken[:50]+"...") 63 | fmt.Printf("New Refresh Token: %s\n", newTokenPair.RefreshToken) 64 | fmt.Printf( 65 | "Old refresh token revoked: %t\n", 66 | tokenPair.RefreshToken != newTokenPair.RefreshToken, 67 | ) 68 | 69 | // Verify old refresh token is invalid 70 | fmt.Println("\n=== Verifying Old Token Revocation ===") 71 | _, err = authMiddleware.TokenGeneratorWithRevocation(ctx, userData, tokenPair.RefreshToken) 72 | if err != nil { 73 | fmt.Printf("Old refresh token correctly rejected: %s\n", err) 74 | } 75 | 76 | fmt.Println("\n=== Token Generation Complete! ===") 77 | fmt.Println("You can now use these tokens without needing middleware handlers!") 78 | } 79 | -------------------------------------------------------------------------------- /auth_jwt_redis.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "crypto/tls" 5 | "log" 6 | "time" 7 | 8 | "github.com/appleboy/gin-jwt/v3/store" 9 | ) 10 | 11 | // RedisOption defines a function type for configuring Redis store 12 | type RedisOption func(*store.RedisConfig) 13 | 14 | // WithRedisAddr sets the Redis server address 15 | func WithRedisAddr(addr string) RedisOption { 16 | return func(config *store.RedisConfig) { 17 | config.Addr = addr 18 | } 19 | } 20 | 21 | // WithRedisAuth sets Redis authentication 22 | func WithRedisAuth(password string, db int) RedisOption { 23 | return func(config *store.RedisConfig) { 24 | config.Password = password 25 | config.DB = db 26 | } 27 | } 28 | 29 | // WithRedisCache configures client-side cache 30 | func WithRedisCache(size int, ttl time.Duration) RedisOption { 31 | return func(config *store.RedisConfig) { 32 | config.CacheSize = size 33 | config.CacheTTL = ttl 34 | } 35 | } 36 | 37 | // WithRedisPool configures connection pool 38 | func WithRedisPool(poolSize int, maxIdleTime, maxLifetime time.Duration) RedisOption { 39 | return func(config *store.RedisConfig) { 40 | config.PoolSize = poolSize 41 | config.ConnMaxIdleTime = maxIdleTime 42 | config.ConnMaxLifetime = maxLifetime 43 | } 44 | } 45 | 46 | // WithRedisKeyPrefix sets the key prefix 47 | func WithRedisKeyPrefix(prefix string) RedisOption { 48 | return func(config *store.RedisConfig) { 49 | config.KeyPrefix = prefix 50 | } 51 | } 52 | 53 | // WithRedisTLS sets the TLS configuration for secure connections 54 | func WithRedisTLS(tlsConfig *tls.Config) RedisOption { 55 | return func(config *store.RedisConfig) { 56 | config.TLSConfig = tlsConfig 57 | } 58 | } 59 | 60 | // EnableRedisStore enables Redis store with optional configuration 61 | func (mw *GinJWTMiddleware) EnableRedisStore(opts ...RedisOption) *GinJWTMiddleware { 62 | mw.UseRedisStore = true 63 | 64 | // Start with default config 65 | config := store.DefaultRedisConfig() 66 | 67 | // Apply all options 68 | for _, opt := range opts { 69 | opt(config) 70 | } 71 | 72 | mw.RedisConfig = config 73 | return mw 74 | } 75 | 76 | // initializeRedisStore attempts to create and initialize Redis store 77 | // Falls back to in-memory store if Redis connection fails 78 | func (mw *GinJWTMiddleware) initializeRedisStore() { 79 | if mw.UseRedisStore { 80 | // Try to create Redis store 81 | redisConfig := mw.RedisConfig 82 | if redisConfig == nil { 83 | redisConfig = store.DefaultRedisConfig() 84 | } 85 | 86 | redisStore, err := store.NewRedisRefreshTokenStore(redisConfig) 87 | if err != nil { 88 | // Fallback to in-memory store 89 | log.Printf("Failed to connect to Redis: %v, falling back to in-memory store", err) 90 | mw.RefreshTokenStore = mw.inMemoryStore 91 | } else { 92 | log.Println("Successfully connected to Redis store with client-side cache enabled") 93 | mw.RefreshTokenStore = redisStore 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /_example/token_generator/README.md: -------------------------------------------------------------------------------- 1 | # Token Generator Example 2 | 3 | This example demonstrates how to use the `TokenGenerator` functionality to create JWT tokens directly without using HTTP middleware handlers. 4 | 5 | ## Features 6 | 7 | - **Direct Token Generation**: Generate complete token pairs (access + refresh) programmatically 8 | - **Refresh Token Management**: Handle refresh token rotation and revocation 9 | - **RFC 6749 Compliant**: Follows OAuth 2.0 standards for token management 10 | - **No HTTP Required**: Generate tokens without needing HTTP requests 11 | 12 | ## Usage 13 | 14 | Run the example: 15 | 16 | ```bash 17 | cd _example/token_generator 18 | go run main.go 19 | ``` 20 | 21 | ## Key Methods 22 | 23 | ### `TokenGenerator(userData any) (*core.Token, error)` 24 | 25 | Generates a complete token pair containing: 26 | 27 | - Access token (JWT) 28 | - Refresh token (opaque) 29 | - Token metadata (expiry, creation time, etc.) 30 | 31 | ```go 32 | tokenPair, err := authMiddleware.TokenGenerator(userData) 33 | if err != nil { 34 | log.Fatal("Failed to generate token pair:", err) 35 | } 36 | 37 | fmt.Printf("Access Token: %s\n", tokenPair.AccessToken) 38 | fmt.Printf("Refresh Token: %s\n", tokenPair.RefreshToken) 39 | fmt.Printf("Expires In: %d seconds\n", tokenPair.ExpiresIn()) 40 | ``` 41 | 42 | ### `TokenGeneratorWithRevocation(userData any, oldRefreshToken string) (*core.Token, error)` 43 | 44 | Generates a new token pair and automatically revokes the old refresh token: 45 | 46 | ```go 47 | newTokenPair, err := authMiddleware.TokenGeneratorWithRevocation(userData, oldRefreshToken) 48 | if err != nil { 49 | log.Fatal("Failed to refresh token pair:", err) 50 | } 51 | ``` 52 | 53 | ## Token Structure 54 | 55 | The `core.Token` struct contains: 56 | 57 | ```go 58 | type Token struct { 59 | AccessToken string `json:"access_token"` // JWT access token 60 | TokenType string `json:"token_type"` // Always "Bearer" 61 | RefreshToken string `json:"refresh_token"` // Opaque refresh token 62 | ExpiresAt int64 `json:"expires_at"` // Unix timestamp 63 | CreatedAt int64 `json:"created_at"` // Unix timestamp 64 | } 65 | ``` 66 | 67 | ### Utility Methods 68 | 69 | - `ExpiresIn()` - Returns seconds until token expires 70 | - Server-side refresh token storage and validation 71 | - Automatic token rotation on refresh 72 | 73 | ## Use Cases 74 | 75 | 1. **Programmatic Authentication**: Generate tokens for service-to-service communication 76 | 2. **Testing**: Create tokens for testing authenticated endpoints 77 | 3. **Registration Flow**: Issue tokens immediately after user registration 78 | 4. **Background Jobs**: Generate tokens for background processing 79 | 5. **Custom Authentication**: Build custom authentication flows 80 | 81 | ## Security Features 82 | 83 | - **Refresh Token Rotation**: Old tokens are automatically revoked 84 | - **Server-side Storage**: Refresh tokens are stored securely server-side 85 | - **Expiry Management**: Both access and refresh tokens have proper expiry 86 | - **RFC 6749 Compliance**: Follows OAuth 2.0 security standards 87 | -------------------------------------------------------------------------------- /_example/redis_simple/README.md: -------------------------------------------------------------------------------- 1 | # Redis Store Example 2 | 3 | This example demonstrates how to use Redis store with gin-jwt middleware using the new convenience methods. 4 | 5 | ## Features 6 | 7 | - **Redis Integration**: Use Redis as the backend store for refresh tokens 8 | - **Client-side Caching**: Built-in client-side caching for improved performance 9 | - **Automatic Fallback**: Falls back to in-memory store if Redis connection fails 10 | - **Easy Configuration**: Simple methods to configure Redis store 11 | 12 | ## Usage 13 | 14 | ### Method 1: Enable Redis with Default Configuration 15 | 16 | ```go 17 | middleware := &jwt.GinJWTMiddleware{ 18 | // ... other configuration 19 | UseRedisStore: true, 20 | } 21 | 22 | // Or using convenience method 23 | middleware.EnableRedisStore() 24 | ``` 25 | 26 | ### Method 2: Enable Redis with Custom Address 27 | 28 | ```go 29 | middleware.EnableRedisStoreWithAddr("localhost:6379") 30 | ``` 31 | 32 | ### Method 3: Enable Redis with Full Options 33 | 34 | ```go 35 | middleware.EnableRedisStoreWithOptions("localhost:6379", "password", 0) 36 | ``` 37 | 38 | ### Method 4: Enable Redis with Custom Configuration 39 | 40 | ```go 41 | config := store.DefaultRedisConfig() 42 | config.Addr = "localhost:6379" 43 | config.CacheSize = 128 * 1024 * 1024 // 128MB 44 | middleware.EnableRedisStoreWithConfig(config) 45 | ``` 46 | 47 | ### Method 5: Configure Client-side Cache 48 | 49 | ```go 50 | middleware.SetRedisClientSideCache(64*1024*1024, 30*time.Second) // 64MB cache, 30s TTL 51 | ``` 52 | 53 | ## Running the Example 54 | 55 | 1. Start Redis server (optional - will fall back to memory store if not available): 56 | 57 | ```bash 58 | redis-server 59 | ``` 60 | 61 | 2. Run the example: 62 | 63 | ```bash 64 | go run main.go 65 | ``` 66 | 67 | 3. Test the endpoints: 68 | 69 | ```bash 70 | # Login 71 | curl -X POST localhost:8000/login -d '{"username": "admin", "password": "admin"}' -H "Content-Type: application/json" 72 | 73 | # Use the returned token in subsequent requests 74 | curl -H "Authorization: Bearer YOUR_TOKEN" localhost:8000/auth/hello 75 | 76 | # Refresh token 77 | curl -X GET localhost:8000/auth/refresh_token -d '{"refresh_token": "YOUR_REFRESH_TOKEN"}' -H "Content-Type: application/json" 78 | ``` 79 | 80 | ## Configuration Options 81 | 82 | ### RedisConfig 83 | 84 | - `Addr`: Redis server address (default: "localhost:6379") 85 | - `Password`: Redis password (default: "") 86 | - `DB`: Redis database number (default: 0) 87 | - `CacheSize`: Client-side cache size in bytes (default: 128MB) 88 | - `CacheTTL`: Client-side cache TTL (default: 1 minute) 89 | - `KeyPrefix`: Prefix for all Redis keys (default: "gin-jwt:") 90 | 91 | ### Fallback Behavior 92 | 93 | If Redis connection fails during initialization: 94 | 95 | - The middleware logs an error message 96 | - Automatically falls back to in-memory store 97 | - Application continues to function normally 98 | 99 | This ensures high availability and prevents application failures due to Redis connectivity issues. 100 | -------------------------------------------------------------------------------- /store/factory.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/appleboy/gin-jwt/v3/core" 7 | ) 8 | 9 | // StoreType represents the type of store to create 10 | type StoreType string 11 | 12 | const ( 13 | // MemoryStore represents an in-memory token store 14 | MemoryStore StoreType = "memory" 15 | // RedisStore represents a Redis-based token store 16 | RedisStore StoreType = "redis" 17 | ) 18 | 19 | // Config holds the configuration for creating a token store 20 | type Config struct { 21 | Type StoreType // Type of store to create (memory or redis) 22 | Redis *RedisConfig // Redis configuration (only used when Type is RedisStore) 23 | } 24 | 25 | // DefaultConfig returns a default configuration with memory store 26 | func DefaultConfig() *Config { 27 | return &Config{ 28 | Type: MemoryStore, 29 | Redis: nil, 30 | } 31 | } 32 | 33 | // NewMemoryConfig creates a configuration for memory store 34 | func NewMemoryConfig() *Config { 35 | return &Config{ 36 | Type: MemoryStore, 37 | Redis: nil, 38 | } 39 | } 40 | 41 | // NewRedisConfig creates a configuration for Redis store 42 | func NewRedisConfig(redisConfig *RedisConfig) *Config { 43 | if redisConfig == nil { 44 | redisConfig = DefaultRedisConfig() 45 | } 46 | return &Config{ 47 | Type: RedisStore, 48 | Redis: redisConfig, 49 | } 50 | } 51 | 52 | // Factory provides methods to create different types of token stores 53 | type Factory struct{} 54 | 55 | // NewFactory creates a new store factory 56 | func NewFactory() *Factory { 57 | return &Factory{} 58 | } 59 | 60 | // CreateStore creates a token store based on the provided configuration 61 | func (f *Factory) CreateStore(config *Config) (core.TokenStore, error) { 62 | if config == nil { 63 | config = DefaultConfig() 64 | } 65 | 66 | switch config.Type { 67 | case MemoryStore: 68 | return NewInMemoryRefreshTokenStore(), nil 69 | 70 | case RedisStore: 71 | redisConfig := config.Redis 72 | if redisConfig == nil { 73 | redisConfig = DefaultRedisConfig() 74 | } 75 | return NewRedisRefreshTokenStore(redisConfig) 76 | 77 | default: 78 | return nil, fmt.Errorf("unsupported store type: %s", config.Type) 79 | } 80 | } 81 | 82 | // Convenience functions for creating stores 83 | 84 | // NewStore creates a token store with the given configuration 85 | func NewStore(config *Config) (core.TokenStore, error) { 86 | factory := NewFactory() 87 | return factory.CreateStore(config) 88 | } 89 | 90 | // NewMemoryStore creates a new in-memory token store 91 | func NewMemoryStore() core.TokenStore { 92 | return NewInMemoryRefreshTokenStore() 93 | } 94 | 95 | // NewRedisStore creates a new Redis token store with the given configuration 96 | func NewRedisStore(config *RedisConfig) (core.TokenStore, error) { 97 | return NewRedisRefreshTokenStore(config) 98 | } 99 | 100 | // MustNewStore creates a token store with the given configuration and panics on error 101 | func MustNewStore(config *Config) core.TokenStore { 102 | store, err := NewStore(config) 103 | if err != nil { 104 | panic(fmt.Sprintf("failed to create token store: %v", err)) 105 | } 106 | return store 107 | } 108 | 109 | // MustNewMemoryStore creates a new in-memory token store (never fails) 110 | func MustNewMemoryStore() core.TokenStore { 111 | return NewMemoryStore() 112 | } 113 | 114 | // MustNewRedisStore creates a new Redis token store and panics on error 115 | func MustNewRedisStore(config *RedisConfig) core.TokenStore { 116 | store, err := NewRedisStore(config) 117 | if err != nil { 118 | panic(fmt.Sprintf("failed to create Redis token store: %v", err)) 119 | } 120 | return store 121 | } 122 | -------------------------------------------------------------------------------- /store/memory.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "time" 8 | 9 | "github.com/appleboy/gin-jwt/v3/core" 10 | ) 11 | 12 | var _ core.TokenStore = &InMemoryRefreshTokenStore{} 13 | 14 | // InMemoryRefreshTokenStore provides a simple in-memory refresh token store 15 | // This implementation is thread-safe and suitable for single-instance applications 16 | // For distributed systems, consider using Redis or database-based implementations 17 | type InMemoryRefreshTokenStore struct { 18 | tokens map[string]*core.RefreshTokenData 19 | mu sync.RWMutex 20 | } 21 | 22 | // NewInMemoryRefreshTokenStore creates a new in-memory refresh token store 23 | func NewInMemoryRefreshTokenStore() *InMemoryRefreshTokenStore { 24 | return &InMemoryRefreshTokenStore{ 25 | tokens: make(map[string]*core.RefreshTokenData), 26 | } 27 | } 28 | 29 | // Set stores a refresh token with associated user data and expiration 30 | func (s *InMemoryRefreshTokenStore) Set( 31 | ctx context.Context, 32 | token string, 33 | userData any, 34 | expiry time.Time, 35 | ) error { 36 | if token == "" { 37 | return errors.New("token cannot be empty") 38 | } 39 | 40 | s.mu.Lock() 41 | defer s.mu.Unlock() 42 | 43 | s.tokens[token] = &core.RefreshTokenData{ 44 | UserData: userData, 45 | Expiry: expiry, 46 | Created: time.Now(), 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // Get retrieves user data associated with a refresh token 53 | func (s *InMemoryRefreshTokenStore) Get(ctx context.Context, token string) (any, error) { 54 | if token == "" { 55 | return nil, ErrRefreshTokenNotFound 56 | } 57 | 58 | s.mu.RLock() 59 | data, exists := s.tokens[token] 60 | s.mu.RUnlock() 61 | 62 | if !exists { 63 | return nil, core.ErrRefreshTokenNotFound 64 | } 65 | 66 | if data.IsExpired() { 67 | // Clean up expired token 68 | s.mu.Lock() 69 | delete(s.tokens, token) 70 | s.mu.Unlock() 71 | return nil, core.ErrRefreshTokenNotFound 72 | } 73 | 74 | return data.UserData, nil 75 | } 76 | 77 | // Delete removes a refresh token from storage 78 | func (s *InMemoryRefreshTokenStore) Delete(ctx context.Context, token string) error { 79 | if token == "" { 80 | return nil // No error for empty token deletion 81 | } 82 | 83 | s.mu.Lock() 84 | defer s.mu.Unlock() 85 | 86 | delete(s.tokens, token) 87 | return nil 88 | } 89 | 90 | // Cleanup removes expired tokens and returns the number of tokens cleaned up 91 | func (s *InMemoryRefreshTokenStore) Cleanup(ctx context.Context) (int, error) { 92 | s.mu.Lock() 93 | defer s.mu.Unlock() 94 | 95 | var cleaned int 96 | now := time.Now() 97 | 98 | for token, data := range s.tokens { 99 | if now.After(data.Expiry) { 100 | delete(s.tokens, token) 101 | cleaned++ 102 | } 103 | } 104 | 105 | return cleaned, nil 106 | } 107 | 108 | // Count returns the total number of active refresh tokens 109 | func (s *InMemoryRefreshTokenStore) Count(ctx context.Context) (int, error) { 110 | s.mu.RLock() 111 | defer s.mu.RUnlock() 112 | 113 | return len(s.tokens), nil 114 | } 115 | 116 | // GetAll returns all active tokens (for debugging/monitoring purposes) 117 | // Note: This method is not part of the RefreshTokenStorer interface 118 | // and should be used carefully in production environments 119 | func (s *InMemoryRefreshTokenStore) GetAll() map[string]*core.RefreshTokenData { 120 | s.mu.RLock() 121 | defer s.mu.RUnlock() 122 | 123 | // Create a copy to prevent external modifications 124 | result := make(map[string]*core.RefreshTokenData) 125 | for token, data := range s.tokens { 126 | if !data.IsExpired() { 127 | result[token] = &core.RefreshTokenData{ 128 | UserData: data.UserData, 129 | Expiry: data.Expiry, 130 | Created: data.Created, 131 | } 132 | } 133 | } 134 | 135 | return result 136 | } 137 | 138 | // Clear removes all tokens from the store (useful for testing) 139 | // Note: This method is not part of the RefreshTokenStorer interface 140 | func (s *InMemoryRefreshTokenStore) Clear() { 141 | s.mu.Lock() 142 | defer s.mu.Unlock() 143 | 144 | s.tokens = make(map[string]*core.RefreshTokenData) 145 | } 146 | -------------------------------------------------------------------------------- /_example/redis_store/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | jwt "github.com/appleboy/gin-jwt/v3" 9 | "github.com/gin-gonic/gin" 10 | gojwt "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | type User struct { 14 | UserName string `json:"username"` 15 | Password string `json:"password"` 16 | FirstName string `json:"firstname"` 17 | LastName string `json:"lastname"` 18 | } 19 | 20 | const userAdmin = "admin" 21 | 22 | var identityKey = "id" 23 | 24 | func main() { 25 | r := gin.Default() 26 | 27 | // Method 1: Using functional options with EnableRedisStore (recommended) 28 | middleware := &jwt.GinJWTMiddleware{ 29 | Realm: "test zone", 30 | Key: []byte("secret key"), 31 | Timeout: time.Hour, 32 | MaxRefresh: time.Hour, 33 | IdentityKey: identityKey, 34 | PayloadFunc: func(data any) gojwt.MapClaims { 35 | if v, ok := data.(*User); ok { 36 | return gojwt.MapClaims{ 37 | identityKey: v.UserName, 38 | } 39 | } 40 | return gojwt.MapClaims{} 41 | }, 42 | IdentityHandler: func(c *gin.Context) any { 43 | claims := jwt.ExtractClaims(c) 44 | return &User{ 45 | UserName: claims[identityKey].(string), 46 | } 47 | }, 48 | Authenticator: func(c *gin.Context) (any, error) { 49 | var loginVals User 50 | if err := c.ShouldBind(&loginVals); err != nil { 51 | return "", jwt.ErrMissingLoginValues 52 | } 53 | userID := loginVals.UserName 54 | password := loginVals.Password 55 | 56 | if (userID == userAdmin && password == userAdmin) || 57 | (userID == "test" && password == "test") { 58 | return &User{ 59 | UserName: userID, 60 | LastName: "Bo-Yi", 61 | FirstName: "Wu", 62 | }, nil 63 | } 64 | 65 | return nil, jwt.ErrFailedAuthentication 66 | }, 67 | Authorizer: func(c *gin.Context, data any) bool { 68 | if v, ok := data.(*User); ok && v.UserName == userAdmin { 69 | return true 70 | } 71 | 72 | return false 73 | }, 74 | Unauthorized: func(c *gin.Context, code int, message string) { 75 | c.JSON(code, gin.H{ 76 | "code": code, 77 | "message": message, 78 | }) 79 | }, 80 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 81 | TokenHeadName: "Bearer", 82 | TimeFunc: time.Now, 83 | } 84 | 85 | // Configure Redis using functional options 86 | middleware.EnableRedisStore( 87 | jwt.WithRedisAddr("localhost:6379"), 88 | jwt.WithRedisCache(64*1024*1024, 30*time.Second), // 64MB client-side cache, 30s TTL 89 | ) 90 | 91 | authMiddleware, err := jwt.New(middleware) 92 | if err != nil { 93 | log.Fatal("JWT Error:" + err.Error()) 94 | } 95 | 96 | // When you use jwt.New(), the function is already automatically called for checking, 97 | // which means you don't need to call it again. 98 | errInit := authMiddleware.MiddlewareInit() 99 | 100 | if errInit != nil { 101 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 102 | } 103 | 104 | r.POST("/login", authMiddleware.LoginHandler) 105 | 106 | r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { 107 | claims := jwt.ExtractClaims(c) 108 | log.Printf("NoRoute claims: %#v\n", claims) 109 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 110 | }) 111 | 112 | auth := r.Group("/auth") 113 | // Refresh time can be longer than token timeout 114 | auth.GET("/refresh_token", authMiddleware.RefreshHandler) 115 | auth.Use(authMiddleware.MiddlewareFunc()) 116 | { 117 | auth.GET("/hello", helloHandler) 118 | auth.GET("/store-info", storeInfoHandler()) 119 | } 120 | 121 | log.Println("Server starting on :8000") 122 | log.Println("Using functional options Redis configuration") 123 | log.Println("Alternative methods shown below as comments:") 124 | 125 | srv := &http.Server{ 126 | Addr: ":8000", 127 | Handler: r, 128 | ReadHeaderTimeout: 5 * time.Second, 129 | } 130 | if err := srv.ListenAndServe(); err != nil { 131 | log.Fatal(err) 132 | } 133 | } 134 | 135 | func helloHandler(c *gin.Context) { 136 | claims := jwt.ExtractClaims(c) 137 | user, _ := c.Get(identityKey) 138 | c.JSON(200, gin.H{ 139 | "userID": claims[identityKey], 140 | "userName": user.(*User).UserName, 141 | "text": "Hello World.", 142 | }) 143 | } 144 | 145 | // storeInfoHandler provides information about the current token store 146 | func storeInfoHandler() gin.HandlerFunc { 147 | return func(c *gin.Context) { 148 | c.JSON(200, gin.H{ 149 | "configuration": "functional_options", 150 | "redis_enabled": true, 151 | "message": "Using functional options pattern for Redis configuration", 152 | }) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/appleboy/gin-jwt/v3 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/appleboy/gofight/v2 v2.1.2 7 | github.com/gin-gonic/gin v1.11.0 8 | github.com/golang-jwt/jwt/v5 v5.3.0 9 | github.com/redis/rueidis v1.0.66 10 | github.com/stretchr/testify v1.11.1 11 | github.com/testcontainers/testcontainers-go v0.39.0 12 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 13 | github.com/tidwall/gjson v1.17.1 14 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 15 | ) 16 | 17 | require ( 18 | dario.cat/mergo v1.0.2 // indirect 19 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 20 | github.com/Microsoft/go-winio v0.6.2 // indirect 21 | github.com/bytedance/gopkg v0.1.3 // indirect 22 | github.com/bytedance/sonic v1.14.1 // indirect 23 | github.com/bytedance/sonic/loader v0.3.0 // indirect 24 | github.com/cenkalti/backoff/v4 v4.2.1 // indirect 25 | github.com/cloudwego/base64x v0.1.6 // indirect 26 | github.com/containerd/errdefs v1.0.0 // indirect 27 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 28 | github.com/containerd/log v0.1.0 // indirect 29 | github.com/containerd/platforms v0.2.1 // indirect 30 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 31 | github.com/davecgh/go-spew v1.1.1 // indirect 32 | github.com/distribution/reference v0.6.0 // indirect 33 | github.com/docker/docker v28.3.3+incompatible // indirect 34 | github.com/docker/go-connections v0.6.0 // indirect 35 | github.com/docker/go-units v0.5.0 // indirect 36 | github.com/ebitengine/purego v0.8.4 // indirect 37 | github.com/felixge/httpsnoop v1.0.4 // indirect 38 | github.com/gabriel-vasile/mimetype v1.4.10 // indirect 39 | github.com/gin-contrib/sse v1.1.0 // indirect 40 | github.com/go-logr/logr v1.4.3 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/go-ole/go-ole v1.2.6 // indirect 43 | github.com/go-playground/locales v0.14.1 // indirect 44 | github.com/go-playground/universal-translator v0.18.1 // indirect 45 | github.com/go-playground/validator/v10 v10.28.0 // indirect 46 | github.com/goccy/go-json v0.10.5 // indirect 47 | github.com/goccy/go-yaml v1.18.0 // indirect 48 | github.com/gogo/protobuf v1.3.2 // indirect 49 | github.com/google/uuid v1.6.0 // indirect 50 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect 51 | github.com/json-iterator/go v1.1.12 // indirect 52 | github.com/klauspost/compress v1.18.0 // indirect 53 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 54 | github.com/leodido/go-urn v1.4.0 // indirect 55 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 56 | github.com/magiconair/properties v1.8.10 // indirect 57 | github.com/mattn/go-isatty v0.0.20 // indirect 58 | github.com/mdelapenya/tlscert v0.2.0 // indirect 59 | github.com/moby/docker-image-spec v1.3.1 // indirect 60 | github.com/moby/go-archive v0.1.0 // indirect 61 | github.com/moby/patternmatcher v0.6.0 // indirect 62 | github.com/moby/sys/sequential v0.6.0 // indirect 63 | github.com/moby/sys/user v0.4.0 // indirect 64 | github.com/moby/sys/userns v0.1.0 // indirect 65 | github.com/moby/term v0.5.0 // indirect 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 67 | github.com/modern-go/reflect2 v1.0.2 // indirect 68 | github.com/morikuni/aec v1.0.0 // indirect 69 | github.com/opencontainers/go-digest v1.0.0 // indirect 70 | github.com/opencontainers/image-spec v1.1.1 // indirect 71 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 72 | github.com/pkg/errors v0.9.1 // indirect 73 | github.com/pmezard/go-difflib v1.0.0 // indirect 74 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 75 | github.com/quic-go/qpack v0.6.0 // indirect 76 | github.com/quic-go/quic-go v0.57.1 // indirect 77 | github.com/shirou/gopsutil/v4 v4.25.6 // indirect 78 | github.com/sirupsen/logrus v1.9.3 // indirect 79 | github.com/tidwall/match v1.1.1 // indirect 80 | github.com/tidwall/pretty v1.2.0 // indirect 81 | github.com/tklauser/go-sysconf v0.3.12 // indirect 82 | github.com/tklauser/numcpus v0.6.1 // indirect 83 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 84 | github.com/ugorji/go/codec v1.3.0 // indirect 85 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 86 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 87 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 88 | go.opentelemetry.io/otel v1.37.0 // indirect 89 | go.opentelemetry.io/otel/metric v1.37.0 // indirect 90 | go.opentelemetry.io/otel/sdk v1.37.0 // indirect 91 | go.opentelemetry.io/otel/trace v1.37.0 // indirect 92 | go.uber.org/mock v0.6.0 // indirect 93 | golang.org/x/arch v0.22.0 // indirect 94 | golang.org/x/crypto v0.45.0 // indirect 95 | golang.org/x/net v0.47.0 // indirect 96 | golang.org/x/sys v0.38.0 // indirect 97 | golang.org/x/text v0.31.0 // indirect 98 | google.golang.org/grpc v1.75.1 // indirect 99 | google.golang.org/protobuf v1.36.10 // indirect 100 | gopkg.in/yaml.v3 v3.0.1 // indirect 101 | ) 102 | -------------------------------------------------------------------------------- /_example/redis_simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | jwt "github.com/appleboy/gin-jwt/v3" 9 | "github.com/gin-gonic/gin" 10 | gojwt "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | type User struct { 14 | UserName string `json:"username"` 15 | Password string `json:"password"` 16 | FirstName string `json:"firstname"` 17 | LastName string `json:"lastname"` 18 | } 19 | 20 | var identityKey = "id" 21 | 22 | func main() { 23 | r := gin.Default() 24 | 25 | // Create JWT middleware configuration 26 | middleware := &jwt.GinJWTMiddleware{ 27 | Realm: "test zone", 28 | Key: []byte("secret key"), 29 | Timeout: time.Hour, 30 | MaxRefresh: time.Hour, 31 | IdentityKey: identityKey, 32 | 33 | PayloadFunc: func(data any) gojwt.MapClaims { 34 | if v, ok := data.(*User); ok { 35 | return gojwt.MapClaims{ 36 | identityKey: v.UserName, 37 | } 38 | } 39 | return gojwt.MapClaims{} 40 | }, 41 | 42 | IdentityHandler: func(c *gin.Context) any { 43 | claims := jwt.ExtractClaims(c) 44 | return &User{ 45 | UserName: claims[identityKey].(string), 46 | } 47 | }, 48 | 49 | Authenticator: func(c *gin.Context) (any, error) { 50 | var loginVals User 51 | if err := c.ShouldBind(&loginVals); err != nil { 52 | return "", jwt.ErrMissingLoginValues 53 | } 54 | userID := loginVals.UserName 55 | password := loginVals.Password 56 | 57 | if (userID == "admin" && password == "admin") || 58 | (userID == "test" && password == "test") { 59 | return &User{ 60 | UserName: userID, 61 | LastName: "Bo-Yi", 62 | FirstName: "Wu", 63 | }, nil 64 | } 65 | 66 | return nil, jwt.ErrFailedAuthentication 67 | }, 68 | 69 | Authorizer: func(c *gin.Context, data any) bool { 70 | if v, ok := data.(*User); ok && v.UserName == "admin" { 71 | return true 72 | } 73 | return false 74 | }, 75 | 76 | Unauthorized: func(c *gin.Context, code int, message string) { 77 | c.JSON(code, gin.H{ 78 | "code": code, 79 | "message": message, 80 | }) 81 | }, 82 | 83 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 84 | TokenHeadName: "Bearer", 85 | TimeFunc: time.Now, 86 | } 87 | 88 | // Configure Redis store using functional options pattern 89 | middleware.EnableRedisStore( 90 | jwt.WithRedisCache(64*1024*1024, 30*time.Second), // 64MB cache, 30s TTL 91 | ) 92 | 93 | // Create the JWT middleware 94 | authMiddleware, err := jwt.New(middleware) 95 | // Alternative initialization methods using functional options: 96 | // 97 | // Method 1: Simple enable with defaults 98 | // }.EnableRedisStore()) 99 | // 100 | // Method 2: Enable with custom address 101 | // }.EnableRedisStore(jwt.WithRedisAddr("redis:6379"))) 102 | // 103 | // Method 3: Enable with full options 104 | // }.EnableRedisStore( 105 | // jwt.WithRedisAddr("localhost:6379"), 106 | // jwt.WithRedisAuth("", 0), 107 | // jwt.WithRedisCache(128*1024*1024, time.Minute), 108 | // )) 109 | // 110 | // Method 4: Enable with comprehensive configuration 111 | // }.EnableRedisStore( 112 | // jwt.WithRedisAddr("localhost:6379"), 113 | // jwt.WithRedisAuth("password", 1), 114 | // jwt.WithRedisCache(128*1024*1024, time.Minute), 115 | // jwt.WithRedisPool(20, time.Hour, 2*time.Hour), 116 | // jwt.WithRedisKeyPrefix("myapp:jwt:"), 117 | // )) 118 | // 119 | // Method 5: Enable with TLS configuration (for secure Redis connections) 120 | // tlsConfig := &tls.Config{ 121 | // MinVersion: tls.VersionTLS12, 122 | // // Add your certificates here if needed 123 | // } 124 | // }.EnableRedisStore( 125 | // jwt.WithRedisAddr("redis.example.com:6380"), 126 | // jwt.WithRedisAuth("password", 0), 127 | // jwt.WithRedisTLS(tlsConfig), 128 | // )) 129 | if err != nil { 130 | log.Fatal("JWT Error:" + err.Error()) 131 | } 132 | 133 | // When you use jwt.New(), the function is already automatically called for checking, 134 | // which means you don't need to call it again. 135 | errInit := authMiddleware.MiddlewareInit() 136 | if errInit != nil { 137 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 138 | } 139 | 140 | r.POST("/login", authMiddleware.LoginHandler) 141 | 142 | r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { 143 | claims := jwt.ExtractClaims(c) 144 | log.Printf("NoRoute claims: %#v\n", claims) 145 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 146 | }) 147 | 148 | auth := r.Group("/auth") 149 | // Refresh time can be longer than token timeout 150 | auth.GET("/refresh_token", authMiddleware.RefreshHandler) 151 | auth.Use(authMiddleware.MiddlewareFunc()) 152 | { 153 | auth.GET("/hello", helloHandler) 154 | } 155 | 156 | log.Println("Server starting on :8000") 157 | log.Println("Redis store is enabled - will fall back to memory if Redis is not available") 158 | if err := http.ListenAndServe(":8000", r); err != nil { 159 | log.Fatal(err) 160 | } 161 | } 162 | 163 | func helloHandler(c *gin.Context) { 164 | claims := jwt.ExtractClaims(c) 165 | user, _ := c.Get(identityKey) 166 | c.JSON(200, gin.H{ 167 | "userID": claims[identityKey], 168 | "userName": user.(*User).UserName, 169 | "text": "Hello World.", 170 | }) 171 | } 172 | -------------------------------------------------------------------------------- /_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/v3" 10 | "github.com/gin-gonic/gin" 11 | gojwt "github.com/golang-jwt/jwt/v5" 12 | ) 13 | 14 | type login struct { 15 | Username string `form:"username" json:"username" binding:"required"` 16 | Password string `form:"password" json:"password" binding:"required"` 17 | } 18 | 19 | const userAdmin = "admin" 20 | 21 | var ( 22 | identityKey = "id" 23 | port string 24 | ) 25 | 26 | // User demo 27 | type User struct { 28 | UserName string 29 | FirstName string 30 | LastName string 31 | } 32 | 33 | func init() { 34 | port = os.Getenv("PORT") 35 | if port == "" { 36 | port = "8000" 37 | } 38 | } 39 | 40 | func main() { 41 | engine := gin.Default() 42 | // the jwt middleware 43 | authMiddleware, err := jwt.New(initParams()) 44 | if err != nil { 45 | log.Fatal("JWT Error:" + err.Error()) 46 | } 47 | 48 | // initialize middleware 49 | errInit := authMiddleware.MiddlewareInit() 50 | if errInit != nil { 51 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 52 | } 53 | 54 | // register route 55 | registerRoute(engine, authMiddleware) 56 | 57 | // start http server with proper timeouts 58 | srv := &http.Server{ 59 | Addr: ":" + port, 60 | Handler: engine, 61 | ReadHeaderTimeout: 5 * time.Second, 62 | } 63 | if err = srv.ListenAndServe(); err != nil { 64 | log.Fatal(err) 65 | } 66 | } 67 | 68 | func registerRoute(r *gin.Engine, handle *jwt.GinJWTMiddleware) { 69 | // Public routes 70 | r.POST("/login", handle.LoginHandler) 71 | r.POST("/refresh", handle.RefreshHandler) // RFC 6749 compliant refresh endpoint 72 | 73 | r.NoRoute(handle.MiddlewareFunc(), handleNoRoute()) 74 | 75 | // Protected routes 76 | auth := r.Group("/auth", handle.MiddlewareFunc()) 77 | auth.GET("/hello", helloHandler) 78 | auth.POST("/logout", handle.LogoutHandler) // Logout with refresh token revocation 79 | } 80 | 81 | func initParams() *jwt.GinJWTMiddleware { 82 | return &jwt.GinJWTMiddleware{ 83 | Realm: "test zone", 84 | Key: []byte("secret key"), 85 | Timeout: time.Hour, 86 | MaxRefresh: time.Hour, 87 | IdentityKey: identityKey, 88 | PayloadFunc: payloadFunc(), 89 | 90 | IdentityHandler: identityHandler(), 91 | Authenticator: authenticator(), 92 | Authorizer: authorizator(), 93 | Unauthorized: unauthorized(), 94 | LogoutResponse: logoutResponse(), 95 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 96 | // TokenLookup: "query:token", 97 | // TokenLookup: "cookie:token", 98 | TokenHeadName: "Bearer", 99 | TimeFunc: time.Now, 100 | } 101 | } 102 | 103 | func payloadFunc() func(data any) gojwt.MapClaims { 104 | return func(data any) gojwt.MapClaims { 105 | if v, ok := data.(*User); ok { 106 | return gojwt.MapClaims{ 107 | identityKey: v.UserName, 108 | } 109 | } 110 | return gojwt.MapClaims{} 111 | } 112 | } 113 | 114 | func identityHandler() func(c *gin.Context) any { 115 | return func(c *gin.Context) any { 116 | claims := jwt.ExtractClaims(c) 117 | return &User{ 118 | UserName: claims[identityKey].(string), 119 | } 120 | } 121 | } 122 | 123 | func authenticator() func(c *gin.Context) (any, error) { 124 | return func(c *gin.Context) (any, error) { 125 | var loginVals login 126 | if err := c.ShouldBind(&loginVals); err != nil { 127 | return "", jwt.ErrMissingLoginValues 128 | } 129 | userID := loginVals.Username 130 | password := loginVals.Password 131 | 132 | if (userID == userAdmin && password == userAdmin) || 133 | (userID == "test" && password == "test") { 134 | return &User{ 135 | UserName: userID, 136 | LastName: "Bo-Yi", 137 | FirstName: "Wu", 138 | }, nil 139 | } 140 | return nil, jwt.ErrFailedAuthentication 141 | } 142 | } 143 | 144 | func authorizator() func(c *gin.Context, data any) bool { 145 | return func(c *gin.Context, data any) bool { 146 | if v, ok := data.(*User); ok && v.UserName == "admin" { 147 | return true 148 | } 149 | return false 150 | } 151 | } 152 | 153 | func unauthorized() func(c *gin.Context, code int, message string) { 154 | return func(c *gin.Context, code int, message string) { 155 | c.JSON(code, gin.H{ 156 | "code": code, 157 | "message": message, 158 | }) 159 | } 160 | } 161 | 162 | func logoutResponse() func(c *gin.Context) { 163 | return func(c *gin.Context) { 164 | // This demonstrates that claims are now accessible during logout 165 | claims := jwt.ExtractClaims(c) 166 | user, exists := c.Get(identityKey) 167 | 168 | response := gin.H{ 169 | "code": http.StatusOK, 170 | "message": "Successfully logged out", 171 | } 172 | 173 | // Show that we can access user information during logout 174 | if len(claims) > 0 { 175 | response["logged_out_user"] = claims[identityKey] 176 | } 177 | if exists { 178 | response["user_info"] = user.(*User).UserName 179 | } 180 | 181 | c.JSON(http.StatusOK, response) 182 | } 183 | } 184 | 185 | func handleNoRoute() func(c *gin.Context) { 186 | return func(c *gin.Context) { 187 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 188 | } 189 | } 190 | 191 | func helloHandler(c *gin.Context) { 192 | claims := jwt.ExtractClaims(c) 193 | user, _ := c.Get(identityKey) 194 | c.JSON(200, gin.H{ 195 | "userID": claims[identityKey], 196 | "userName": user.(*User).UserName, 197 | "text": "Hello World.", 198 | }) 199 | } 200 | -------------------------------------------------------------------------------- /_example/redis_tls/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "log" 7 | "net/http" 8 | "os" 9 | "time" 10 | 11 | jwt "github.com/appleboy/gin-jwt/v3" 12 | "github.com/gin-gonic/gin" 13 | gojwt "github.com/golang-jwt/jwt/v5" 14 | ) 15 | 16 | type User struct { 17 | UserName string `json:"username"` 18 | Password string `json:"password"` 19 | FirstName string `json:"firstname"` 20 | LastName string `json:"lastname"` 21 | } 22 | 23 | var identityKey = "id" 24 | 25 | func main() { 26 | r := gin.Default() 27 | 28 | // Create JWT middleware configuration 29 | middleware := &jwt.GinJWTMiddleware{ 30 | Realm: "test zone", 31 | Key: []byte("secret key"), 32 | Timeout: time.Hour, 33 | MaxRefresh: time.Hour, 34 | IdentityKey: identityKey, 35 | 36 | PayloadFunc: func(data any) gojwt.MapClaims { 37 | if v, ok := data.(*User); ok { 38 | return gojwt.MapClaims{ 39 | identityKey: v.UserName, 40 | } 41 | } 42 | return gojwt.MapClaims{} 43 | }, 44 | 45 | IdentityHandler: func(c *gin.Context) any { 46 | claims := jwt.ExtractClaims(c) 47 | return &User{ 48 | UserName: claims[identityKey].(string), 49 | } 50 | }, 51 | 52 | Authenticator: func(c *gin.Context) (any, error) { 53 | var loginVals User 54 | if err := c.ShouldBind(&loginVals); err != nil { 55 | return "", jwt.ErrMissingLoginValues 56 | } 57 | userID := loginVals.UserName 58 | password := loginVals.Password 59 | 60 | if (userID == "admin" && password == "admin") || 61 | (userID == "test" && password == "test") { 62 | return &User{ 63 | UserName: userID, 64 | LastName: "Bo-Yi", 65 | FirstName: "Wu", 66 | }, nil 67 | } 68 | 69 | return nil, jwt.ErrFailedAuthentication 70 | }, 71 | 72 | Authorizer: func(c *gin.Context, data any) bool { 73 | if v, ok := data.(*User); ok && v.UserName == "admin" { 74 | return true 75 | } 76 | return false 77 | }, 78 | 79 | Unauthorized: func(c *gin.Context, code int, message string) { 80 | c.JSON(code, gin.H{ 81 | "code": code, 82 | "message": message, 83 | }) 84 | }, 85 | 86 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 87 | TokenHeadName: "Bearer", 88 | TimeFunc: time.Now, 89 | } 90 | 91 | // Configure TLS for secure Redis connection 92 | tlsConfig := createTLSConfig() 93 | 94 | // Configure Redis store with TLS enabled 95 | middleware.EnableRedisStore( 96 | jwt.WithRedisAddr("redis.example.com:6380"), // Use TLS port (usually 6380) 97 | jwt.WithRedisAuth("your-password", 0), 98 | jwt.WithRedisTLS(tlsConfig), 99 | jwt.WithRedisCache(64*1024*1024, 30*time.Second), // 64MB cache, 30s TTL 100 | ) 101 | 102 | // Create the JWT middleware 103 | authMiddleware, err := jwt.New(middleware) 104 | if err != nil { 105 | log.Fatal("JWT Error:" + err.Error()) 106 | } 107 | 108 | // When you use jwt.New(), the function is already automatically called for checking, 109 | // which means you don't need to call it again. 110 | errInit := authMiddleware.MiddlewareInit() 111 | if errInit != nil { 112 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 113 | } 114 | 115 | r.POST("/login", authMiddleware.LoginHandler) 116 | 117 | r.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { 118 | claims := jwt.ExtractClaims(c) 119 | log.Printf("NoRoute claims: %#v\n", claims) 120 | c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) 121 | }) 122 | 123 | auth := r.Group("/auth") 124 | // Refresh time can be longer than token timeout 125 | auth.GET("/refresh_token", authMiddleware.RefreshHandler) 126 | auth.Use(authMiddleware.MiddlewareFunc()) 127 | { 128 | auth.GET("/hello", helloHandler) 129 | } 130 | 131 | log.Println("Server starting on :8000") 132 | log.Println("Redis TLS store is enabled") 133 | if err := http.ListenAndServe(":8000", r); err != nil { 134 | log.Fatal(err) 135 | } 136 | } 137 | 138 | func helloHandler(c *gin.Context) { 139 | claims := jwt.ExtractClaims(c) 140 | user, _ := c.Get(identityKey) 141 | c.JSON(200, gin.H{ 142 | "userID": claims[identityKey], 143 | "userName": user.(*User).UserName, 144 | "text": "Hello World.", 145 | }) 146 | } 147 | 148 | // createTLSConfig creates a TLS configuration for Redis connection 149 | func createTLSConfig() *tls.Config { 150 | // Example 1: Basic TLS with system CA certificates 151 | tlsConfig := &tls.Config{ 152 | MinVersion: tls.VersionTLS12, 153 | MaxVersion: tls.VersionTLS13, 154 | } 155 | 156 | // Example 2: TLS with custom CA certificate (uncomment to use) 157 | // caCert, err := os.ReadFile("/path/to/ca.crt") 158 | // if err != nil { 159 | // log.Fatalf("Failed to read CA certificate: %v", err) 160 | // } 161 | // 162 | // caCertPool := x509.NewCertPool() 163 | // if !caCertPool.AppendCertsFromPEM(caCert) { 164 | // log.Fatal("Failed to parse CA certificate") 165 | // } 166 | // 167 | // tlsConfig.RootCAs = caCertPool 168 | 169 | // Example 3: TLS with client certificate (mutual TLS) (uncomment to use) 170 | // cert, err := tls.LoadX509KeyPair("/path/to/client.crt", "/path/to/client.key") 171 | // if err != nil { 172 | // log.Fatalf("Failed to load client certificate: %v", err) 173 | // } 174 | // 175 | // tlsConfig.Certificates = []tls.Certificate{cert} 176 | 177 | // Example 4: Skip certificate verification (NOT recommended for production) 178 | // tlsConfig.InsecureSkipVerify = true 179 | 180 | return tlsConfig 181 | } 182 | 183 | // Example helper function to load custom CA certificate 184 | func loadCACertificate(caPath string) *x509.CertPool { 185 | caCert, err := os.ReadFile(caPath) 186 | if err != nil { 187 | log.Fatalf("Failed to read CA certificate: %v", err) 188 | } 189 | 190 | caCertPool := x509.NewCertPool() 191 | if !caCertPool.AppendCertsFromPEM(caCert) { 192 | log.Fatal("Failed to parse CA certificate") 193 | } 194 | 195 | return caCertPool 196 | } 197 | 198 | // Example helper function to load client certificate for mutual TLS 199 | func loadClientCertificate(certPath, keyPath string) tls.Certificate { 200 | cert, err := tls.LoadX509KeyPair(certPath, keyPath) 201 | if err != nil { 202 | log.Fatalf("Failed to load client certificate: %v", err) 203 | } 204 | return cert 205 | } 206 | -------------------------------------------------------------------------------- /store/redis.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/appleboy/gin-jwt/v3/core" 12 | "github.com/redis/rueidis" 13 | ) 14 | 15 | var _ core.TokenStore = &RedisRefreshTokenStore{} 16 | 17 | // RedisRefreshTokenStore provides a Redis-based refresh token store with client-side caching 18 | type RedisRefreshTokenStore struct { 19 | client rueidis.Client 20 | prefix string 21 | ctx context.Context 22 | cacheTTL time.Duration 23 | } 24 | 25 | // RedisConfig holds the configuration for Redis store 26 | type RedisConfig struct { 27 | // Redis connection configuration 28 | Addr string // Redis server address (default: "localhost:6379") 29 | Password string // Redis password (default: "") 30 | DB int // Redis database number (default: 0) 31 | 32 | // TLS configuration 33 | TLSConfig *tls.Config // TLS configuration for secure connections (optional, default: nil) 34 | 35 | // Client-side cache configuration 36 | CacheSize int // Client-side cache size in bytes (default: 128MB) 37 | CacheTTL time.Duration // Client-side cache TTL (default: 1 minute) 38 | 39 | // Connection pool configuration 40 | PoolSize int // Connection pool size (default: 10) 41 | ConnMaxIdleTime time.Duration // Max idle time for connections (default: 30 minutes) 42 | ConnMaxLifetime time.Duration // Max lifetime for connections (default: 1 hour) 43 | 44 | // Key prefix for Redis keys 45 | KeyPrefix string // Prefix for all Redis keys (default: "gin-jwt:") 46 | } 47 | 48 | // DefaultRedisConfig returns a default Redis configuration 49 | func DefaultRedisConfig() *RedisConfig { 50 | return &RedisConfig{ 51 | Addr: "localhost:6379", 52 | Password: "", 53 | DB: 0, 54 | CacheSize: 128 * 1024 * 1024, // 128MB 55 | CacheTTL: time.Minute, 56 | PoolSize: 10, 57 | ConnMaxIdleTime: 30 * time.Minute, 58 | ConnMaxLifetime: time.Hour, 59 | KeyPrefix: "gin-jwt:", 60 | } 61 | } 62 | 63 | // NewRedisRefreshTokenStore creates a new Redis-based refresh token store with client-side caching 64 | func NewRedisRefreshTokenStore(config *RedisConfig) (*RedisRefreshTokenStore, error) { 65 | if config == nil { 66 | config = DefaultRedisConfig() 67 | } 68 | 69 | // Build Redis client options 70 | clientOpt := rueidis.ClientOption{ 71 | InitAddress: []string{config.Addr}, 72 | Password: config.Password, 73 | SelectDB: config.DB, 74 | 75 | // TLS configuration 76 | TLSConfig: config.TLSConfig, 77 | 78 | // Connection configuration 79 | ConnWriteTimeout: 10 * time.Second, 80 | 81 | // Client-side cache configuration 82 | CacheSizeEachConn: config.CacheSize, 83 | DisableCache: false, 84 | } 85 | 86 | // Create Redis client with client-side caching enabled 87 | client, err := rueidis.NewClient(clientOpt) 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to create Redis client: %w", err) 90 | } 91 | 92 | // Test connection 93 | ctx := context.Background() 94 | if err := client.Do(ctx, client.B().Ping().Build()).Error(); err != nil { 95 | client.Close() 96 | return nil, fmt.Errorf("failed to connect to Redis: %w", err) 97 | } 98 | 99 | return &RedisRefreshTokenStore{ 100 | client: client, 101 | prefix: config.KeyPrefix, 102 | ctx: ctx, 103 | cacheTTL: config.CacheTTL, 104 | }, nil 105 | } 106 | 107 | // Close closes the Redis client connection 108 | func (s *RedisRefreshTokenStore) Close() error { 109 | s.client.Close() 110 | return nil 111 | } 112 | 113 | // buildKey creates a Redis key with the configured prefix 114 | func (s *RedisRefreshTokenStore) buildKey(token string) string { 115 | return s.prefix + token 116 | } 117 | 118 | // Set stores a refresh token with associated user data and expiration 119 | func (s *RedisRefreshTokenStore) Set( 120 | ctx context.Context, 121 | token string, 122 | userData any, 123 | expiry time.Time, 124 | ) error { 125 | if token == "" { 126 | return errors.New("token cannot be empty") 127 | } 128 | 129 | tokenData := &core.RefreshTokenData{ 130 | UserData: userData, 131 | Expiry: expiry, 132 | Created: time.Now(), 133 | } 134 | 135 | // Serialize token data to JSON 136 | data, err := json.Marshal(tokenData) 137 | if err != nil { 138 | return fmt.Errorf("failed to marshal token data: %w", err) 139 | } 140 | 141 | key := s.buildKey(token) 142 | ttl := time.Until(expiry) 143 | 144 | // If TTL is negative or zero, the token has already expired 145 | if ttl <= 0 { 146 | return errors.New("token expiry time must be in the future") 147 | } 148 | 149 | // Store in Redis with expiration 150 | cmd := s.client.B().Setex().Key(key).Seconds(int64(ttl.Seconds())).Value(string(data)).Build() 151 | if err := s.client.Do(ctx, cmd).Error(); err != nil { 152 | return fmt.Errorf("failed to store token in Redis: %w", err) 153 | } 154 | 155 | return nil 156 | } 157 | 158 | // Get retrieves user data associated with a refresh token 159 | // This method benefits from client-side caching for frequently accessed tokens 160 | func (s *RedisRefreshTokenStore) Get(ctx context.Context, token string) (any, error) { 161 | if token == "" { 162 | return nil, core.ErrRefreshTokenNotFound 163 | } 164 | 165 | key := s.buildKey(token) 166 | 167 | // Use client-side cache by default 168 | cmd := s.client.B().Get().Key(key).Cache() 169 | result := s.client.DoCache(ctx, cmd, s.cacheTTL) 170 | 171 | if result.Error() != nil { 172 | if rueidis.IsRedisNil(result.Error()) { 173 | return nil, core.ErrRefreshTokenNotFound 174 | } 175 | return nil, fmt.Errorf("failed to get token from Redis: %w", result.Error()) 176 | } 177 | 178 | data, err := result.ToString() 179 | if err != nil { 180 | return nil, fmt.Errorf("failed to convert Redis result to string: %w", err) 181 | } 182 | 183 | var tokenData core.RefreshTokenData 184 | if err := json.Unmarshal([]byte(data), &tokenData); err != nil { 185 | return nil, fmt.Errorf("failed to unmarshal token data: %w", err) 186 | } 187 | 188 | // Check if token has expired 189 | if tokenData.IsExpired() { 190 | // Clean up expired token asynchronously 191 | go func() { 192 | deleteCmd := s.client.B().Del().Key(key).Build() 193 | s.client.Do(context.Background(), deleteCmd) 194 | }() 195 | return nil, core.ErrRefreshTokenExpired 196 | } 197 | 198 | return tokenData.UserData, nil 199 | } 200 | 201 | // Delete removes a refresh token from storage 202 | func (s *RedisRefreshTokenStore) Delete(ctx context.Context, token string) error { 203 | if token == "" { 204 | return nil // No error for empty token deletion 205 | } 206 | 207 | key := s.buildKey(token) 208 | cmd := s.client.B().Del().Key(key).Build() 209 | 210 | if err := s.client.Do(ctx, cmd).Error(); err != nil { 211 | return fmt.Errorf("failed to delete token from Redis: %w", err) 212 | } 213 | 214 | return nil 215 | } 216 | 217 | // Cleanup removes expired tokens and returns the number of tokens cleaned up 218 | // Note: Redis automatically handles expiration, so this method scans for manually expired tokens 219 | func (s *RedisRefreshTokenStore) Cleanup(ctx context.Context) (int, error) { 220 | pattern := s.buildKey("*") 221 | var cleaned int 222 | var cursor uint64 223 | 224 | for { 225 | // Scan for keys with our prefix 226 | cmd := s.client.B().Scan().Cursor(cursor).Match(pattern).Count(100).Build() 227 | result := s.client.Do(ctx, cmd) 228 | 229 | if result.Error() != nil { 230 | return cleaned, fmt.Errorf("failed to scan Redis keys: %w", result.Error()) 231 | } 232 | 233 | scanResult, err := result.AsScanEntry() 234 | if err != nil { 235 | return cleaned, fmt.Errorf("failed to parse scan result: %w", err) 236 | } 237 | 238 | // Check each key for expiration 239 | for _, key := range scanResult.Elements { 240 | getCmd := s.client.B().Get().Key(key).Build() 241 | getResult := s.client.Do(ctx, getCmd) 242 | 243 | if rueidis.IsRedisNil(getResult.Error()) { 244 | // Key already expired/deleted 245 | continue 246 | } 247 | 248 | if getResult.Error() != nil { 249 | continue // Skip on error 250 | } 251 | 252 | data, err := getResult.ToString() 253 | if err != nil { 254 | continue // Skip on error 255 | } 256 | 257 | var tokenData core.RefreshTokenData 258 | if err := json.Unmarshal([]byte(data), &tokenData); err != nil { 259 | continue // Skip on error 260 | } 261 | 262 | if tokenData.IsExpired() { 263 | deleteCmd := s.client.B().Del().Key(key).Build() 264 | if s.client.Do(ctx, deleteCmd).Error() == nil { 265 | cleaned++ 266 | } 267 | } 268 | } 269 | 270 | cursor = scanResult.Cursor 271 | if cursor == 0 { 272 | break 273 | } 274 | } 275 | 276 | return cleaned, nil 277 | } 278 | 279 | // Count returns the total number of active refresh tokens 280 | func (s *RedisRefreshTokenStore) Count(ctx context.Context) (int, error) { 281 | pattern := s.buildKey("*") 282 | var count int 283 | var cursor uint64 284 | 285 | for { 286 | cmd := s.client.B().Scan().Cursor(cursor).Match(pattern).Count(100).Build() 287 | result := s.client.Do(ctx, cmd) 288 | 289 | if result.Error() != nil { 290 | return 0, fmt.Errorf("failed to scan Redis keys: %w", result.Error()) 291 | } 292 | 293 | scanResult, err := result.AsScanEntry() 294 | if err != nil { 295 | return 0, fmt.Errorf("failed to parse scan result: %w", err) 296 | } 297 | 298 | count += len(scanResult.Elements) 299 | cursor = scanResult.Cursor 300 | 301 | if cursor == 0 { 302 | break 303 | } 304 | } 305 | 306 | return count, nil 307 | } 308 | 309 | // Ping tests the Redis connection 310 | func (s *RedisRefreshTokenStore) Ping() error { 311 | return s.client.Do(s.ctx, s.client.B().Ping().Build()).Error() 312 | } 313 | 314 | // FlushDB removes all keys from the current Redis database (useful for testing) 315 | // Note: This method is not part of the RefreshTokenStorer interface 316 | func (s *RedisRefreshTokenStore) FlushDB() error { 317 | return s.client.Do(s.ctx, s.client.B().Flushdb().Build()).Error() 318 | } 319 | -------------------------------------------------------------------------------- /auth_jwt_methods_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGinJWTMiddleware_FunctionalOptionsOnly(t *testing.T) { 12 | gin.SetMode(gin.TestMode) 13 | 14 | t.Run("EnableRedisStoreDefault", func(t *testing.T) { 15 | middleware := &GinJWTMiddleware{ 16 | Realm: "test zone", 17 | Key: []byte("secret key"), 18 | Timeout: time.Hour, 19 | MaxRefresh: time.Hour * 24, 20 | IdentityKey: "id", 21 | } 22 | 23 | // Test EnableRedisStore with no options (default) 24 | result := middleware.EnableRedisStore() 25 | assert.Equal(t, middleware, result, "should return self for chaining") 26 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 27 | assert.NotNil(t, middleware.RedisConfig, "should set default Redis config") 28 | assert.Equal(t, "localhost:6379", middleware.RedisConfig.Addr, "should set default address") 29 | }) 30 | 31 | t.Run("EnableRedisStoreWithSingleOption", func(t *testing.T) { 32 | middleware := &GinJWTMiddleware{ 33 | Realm: "test zone", 34 | Key: []byte("secret key"), 35 | Timeout: time.Hour, 36 | MaxRefresh: time.Hour * 24, 37 | IdentityKey: "id", 38 | } 39 | 40 | testAddr := "redis.example.com:6379" 41 | 42 | // Test EnableRedisStore with single option 43 | result := middleware.EnableRedisStore(WithRedisAddr(testAddr)) 44 | assert.Equal(t, middleware, result, "should return self for chaining") 45 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 46 | assert.Equal(t, testAddr, middleware.RedisConfig.Addr, "should set custom address") 47 | // Should still have defaults for other values 48 | assert.Equal(t, "", middleware.RedisConfig.Password, "should have default empty password") 49 | assert.Equal(t, 0, middleware.RedisConfig.DB, "should have default DB") 50 | }) 51 | 52 | t.Run("EnableRedisStoreWithMultipleOptions", func(t *testing.T) { 53 | middleware := &GinJWTMiddleware{ 54 | Realm: "test zone", 55 | Key: []byte("secret key"), 56 | Timeout: time.Hour, 57 | MaxRefresh: time.Hour * 24, 58 | IdentityKey: "id", 59 | } 60 | 61 | testAddr := "redis.example.com:6379" 62 | testPassword := "testpass" 63 | testDB := 5 64 | 65 | // Test EnableRedisStore with multiple options 66 | result := middleware.EnableRedisStore( 67 | WithRedisAddr(testAddr), 68 | WithRedisAuth(testPassword, testDB), 69 | ) 70 | assert.Equal(t, middleware, result, "should return self for chaining") 71 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 72 | assert.Equal(t, testAddr, middleware.RedisConfig.Addr, "should set custom address") 73 | assert.Equal(t, testPassword, middleware.RedisConfig.Password, "should set custom password") 74 | assert.Equal(t, testDB, middleware.RedisConfig.DB, "should set custom DB") 75 | }) 76 | 77 | t.Run("EnableRedisStoreWithCacheOptions", func(t *testing.T) { 78 | middleware := &GinJWTMiddleware{ 79 | Realm: "test zone", 80 | Key: []byte("secret key"), 81 | Timeout: time.Hour, 82 | MaxRefresh: time.Hour * 24, 83 | IdentityKey: "id", 84 | } 85 | 86 | cacheSize := 64 * 1024 * 1024 // 64MB 87 | cacheTTL := 30 * time.Second 88 | 89 | // Test EnableRedisStore with cache options 90 | result := middleware.EnableRedisStore( 91 | WithRedisCache(cacheSize, cacheTTL), 92 | ) 93 | assert.Equal(t, middleware, result, "should return self for chaining") 94 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 95 | assert.NotNil(t, middleware.RedisConfig, "should create Redis config") 96 | assert.Equal(t, cacheSize, middleware.RedisConfig.CacheSize, "should set cache size") 97 | assert.Equal(t, cacheTTL, middleware.RedisConfig.CacheTTL, "should set cache TTL") 98 | }) 99 | 100 | t.Run("EnableRedisStoreWithPoolOptions", func(t *testing.T) { 101 | middleware := &GinJWTMiddleware{ 102 | Realm: "test zone", 103 | Key: []byte("secret key"), 104 | Timeout: time.Hour, 105 | MaxRefresh: time.Hour * 24, 106 | IdentityKey: "id", 107 | } 108 | 109 | poolSize := 20 110 | maxIdleTime := 2 * time.Hour 111 | maxLifetime := 4 * time.Hour 112 | 113 | // Test EnableRedisStore with pool options 114 | result := middleware.EnableRedisStore( 115 | WithRedisPool(poolSize, maxIdleTime, maxLifetime), 116 | ) 117 | assert.Equal(t, middleware, result, "should return self for chaining") 118 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 119 | assert.Equal(t, poolSize, middleware.RedisConfig.PoolSize, "should set pool size") 120 | assert.Equal( 121 | t, 122 | maxIdleTime, 123 | middleware.RedisConfig.ConnMaxIdleTime, 124 | "should set max idle time", 125 | ) 126 | assert.Equal( 127 | t, 128 | maxLifetime, 129 | middleware.RedisConfig.ConnMaxLifetime, 130 | "should set max lifetime", 131 | ) 132 | }) 133 | 134 | t.Run("EnableRedisStoreWithKeyPrefix", func(t *testing.T) { 135 | middleware := &GinJWTMiddleware{ 136 | Realm: "test zone", 137 | Key: []byte("secret key"), 138 | Timeout: time.Hour, 139 | MaxRefresh: time.Hour * 24, 140 | IdentityKey: "id", 141 | } 142 | 143 | keyPrefix := "test-app:" 144 | 145 | // Test EnableRedisStore with key prefix 146 | result := middleware.EnableRedisStore( 147 | WithRedisKeyPrefix(keyPrefix), 148 | ) 149 | assert.Equal(t, middleware, result, "should return self for chaining") 150 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 151 | assert.Equal(t, keyPrefix, middleware.RedisConfig.KeyPrefix, "should set key prefix") 152 | }) 153 | 154 | t.Run("EnableRedisStoreWithAllOptions", func(t *testing.T) { 155 | middleware := &GinJWTMiddleware{ 156 | Realm: "test zone", 157 | Key: []byte("secret key"), 158 | Timeout: time.Hour, 159 | MaxRefresh: time.Hour * 24, 160 | IdentityKey: "id", 161 | } 162 | 163 | testAddr := "custom.redis.com:6379" 164 | testPassword := "custom-password" 165 | testDB := 3 166 | cacheSize := 256 * 1024 * 1024 // 256MB 167 | cacheTTL := 5 * time.Minute 168 | poolSize := 25 169 | maxIdleTime := 3 * time.Hour 170 | maxLifetime := 6 * time.Hour 171 | keyPrefix := "custom-prefix:" 172 | 173 | // Test EnableRedisStore with all options 174 | result := middleware.EnableRedisStore( 175 | WithRedisAddr(testAddr), 176 | WithRedisAuth(testPassword, testDB), 177 | WithRedisCache(cacheSize, cacheTTL), 178 | WithRedisPool(poolSize, maxIdleTime, maxLifetime), 179 | WithRedisKeyPrefix(keyPrefix), 180 | ) 181 | 182 | assert.Equal(t, middleware, result, "should return self for chaining") 183 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 184 | assert.Equal(t, testAddr, middleware.RedisConfig.Addr, "should set address") 185 | assert.Equal(t, testPassword, middleware.RedisConfig.Password, "should set password") 186 | assert.Equal(t, testDB, middleware.RedisConfig.DB, "should set DB") 187 | assert.Equal(t, cacheSize, middleware.RedisConfig.CacheSize, "should set cache size") 188 | assert.Equal(t, cacheTTL, middleware.RedisConfig.CacheTTL, "should set cache TTL") 189 | assert.Equal(t, poolSize, middleware.RedisConfig.PoolSize, "should set pool size") 190 | assert.Equal( 191 | t, 192 | maxIdleTime, 193 | middleware.RedisConfig.ConnMaxIdleTime, 194 | "should set max idle time", 195 | ) 196 | assert.Equal( 197 | t, 198 | maxLifetime, 199 | middleware.RedisConfig.ConnMaxLifetime, 200 | "should set max lifetime", 201 | ) 202 | assert.Equal(t, keyPrefix, middleware.RedisConfig.KeyPrefix, "should set key prefix") 203 | }) 204 | 205 | t.Run("MultipleEnableRedisStoreCalls", func(t *testing.T) { 206 | middleware := &GinJWTMiddleware{ 207 | Realm: "test zone", 208 | Key: []byte("secret key"), 209 | Timeout: time.Hour, 210 | MaxRefresh: time.Hour * 24, 211 | IdentityKey: "id", 212 | } 213 | 214 | // First call 215 | middleware.EnableRedisStore( 216 | WithRedisAddr("first.redis.com:6379"), 217 | WithRedisAuth("first-pass", 1), 218 | ) 219 | 220 | // Second call should override the first 221 | result := middleware.EnableRedisStore( 222 | WithRedisAddr("second.redis.com:6379"), 223 | WithRedisAuth("second-pass", 2), 224 | WithRedisKeyPrefix("second:"), 225 | ) 226 | 227 | assert.Equal(t, middleware, result, "should return self for chaining") 228 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 229 | assert.Equal( 230 | t, 231 | "second.redis.com:6379", 232 | middleware.RedisConfig.Addr, 233 | "should use second address", 234 | ) 235 | assert.Equal( 236 | t, 237 | "second-pass", 238 | middleware.RedisConfig.Password, 239 | "should use second password", 240 | ) 241 | assert.Equal(t, 2, middleware.RedisConfig.DB, "should use second DB") 242 | assert.Equal(t, "second:", middleware.RedisConfig.KeyPrefix, "should use second key prefix") 243 | }) 244 | 245 | t.Run("DefaultConfiguration", func(t *testing.T) { 246 | middleware := &GinJWTMiddleware{ 247 | Realm: "test zone", 248 | Key: []byte("secret key"), 249 | Timeout: time.Hour, 250 | MaxRefresh: time.Hour * 24, 251 | IdentityKey: "id", 252 | } 253 | 254 | // Test EnableRedisStore with defaults 255 | result := middleware.EnableRedisStore() 256 | config := result.RedisConfig 257 | 258 | // Verify all default values 259 | assert.Equal(t, "localhost:6379", config.Addr, "should have default address") 260 | assert.Equal(t, "", config.Password, "should have empty default password") 261 | assert.Equal(t, 0, config.DB, "should have default DB") 262 | assert.Equal(t, 128*1024*1024, config.CacheSize, "should have default cache size") 263 | assert.Equal(t, time.Minute, config.CacheTTL, "should have default cache TTL") 264 | assert.Equal(t, 10, config.PoolSize, "should have default pool size") 265 | assert.Equal(t, 30*time.Minute, config.ConnMaxIdleTime, "should have default max idle time") 266 | assert.Equal(t, time.Hour, config.ConnMaxLifetime, "should have default max lifetime") 267 | assert.Equal(t, "gin-jwt:", config.KeyPrefix, "should have default key prefix") 268 | }) 269 | } 270 | -------------------------------------------------------------------------------- /store/redis_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | "github.com/appleboy/gin-jwt/v3/core" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "github.com/testcontainers/testcontainers-go" 13 | "github.com/testcontainers/testcontainers-go/modules/redis" 14 | ) 15 | 16 | func setupRedisContainer(t *testing.T) (string, string) { 17 | ctx := context.Background() 18 | 19 | // Start Redis container 20 | redisContainer, err := redis.Run(ctx, 21 | "redis:alpine", 22 | ) 23 | require.NoError(t, err, "failed to start Redis container") 24 | 25 | // Get host and port 26 | host, err := redisContainer.Host(ctx) 27 | require.NoError(t, err, "failed to get Redis host") 28 | 29 | mappedPort, err := redisContainer.MappedPort(ctx, "6379") 30 | require.NoError(t, err, "failed to get Redis port") 31 | 32 | t.Cleanup(func() { 33 | if err := testcontainers.TerminateContainer(redisContainer); err != nil { 34 | t.Logf("failed to terminate Redis container: %s", err) 35 | } 36 | }) 37 | 38 | return host, mappedPort.Port() 39 | } 40 | 41 | func TestRedisRefreshTokenStore_Integration(t *testing.T) { 42 | host, port := setupRedisContainer(t) 43 | 44 | // Create Redis store configuration 45 | config := &RedisConfig{ 46 | Addr: fmt.Sprintf("%s:%s", host, port), 47 | Password: "", 48 | DB: 0, 49 | CacheSize: 1024 * 1024, // 1MB for testing 50 | CacheTTL: time.Second, 51 | KeyPrefix: "test-jwt:", 52 | } 53 | 54 | store, err := NewRedisRefreshTokenStore(config) 55 | require.NoError(t, err, "failed to create Redis store") 56 | defer func() { 57 | if closeErr := store.Close(); closeErr != nil { 58 | t.Logf("failed to close Redis store: %v", closeErr) 59 | } 60 | }() 61 | 62 | t.Run("BasicOperations", func(t *testing.T) { 63 | testBasicOperations(t, store) 64 | }) 65 | 66 | t.Run("Expiration", func(t *testing.T) { 67 | testExpiration(t, store) 68 | }) 69 | 70 | t.Run("Cleanup", func(t *testing.T) { 71 | testCleanup(t, store) 72 | }) 73 | 74 | t.Run("Count", func(t *testing.T) { 75 | testCount(t, store) 76 | }) 77 | 78 | t.Run("ClientSideCache", func(t *testing.T) { 79 | testClientSideCache(t, store) 80 | }) 81 | } 82 | 83 | func testBasicOperations(t *testing.T, store *RedisRefreshTokenStore) { 84 | ctx := context.Background() 85 | token := "test-token-basic" 86 | userData := map[string]any{ 87 | "user_id": 123, 88 | "username": "testuser", 89 | } 90 | expiry := time.Now().Add(time.Hour) 91 | 92 | // Test Set 93 | err := store.Set(ctx, token, userData, expiry) 94 | assert.NoError(t, err, "Set should not return error") 95 | 96 | // Test Get 97 | retrievedData, err := store.Get(ctx, token) 98 | assert.NoError(t, err, "Get should not return error") 99 | assert.Equal(t, userData, retrievedData, "Retrieved data should match stored data") 100 | 101 | // Test Delete 102 | err = store.Delete(ctx, token) 103 | assert.NoError(t, err, "Delete should not return error") 104 | 105 | // Verify deletion 106 | _, err = store.Get(ctx, token) 107 | assert.ErrorIs(t, err, core.ErrRefreshTokenNotFound, "Token should not be found after deletion") 108 | 109 | // Test empty token 110 | err = store.Set(ctx, "", userData, expiry) 111 | assert.Error(t, err, "Set with empty token should return error") 112 | 113 | _, err = store.Get(ctx, "") 114 | assert.ErrorIs( 115 | t, 116 | err, 117 | core.ErrRefreshTokenNotFound, 118 | "Get with empty token should return not found error", 119 | ) 120 | 121 | err = store.Delete(ctx, "") 122 | assert.NoError(t, err, "Delete with empty token should not return error") 123 | 124 | // Test ping 125 | err = store.Ping() 126 | assert.NoError(t, err, "Ping should not return error") 127 | 128 | // Clean up test data 129 | _ = store.client.Do(ctx, store.client.B().Del().Key(store.buildKey(token)).Build()) 130 | } 131 | 132 | func testExpiration(t *testing.T, store *RedisRefreshTokenStore) { 133 | ctx := context.Background() 134 | token := "test-token-expiry" // #nosec G101 -- This is a test token identifier, not a credential 135 | userData := "test-data" 136 | 137 | // Set token with very short expiry 138 | shortExpiry := time.Now().Add(100 * time.Millisecond) 139 | err := store.Set(ctx, token, userData, shortExpiry) 140 | assert.NoError(t, err, "Set should not return error") 141 | 142 | // Token should be available immediately 143 | retrievedData, err := store.Get(ctx, token) 144 | assert.NoError(t, err, "Get should not return error immediately after set") 145 | assert.Equal(t, userData, retrievedData, "Retrieved data should match stored data") 146 | 147 | // Wait for expiration 148 | time.Sleep(200 * time.Millisecond) 149 | 150 | // Token should be expired 151 | _, err = store.Get(ctx, token) 152 | assert.ErrorIs(t, err, core.ErrRefreshTokenExpired, "Token should be expired") 153 | 154 | // Clean up test data 155 | _ = store.client.Do(ctx, store.client.B().Del().Key(store.buildKey(token)).Build()) 156 | } 157 | 158 | func testCleanup(t *testing.T, store *RedisRefreshTokenStore) { 159 | ctx := context.Background() 160 | 161 | // Create multiple tokens with different expiration times 162 | tokens := []string{"cleanup-token-1", "cleanup-token-2", "cleanup-token-3"} 163 | userData := "cleanup-data" 164 | 165 | // Set tokens with past expiry (already expired) 166 | pastExpiry := time.Now().Add(-time.Hour) 167 | for _, token := range tokens[:2] { 168 | err := store.Set(ctx, token, userData, pastExpiry) 169 | assert.NoError(t, err, "Set should not return error") 170 | } 171 | 172 | // Set one token with future expiry 173 | futureExpiry := time.Now().Add(time.Hour) 174 | err := store.Set(ctx, tokens[2], userData, futureExpiry) 175 | assert.NoError(t, err, "Set should not return error") 176 | 177 | // Run cleanup 178 | cleaned, err := store.Cleanup(ctx) 179 | assert.NoError(t, err, "Cleanup should not return error") 180 | assert.GreaterOrEqual(t, cleaned, 0, "Cleanup should return non-negative count") 181 | 182 | // Verify that non-expired token still exists 183 | _, err = store.Get(ctx, tokens[2]) 184 | assert.NoError(t, err, "Non-expired token should still exist after cleanup") 185 | 186 | // Clean up test data 187 | for _, token := range tokens { 188 | _ = store.client.Do(ctx, store.client.B().Del().Key(store.buildKey(token)).Build()) 189 | } 190 | } 191 | 192 | func testCount(t *testing.T, store *RedisRefreshTokenStore) { 193 | ctx := context.Background() 194 | 195 | // Clear any existing test data first 196 | keys := []string{"count-token-1", "count-token-2", "count-token-3"} 197 | for _, key := range keys { 198 | _ = store.client.Do(ctx, store.client.B().Del().Key(store.buildKey(key)).Build()) 199 | } 200 | 201 | // Get initial count 202 | initialCount, err := store.Count(ctx) 203 | assert.NoError(t, err, "Count should not return error") 204 | 205 | // Add some tokens 206 | userData := "count-data" 207 | expiry := time.Now().Add(time.Hour) 208 | 209 | for i, token := range keys { 210 | err := store.Set(ctx, token, fmt.Sprintf("%s-%d", userData, i), expiry) 211 | assert.NoError(t, err, "Set should not return error") 212 | } 213 | 214 | // Count should increase 215 | newCount, err := store.Count(ctx) 216 | assert.NoError(t, err, "Count should not return error") 217 | assert.GreaterOrEqual(t, newCount, initialCount+len(keys), "Count should include new tokens") 218 | 219 | // Clean up test data 220 | for _, token := range keys { 221 | err := store.Delete(ctx, token) 222 | assert.NoError(t, err, "Delete should not return error") 223 | } 224 | } 225 | 226 | func testClientSideCache(t *testing.T, store *RedisRefreshTokenStore) { 227 | ctx := context.Background() 228 | token := "test-token-cache" 229 | userData := "cache-test-data" 230 | expiry := time.Now().Add(time.Hour) 231 | 232 | // Set token 233 | err := store.Set(ctx, token, userData, expiry) 234 | assert.NoError(t, err, "Set should not return error") 235 | 236 | // First get (should populate cache) 237 | start1 := time.Now() 238 | retrievedData1, err := store.Get(ctx, token) 239 | duration1 := time.Since(start1) 240 | assert.NoError(t, err, "First Get should not return error") 241 | assert.Equal(t, userData, retrievedData1, "First retrieved data should match stored data") 242 | 243 | // Second get (should use cache and be faster) 244 | start2 := time.Now() 245 | retrievedData2, err := store.Get(ctx, token) 246 | duration2 := time.Since(start2) 247 | assert.NoError(t, err, "Second Get should not return error") 248 | assert.Equal(t, userData, retrievedData2, "Second retrieved data should match stored data") 249 | 250 | // Note: Cache performance test might be flaky in CI environments 251 | t.Logf("First get took: %v, Second get took: %v", duration1, duration2) 252 | 253 | // Clean up test data 254 | _ = store.client.Do(ctx, store.client.B().Del().Key(store.buildKey(token)).Build()) 255 | } 256 | 257 | func TestRedisRefreshTokenStore_ConnectionFailure(t *testing.T) { 258 | // Test with invalid Redis configuration 259 | config := &RedisConfig{ 260 | Addr: "invalid-host:6379", 261 | Password: "", 262 | DB: 0, 263 | } 264 | 265 | _, err := NewRedisRefreshTokenStore(config) 266 | assert.Error(t, err, "Should return error for invalid Redis configuration") 267 | assert.Contains( 268 | t, 269 | err.Error(), 270 | "failed to create Redis client", 271 | "Error should mention Redis client creation failure", 272 | ) 273 | } 274 | 275 | func TestRedisRefreshTokenStore_InvalidToken(t *testing.T) { 276 | host, port := setupRedisContainer(t) 277 | 278 | config := &RedisConfig{ 279 | Addr: fmt.Sprintf("%s:%s", host, port), 280 | Password: "", 281 | DB: 0, 282 | KeyPrefix: "test-jwt:", 283 | } 284 | 285 | store, err := NewRedisRefreshTokenStore(config) 286 | require.NoError(t, err) 287 | defer func() { 288 | if closeErr := store.Close(); closeErr != nil { 289 | t.Logf("failed to close Redis store: %v", closeErr) 290 | } 291 | }() 292 | 293 | // Test with expired token in past 294 | expiredTime := time.Now().Add(-time.Hour) 295 | err = store.Set(context.Background(), "expired-token", "data", expiredTime) 296 | assert.Error(t, err, "Should return error when setting token with past expiry time") 297 | assert.Contains( 298 | t, 299 | err.Error(), 300 | "expiry time must be in the future", 301 | "Error should mention future expiry requirement", 302 | ) 303 | } 304 | 305 | func TestDefaultRedisConfig(t *testing.T) { 306 | config := DefaultRedisConfig() 307 | 308 | assert.Equal(t, "localhost:6379", config.Addr, "Default address should be localhost:6379") 309 | assert.Equal(t, "", config.Password, "Default password should be empty") 310 | assert.Equal(t, 0, config.DB, "Default DB should be 0") 311 | assert.Equal(t, 128*1024*1024, config.CacheSize, "Default cache size should be 128MB") 312 | assert.Equal(t, time.Minute, config.CacheTTL, "Default cache TTL should be 1 minute") 313 | assert.Equal(t, "gin-jwt:", config.KeyPrefix, "Default key prefix should be gin-jwt:") 314 | } 315 | -------------------------------------------------------------------------------- /_example/authorization/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | jwt "github.com/appleboy/gin-jwt/v3" 11 | "github.com/gin-gonic/gin" 12 | gojwt "github.com/golang-jwt/jwt/v5" 13 | ) 14 | 15 | type login struct { 16 | Username string `form:"username" json:"username" binding:"required"` 17 | Password string `form:"password" json:"password" binding:"required"` 18 | } 19 | 20 | const roleAdmin = "admin" 21 | 22 | var ( 23 | identityKey = "id" 24 | roleKey = "role" 25 | port string 26 | ) 27 | 28 | type User struct { 29 | UserName string 30 | Role string 31 | } 32 | 33 | func init() { 34 | port = os.Getenv("PORT") 35 | if port == "" { 36 | port = "8000" 37 | } 38 | } 39 | 40 | func main() { 41 | engine := gin.Default() 42 | 43 | // Create middleware with comprehensive authorizer 44 | authMiddleware, err := jwt.New(initParams()) 45 | if err != nil { 46 | log.Fatal("JWT Error:" + err.Error()) 47 | } 48 | 49 | // Initialize middleware 50 | errInit := authMiddleware.MiddlewareInit() 51 | if errInit != nil { 52 | log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error()) 53 | } 54 | 55 | // Register routes 56 | registerRoutes(engine, authMiddleware) 57 | 58 | log.Printf("Server starting on port %s", port) 59 | log.Println("Available users:") 60 | log.Println(" admin/admin (role: admin)") 61 | log.Println(" user/user (role: user)") 62 | log.Println(" guest/guest (role: guest)") 63 | 64 | // Start server with proper timeouts 65 | srv := &http.Server{ 66 | Addr: ":" + port, 67 | Handler: engine, 68 | ReadHeaderTimeout: 5 * time.Second, 69 | } 70 | if err = srv.ListenAndServe(); err != nil { 71 | log.Fatal(err) 72 | } 73 | } 74 | 75 | func registerRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware) { 76 | // Public routes 77 | r.POST("/login", authMiddleware.LoginHandler) 78 | r.POST("/refresh", authMiddleware.RefreshHandler) 79 | 80 | // Public info endpoint 81 | r.GET("/info", func(c *gin.Context) { 82 | c.JSON(200, gin.H{ 83 | "message": "Authorization Example API", 84 | "users": gin.H{ 85 | "admin": gin.H{"password": "admin", "role": "admin", "access": "All routes"}, 86 | "user": gin.H{ 87 | "password": "user", 88 | "role": "user", 89 | "access": "/user/* and /auth/profile", 90 | }, 91 | "guest": gin.H{"password": "guest", "role": "guest", "access": "/auth/hello only"}, 92 | }, 93 | "routes": gin.H{ 94 | "public": []string{"/login", "/refresh", "/info"}, 95 | "admin": []string{"/admin/users", "/admin/settings", "/admin/reports"}, 96 | "user": []string{"/user/profile", "/user/settings"}, 97 | "auth": []string{"/auth/hello", "/auth/profile", "/auth/logout"}, 98 | }, 99 | }) 100 | }) 101 | 102 | // Admin routes - only admin role can access 103 | adminRoutes := r.Group("/admin", authMiddleware.MiddlewareFunc()) 104 | { 105 | adminRoutes.GET("/users", adminUsersHandler) 106 | adminRoutes.GET("/settings", adminSettingsHandler) 107 | adminRoutes.GET("/reports", adminReportsHandler) 108 | adminRoutes.POST("/users", createUserHandler) 109 | adminRoutes.DELETE("/users/:id", deleteUserHandler) 110 | } 111 | 112 | // User routes - user and admin roles can access 113 | userRoutes := r.Group("/user", authMiddleware.MiddlewareFunc()) 114 | { 115 | userRoutes.GET("/profile", userProfileHandler) 116 | userRoutes.PUT("/profile", updateProfileHandler) 117 | userRoutes.GET("/settings", userSettingsHandler) 118 | } 119 | 120 | // General auth routes - different permissions based on path 121 | authRoutes := r.Group("/auth", authMiddleware.MiddlewareFunc()) 122 | { 123 | authRoutes.GET("/hello", helloHandler) // All authenticated users 124 | authRoutes.GET("/profile", profileHandler) // User and admin only 125 | authRoutes.POST("/logout", authMiddleware.LogoutHandler) // User Logout 126 | authRoutes.GET("/whoami", whoAmIHandler) // All authenticated users 127 | } 128 | } 129 | 130 | func initParams() *jwt.GinJWTMiddleware { 131 | return &jwt.GinJWTMiddleware{ 132 | Realm: "authorization example", 133 | Key: []byte("secret key"), 134 | Timeout: time.Hour, 135 | MaxRefresh: time.Hour, 136 | IdentityKey: identityKey, 137 | PayloadFunc: payloadFunc(), 138 | 139 | IdentityHandler: identityHandler(), 140 | Authenticator: authenticator(), 141 | Authorizer: authorizator(), 142 | Unauthorized: unauthorized(), 143 | TokenLookup: "header: Authorization, query: token, cookie: jwt", 144 | TokenHeadName: "Bearer", 145 | TimeFunc: time.Now, 146 | } 147 | } 148 | 149 | func payloadFunc() func(data any) gojwt.MapClaims { 150 | return func(data any) gojwt.MapClaims { 151 | if v, ok := data.(*User); ok { 152 | return gojwt.MapClaims{ 153 | identityKey: v.UserName, 154 | roleKey: v.Role, 155 | } 156 | } 157 | return gojwt.MapClaims{} 158 | } 159 | } 160 | 161 | func identityHandler() func(c *gin.Context) any { 162 | return func(c *gin.Context) any { 163 | claims := jwt.ExtractClaims(c) 164 | role, _ := claims[roleKey].(string) 165 | return &User{ 166 | UserName: claims[identityKey].(string), 167 | Role: role, 168 | } 169 | } 170 | } 171 | 172 | func authenticator() func(c *gin.Context) (any, error) { 173 | return func(c *gin.Context) (any, error) { 174 | var loginVals login 175 | if err := c.ShouldBind(&loginVals); err != nil { 176 | return "", jwt.ErrMissingLoginValues 177 | } 178 | 179 | userID := loginVals.Username 180 | password := loginVals.Password 181 | 182 | // Define users with their roles 183 | users := map[string]map[string]string{ 184 | "admin": {"password": "admin", "role": "admin"}, 185 | "user": {"password": "user", "role": "user"}, 186 | "guest": {"password": "guest", "role": "guest"}, 187 | } 188 | 189 | if userData, exists := users[userID]; exists && userData["password"] == password { 190 | return &User{ 191 | UserName: userID, 192 | Role: userData["role"], 193 | }, nil 194 | } 195 | 196 | return nil, jwt.ErrFailedAuthentication 197 | } 198 | } 199 | 200 | // Comprehensive authorizer that demonstrates different authorization patterns 201 | func authorizator() func(c *gin.Context, data any) bool { 202 | return func(c *gin.Context, data any) bool { 203 | user, ok := data.(*User) 204 | if !ok { 205 | return false 206 | } 207 | 208 | path := c.Request.URL.Path 209 | method := c.Request.Method 210 | 211 | log.Printf("Authorization check - User: %s, Role: %s, Path: %s, Method: %s", 212 | user.UserName, user.Role, path, method) 213 | 214 | // Admin has access to everything 215 | if user.Role == roleAdmin { 216 | return true 217 | } 218 | 219 | // Admin routes - only admin allowed (already handled above, but explicit for clarity) 220 | if strings.HasPrefix(path, "/admin/") { 221 | return user.Role == roleAdmin 222 | } 223 | 224 | // User routes - user and admin roles allowed 225 | if strings.HasPrefix(path, "/user/") { 226 | return user.Role == "user" || user.Role == roleAdmin 227 | } 228 | 229 | // Auth routes with specific rules 230 | if strings.HasPrefix(path, "/auth/") { 231 | switch path { 232 | case "/auth/hello", "/auth/whoami", "/auth/logout": 233 | // All authenticated users can access 234 | return true 235 | case "/auth/profile": 236 | // Only user and admin roles 237 | return user.Role == "user" || user.Role == roleAdmin 238 | } 239 | } 240 | 241 | // Default: deny access 242 | return false 243 | } 244 | } 245 | 246 | func unauthorized() func(c *gin.Context, code int, message string) { 247 | return func(c *gin.Context, code int, message string) { 248 | c.JSON(code, gin.H{ 249 | "code": code, 250 | "message": message, 251 | "path": c.Request.URL.Path, 252 | "method": c.Request.Method, 253 | }) 254 | } 255 | } 256 | 257 | // Handler functions 258 | func adminUsersHandler(c *gin.Context) { 259 | c.JSON(200, gin.H{ 260 | "message": "Admin Users Management", 261 | "users": []string{"admin", "user1", "user2", "guest1"}, 262 | "access": "admin only", 263 | }) 264 | } 265 | 266 | func adminSettingsHandler(c *gin.Context) { 267 | c.JSON(200, gin.H{ 268 | "message": "Admin Settings", 269 | "settings": gin.H{"max_users": 100, "allow_registration": true}, 270 | "access": "admin only", 271 | }) 272 | } 273 | 274 | func adminReportsHandler(c *gin.Context) { 275 | c.JSON(200, gin.H{ 276 | "message": "Admin Reports", 277 | "reports": []string{"daily_usage", "user_activity", "system_health"}, 278 | "access": "admin only", 279 | }) 280 | } 281 | 282 | func createUserHandler(c *gin.Context) { 283 | c.JSON(200, gin.H{ 284 | "message": "User created successfully", 285 | "access": "admin only", 286 | }) 287 | } 288 | 289 | func deleteUserHandler(c *gin.Context) { 290 | userID := c.Param("id") 291 | c.JSON(200, gin.H{ 292 | "message": "User deleted successfully", 293 | "user_id": userID, 294 | "access": "admin only", 295 | }) 296 | } 297 | 298 | func userProfileHandler(c *gin.Context) { 299 | user, _ := c.Get(identityKey) 300 | c.JSON(200, gin.H{ 301 | "message": "User Profile", 302 | "username": user.(*User).UserName, 303 | "role": user.(*User).Role, 304 | "access": "user and admin only", 305 | }) 306 | } 307 | 308 | func updateProfileHandler(c *gin.Context) { 309 | user, _ := c.Get(identityKey) 310 | c.JSON(200, gin.H{ 311 | "message": "Profile updated successfully", 312 | "username": user.(*User).UserName, 313 | "access": "user and admin only", 314 | }) 315 | } 316 | 317 | func userSettingsHandler(c *gin.Context) { 318 | user, _ := c.Get(identityKey) 319 | c.JSON(200, gin.H{ 320 | "message": "User Settings", 321 | "username": user.(*User).UserName, 322 | "settings": gin.H{"theme": "dark", "notifications": true}, 323 | "access": "user and admin only", 324 | }) 325 | } 326 | 327 | func helloHandler(c *gin.Context) { 328 | claims := jwt.ExtractClaims(c) 329 | user, _ := c.Get(identityKey) 330 | c.JSON(200, gin.H{ 331 | "message": "Hello World!", 332 | "userID": claims[identityKey], 333 | "userName": user.(*User).UserName, 334 | "role": user.(*User).Role, 335 | "access": "all authenticated users", 336 | }) 337 | } 338 | 339 | func profileHandler(c *gin.Context) { 340 | claims := jwt.ExtractClaims(c) 341 | user, _ := c.Get(identityKey) 342 | c.JSON(200, gin.H{ 343 | "message": "Profile Information", 344 | "userID": claims[identityKey], 345 | "userName": user.(*User).UserName, 346 | "role": user.(*User).Role, 347 | "access": "user and admin roles only", 348 | }) 349 | } 350 | 351 | func whoAmIHandler(c *gin.Context) { 352 | claims := jwt.ExtractClaims(c) 353 | user, _ := c.Get(identityKey) 354 | c.JSON(200, gin.H{ 355 | "identity": claims[identityKey], 356 | "role": claims[roleKey], 357 | "user": user.(*User), 358 | "claims": claims, 359 | "access": "all authenticated users", 360 | }) 361 | } 362 | -------------------------------------------------------------------------------- /store/memory_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // User represents a test user for testing purposes 14 | type User struct { 15 | ID string `json:"id"` 16 | Username string `json:"username"` 17 | Email string `json:"email"` 18 | } 19 | 20 | func TestNewInMemoryRefreshTokenStore(t *testing.T) { 21 | store := NewInMemoryRefreshTokenStore() 22 | 23 | if store == nil { 24 | t.Fatal("NewInMemoryRefreshTokenStore returned nil") 25 | } 26 | 27 | if store.tokens == nil { 28 | t.Fatal("store.tokens is nil") 29 | } 30 | 31 | count, err := store.Count(context.Background()) 32 | if err != nil { 33 | t.Fatalf("Count() returned error: %v", err) 34 | } 35 | 36 | if count != 0 { 37 | t.Fatalf("Expected count to be 0, got %d", count) 38 | } 39 | } 40 | 41 | func TestInMemoryRefreshTokenStore_Set(t *testing.T) { 42 | store := NewInMemoryRefreshTokenStore() 43 | user := &User{ID: "123", Username: "testuser", Email: "test@example.com"} 44 | expiry := time.Now().Add(time.Hour) 45 | 46 | err := store.Set(context.Background(), "token123", user, expiry) 47 | if err != nil { 48 | t.Fatalf("Set() returned error: %v", err) 49 | } 50 | 51 | count, _ := store.Count(context.Background()) 52 | if count != 1 { 53 | t.Fatalf("Expected count to be 1, got %d", count) 54 | } 55 | } 56 | 57 | func TestInMemoryRefreshTokenStore_SetEmptyToken(t *testing.T) { 58 | store := NewInMemoryRefreshTokenStore() 59 | user := &User{ID: "123", Username: "testuser"} 60 | expiry := time.Now().Add(time.Hour) 61 | 62 | err := store.Set(context.Background(), "", user, expiry) 63 | if err == nil { 64 | t.Fatal("Set() should return error for empty token") 65 | } 66 | 67 | if err.Error() != "token cannot be empty" { 68 | t.Fatalf("Expected 'token cannot be empty' error, got: %v", err) 69 | } 70 | } 71 | 72 | func TestInMemoryRefreshTokenStore_Get(t *testing.T) { 73 | store := NewInMemoryRefreshTokenStore() 74 | user := &User{ID: "123", Username: "testuser", Email: "test@example.com"} 75 | expiry := time.Now().Add(time.Hour) 76 | 77 | // Set a token 78 | err := store.Set(context.Background(), "token123", user, expiry) 79 | if err != nil { 80 | t.Fatalf("Set() returned error: %v", err) 81 | } 82 | 83 | // Get the token 84 | userData, err := store.Get(context.Background(), "token123") 85 | if err != nil { 86 | t.Fatalf("Get() returned error: %v", err) 87 | } 88 | 89 | retrievedUser, ok := userData.(*User) 90 | if !ok { 91 | t.Fatal("Retrieved user data is not of type *User") 92 | } 93 | 94 | if retrievedUser.ID != user.ID || retrievedUser.Username != user.Username || 95 | retrievedUser.Email != user.Email { 96 | t.Fatalf("Retrieved user data doesn't match. Expected: %+v, Got: %+v", user, retrievedUser) 97 | } 98 | } 99 | 100 | func TestInMemoryRefreshTokenStore_GetNonExistent(t *testing.T) { 101 | store := NewInMemoryRefreshTokenStore() 102 | 103 | _, err := store.Get(context.Background(), "nonexistent") 104 | if err != ErrRefreshTokenNotFound { 105 | t.Fatalf("Expected ErrRefreshTokenNotFound, got: %v", err) 106 | } 107 | } 108 | 109 | func TestInMemoryRefreshTokenStore_GetEmptyToken(t *testing.T) { 110 | store := NewInMemoryRefreshTokenStore() 111 | 112 | _, err := store.Get(context.Background(), "") 113 | if err != ErrRefreshTokenNotFound { 114 | t.Fatalf("Expected ErrRefreshTokenNotFound for empty token, got: %v", err) 115 | } 116 | } 117 | 118 | func TestInMemoryRefreshTokenStore_GetExpired(t *testing.T) { 119 | store := NewInMemoryRefreshTokenStore() 120 | user := &User{ID: "123", Username: "testuser"} 121 | expiry := time.Now().Add(-time.Hour) // Expired 1 hour ago 122 | 123 | // Set an expired token 124 | err := store.Set(context.Background(), "expired_token", user, expiry) 125 | if err != nil { 126 | t.Fatalf("Set() returned error: %v", err) 127 | } 128 | 129 | // Try to get the expired token 130 | _, err = store.Get(context.Background(), "expired_token") 131 | if err != ErrRefreshTokenNotFound { 132 | t.Fatalf("Expected ErrRefreshTokenNotFound for expired token, got: %v", err) 133 | } 134 | 135 | // Verify the expired token was cleaned up 136 | count, _ := store.Count(context.Background()) 137 | if count != 0 { 138 | t.Fatalf("Expected count to be 0 after expired token cleanup, got %d", count) 139 | } 140 | } 141 | 142 | func TestInMemoryRefreshTokenStore_Delete(t *testing.T) { 143 | store := NewInMemoryRefreshTokenStore() 144 | user := &User{ID: "123", Username: "testuser"} 145 | expiry := time.Now().Add(time.Hour) 146 | 147 | // Set a token 148 | err := store.Set(context.Background(), "token123", user, expiry) 149 | if err != nil { 150 | t.Fatalf("Set() returned error: %v", err) 151 | } 152 | 153 | // Delete the token 154 | err = store.Delete(context.Background(), "token123") 155 | if err != nil { 156 | t.Fatalf("Delete() returned error: %v", err) 157 | } 158 | 159 | // Verify the token is gone 160 | _, err = store.Get(context.Background(), "token123") 161 | if err != ErrRefreshTokenNotFound { 162 | t.Fatalf("Expected ErrRefreshTokenNotFound after deletion, got: %v", err) 163 | } 164 | 165 | count, _ := store.Count(context.Background()) 166 | if count != 0 { 167 | t.Fatalf("Expected count to be 0 after deletion, got %d", count) 168 | } 169 | } 170 | 171 | func TestInMemoryRefreshTokenStore_DeleteNonExistent(t *testing.T) { 172 | store := NewInMemoryRefreshTokenStore() 173 | 174 | // Should not return error for deleting non-existent token 175 | err := store.Delete(context.Background(), "nonexistent") 176 | if err != nil { 177 | t.Fatalf("Delete() should not return error for non-existent token, got: %v", err) 178 | } 179 | } 180 | 181 | func TestInMemoryRefreshTokenStore_DeleteEmptyToken(t *testing.T) { 182 | store := NewInMemoryRefreshTokenStore() 183 | 184 | // Should not return error for empty token 185 | err := store.Delete(context.Background(), "") 186 | if err != nil { 187 | t.Fatalf("Delete() should not return error for empty token, got: %v", err) 188 | } 189 | } 190 | 191 | func TestInMemoryRefreshTokenStore_Cleanup(t *testing.T) { 192 | store := NewInMemoryRefreshTokenStore() 193 | 194 | // Add some tokens: 2 valid, 2 expired 195 | validExpiry := time.Now().Add(time.Hour) 196 | expiredExpiry := time.Now().Add(-time.Hour) 197 | 198 | err := store.Set(context.Background(), "valid1", &User{ID: "1"}, validExpiry) 199 | assert.NoError(t, err) 200 | err = store.Set(context.Background(), "valid2", &User{ID: "2"}, validExpiry) 201 | assert.NoError(t, err) 202 | err = store.Set(context.Background(), "expired1", &User{ID: "3"}, expiredExpiry) 203 | assert.NoError(t, err) 204 | err = store.Set(context.Background(), "expired2", &User{ID: "4"}, expiredExpiry) 205 | assert.NoError(t, err) 206 | 207 | // Verify initial count 208 | count, _ := store.Count(context.Background()) 209 | if count != 4 { 210 | t.Fatalf("Expected initial count to be 4, got %d", count) 211 | } 212 | 213 | // Cleanup expired tokens 214 | cleaned, err := store.Cleanup(context.Background()) 215 | if err != nil { 216 | t.Fatalf("Cleanup() returned error: %v", err) 217 | } 218 | 219 | if cleaned != 2 { 220 | t.Fatalf("Expected 2 tokens to be cleaned up, got %d", cleaned) 221 | } 222 | 223 | // Verify final count 224 | count, _ = store.Count(context.Background()) 225 | if count != 2 { 226 | t.Fatalf("Expected final count to be 2, got %d", count) 227 | } 228 | 229 | // Verify valid tokens still exist 230 | _, err = store.Get(context.Background(), "valid1") 231 | if err != nil { 232 | t.Fatalf("valid1 token should still exist: %v", err) 233 | } 234 | 235 | _, err = store.Get(context.Background(), "valid2") 236 | if err != nil { 237 | t.Fatalf("valid2 token should still exist: %v", err) 238 | } 239 | 240 | // Verify expired tokens are gone 241 | _, err = store.Get(context.Background(), "expired1") 242 | if err != ErrRefreshTokenNotFound { 243 | t.Fatalf("expired1 token should be gone: %v", err) 244 | } 245 | 246 | _, err = store.Get(context.Background(), "expired2") 247 | if err != ErrRefreshTokenNotFound { 248 | t.Fatalf("expired2 token should be gone: %v", err) 249 | } 250 | } 251 | 252 | func TestInMemoryRefreshTokenStore_Count(t *testing.T) { 253 | store := NewInMemoryRefreshTokenStore() 254 | 255 | // Initially empty 256 | count, err := store.Count(context.Background()) 257 | if err != nil { 258 | t.Fatalf("Count() returned error: %v", err) 259 | } 260 | if count != 0 { 261 | t.Fatalf("Expected initial count to be 0, got %d", count) 262 | } 263 | 264 | // Add tokens 265 | expiry := time.Now().Add(time.Hour) 266 | for i := 0; i < 5; i++ { 267 | token := fmt.Sprintf("token%d", i) 268 | user := &User{ID: fmt.Sprintf("%d", i)} 269 | _ = store.Set(context.Background(), token, user, expiry) 270 | } 271 | 272 | count, err = store.Count(context.Background()) 273 | if err != nil { 274 | t.Fatalf("Count() returned error: %v", err) 275 | } 276 | if count != 5 { 277 | t.Fatalf("Expected count to be 5, got %d", count) 278 | } 279 | } 280 | 281 | func TestInMemoryRefreshTokenStore_GetAll(t *testing.T) { 282 | store := NewInMemoryRefreshTokenStore() 283 | 284 | // Add some tokens 285 | validExpiry := time.Now().Add(time.Hour) 286 | expiredExpiry := time.Now().Add(-time.Hour) 287 | 288 | _ = store.Set(context.Background(), "valid1", &User{ID: "1"}, validExpiry) 289 | _ = store.Set(context.Background(), "valid2", &User{ID: "2"}, validExpiry) 290 | _ = store.Set(context.Background(), "expired1", &User{ID: "3"}, expiredExpiry) 291 | 292 | all := store.GetAll() 293 | 294 | // Should only return valid tokens 295 | if len(all) != 2 { 296 | t.Fatalf("Expected 2 valid tokens, got %d", len(all)) 297 | } 298 | 299 | if _, exists := all["valid1"]; !exists { 300 | t.Fatal("valid1 should be in GetAll() result") 301 | } 302 | 303 | if _, exists := all["valid2"]; !exists { 304 | t.Fatal("valid2 should be in GetAll() result") 305 | } 306 | 307 | if _, exists := all["expired1"]; exists { 308 | t.Fatal("expired1 should not be in GetAll() result") 309 | } 310 | } 311 | 312 | func TestInMemoryRefreshTokenStore_Clear(t *testing.T) { 313 | store := NewInMemoryRefreshTokenStore() 314 | 315 | // Add some tokens 316 | expiry := time.Now().Add(time.Hour) 317 | _ = store.Set(context.Background(), "token1", &User{ID: "1"}, expiry) 318 | _ = store.Set(context.Background(), "token2", &User{ID: "2"}, expiry) 319 | 320 | count, _ := store.Count(context.Background()) 321 | if count != 2 { 322 | t.Fatalf("Expected count to be 2, got %d", count) 323 | } 324 | 325 | // Clear all tokens 326 | store.Clear() 327 | 328 | count, _ = store.Count(context.Background()) 329 | if count != 0 { 330 | t.Fatalf("Expected count to be 0 after Clear(), got %d", count) 331 | } 332 | } 333 | 334 | // TestInMemoryRefreshTokenStore_ConcurrentAccess tests thread safety 335 | func TestInMemoryRefreshTokenStore_ConcurrentAccess(t *testing.T) { 336 | store := NewInMemoryRefreshTokenStore() 337 | var wg sync.WaitGroup 338 | numGoroutines := 100 339 | 340 | // Concurrent writes 341 | wg.Add(numGoroutines) 342 | for i := 0; i < numGoroutines; i++ { 343 | go func(id int) { 344 | defer wg.Done() 345 | token := fmt.Sprintf("token%d", id) 346 | user := &User{ID: fmt.Sprintf("%d", id)} 347 | expiry := time.Now().Add(time.Hour) 348 | _ = store.Set(context.Background(), token, user, expiry) 349 | }(i) 350 | } 351 | wg.Wait() 352 | 353 | // Verify all tokens were added 354 | count, _ := store.Count(context.Background()) 355 | if count != numGoroutines { 356 | t.Fatalf("Expected count to be %d, got %d", numGoroutines, count) 357 | } 358 | 359 | // Concurrent reads 360 | wg.Add(numGoroutines) 361 | for i := 0; i < numGoroutines; i++ { 362 | go func(id int) { 363 | defer wg.Done() 364 | token := fmt.Sprintf("token%d", id) 365 | _, err := store.Get(context.Background(), token) 366 | if err != nil { 367 | t.Errorf("Failed to get token%d: %v", id, err) 368 | } 369 | }(i) 370 | } 371 | wg.Wait() 372 | 373 | // Concurrent deletes 374 | wg.Add(numGoroutines) 375 | for i := 0; i < numGoroutines; i++ { 376 | go func(id int) { 377 | defer wg.Done() 378 | token := fmt.Sprintf("token%d", id) 379 | _ = store.Delete(context.Background(), token) 380 | }(i) 381 | } 382 | wg.Wait() 383 | 384 | // Verify all tokens were deleted 385 | count, _ = store.Count(context.Background()) 386 | if count != 0 { 387 | t.Fatalf("Expected count to be 0 after concurrent deletes, got %d", count) 388 | } 389 | } 390 | 391 | func TestRefreshTokenData_IsExpired(t *testing.T) { 392 | // Test non-expired token 393 | data := &RefreshTokenData{ 394 | UserData: &User{ID: "123"}, 395 | Expiry: time.Now().Add(time.Hour), 396 | Created: time.Now(), 397 | } 398 | 399 | if data.IsExpired() { 400 | t.Fatal("Token should not be expired") 401 | } 402 | 403 | // Test expired token 404 | data.Expiry = time.Now().Add(-time.Hour) 405 | if !data.IsExpired() { 406 | t.Fatal("Token should be expired") 407 | } 408 | } 409 | 410 | // Benchmark tests 411 | func BenchmarkInMemoryRefreshTokenStore_Set(b *testing.B) { 412 | store := NewInMemoryRefreshTokenStore() 413 | user := &User{ID: "123", Username: "testuser"} 414 | expiry := time.Now().Add(time.Hour) 415 | 416 | b.ResetTimer() 417 | for i := 0; i < b.N; i++ { 418 | token := fmt.Sprintf("token%d", i) 419 | _ = store.Set(context.Background(), token, user, expiry) 420 | } 421 | } 422 | 423 | func BenchmarkInMemoryRefreshTokenStore_Get(b *testing.B) { 424 | store := NewInMemoryRefreshTokenStore() 425 | user := &User{ID: "123", Username: "testuser"} 426 | expiry := time.Now().Add(time.Hour) 427 | 428 | // Pre-populate with tokens 429 | for i := 0; i < 1000; i++ { 430 | token := fmt.Sprintf("token%d", i) 431 | _ = store.Set(context.Background(), token, user, expiry) 432 | } 433 | 434 | b.ResetTimer() 435 | for i := 0; i < b.N; i++ { 436 | token := fmt.Sprintf("token%d", i%1000) 437 | _, _ = store.Get(context.Background(), token) 438 | } 439 | } 440 | 441 | func BenchmarkInMemoryRefreshTokenStore_Delete(b *testing.B) { 442 | store := NewInMemoryRefreshTokenStore() 443 | user := &User{ID: "123", Username: "testuser"} 444 | expiry := time.Now().Add(time.Hour) 445 | 446 | // Pre-populate with tokens 447 | for i := 0; i < b.N; i++ { 448 | token := fmt.Sprintf("token%d", i) 449 | _ = store.Set(context.Background(), token, user, expiry) 450 | } 451 | 452 | b.ResetTimer() 453 | for i := 0; i < b.N; i++ { 454 | token := fmt.Sprintf("token%d", i) 455 | _ = store.Delete(context.Background(), token) 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /auth_jwt_redis_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/appleboy/gin-jwt/v3/store" 14 | "github.com/gin-gonic/gin" 15 | gojwt "github.com/golang-jwt/jwt/v5" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | "github.com/testcontainers/testcontainers-go/modules/redis" 19 | ) 20 | 21 | func setupRedisContainerForJWT(t *testing.T) (string, string) { 22 | ctx := context.Background() 23 | t.Helper() 24 | 25 | // Start Redis container 26 | redisContainer, err := redis.Run(ctx, "redis:8-alpine") 27 | require.NoError(t, err, "failed to start Redis container") 28 | 29 | // Get host and port 30 | host, err := redisContainer.Host(ctx) 31 | require.NoError(t, err, "failed to get Redis host") 32 | 33 | mappedPort, err := redisContainer.MappedPort(ctx, "6379/tcp") 34 | require.NoError(t, err, "failed to get Redis port") 35 | 36 | t.Cleanup(func() { 37 | if err := redisContainer.Terminate(ctx); err != nil { 38 | t.Logf("failed to terminate Redis container: %s", err) 39 | } 40 | }) 41 | 42 | return host, mappedPort.Port() 43 | } 44 | 45 | func TestGinJWTMiddleware_RedisStore_Integration(t *testing.T) { 46 | gin.SetMode(gin.TestMode) 47 | 48 | host, port := setupRedisContainerForJWT(t) 49 | 50 | // Create middleware with Redis store 51 | middleware := createTestMiddleware(fmt.Sprintf("%s:%s", host, port)) 52 | 53 | // Initialize middleware 54 | err := middleware.MiddlewareInit() 55 | require.NoError(t, err, "middleware initialization should not fail") 56 | 57 | // Create test router 58 | r := gin.New() 59 | r.POST("/login", middleware.LoginHandler) 60 | r.POST("/refresh", middleware.RefreshHandler) 61 | 62 | auth := r.Group("/auth") 63 | auth.Use(middleware.MiddlewareFunc()) 64 | { 65 | auth.GET("/hello", func(c *gin.Context) { 66 | c.JSON(200, gin.H{"message": "hello"}) 67 | }) 68 | } 69 | 70 | t.Run("LoginAndRefreshWithRedis", func(t *testing.T) { 71 | testLoginAndRefreshFlow(t, r) 72 | }) 73 | 74 | t.Run("TokenPersistenceAcrossRequests", func(t *testing.T) { 75 | testTokenPersistenceAcrossRequests(t, r) 76 | }) 77 | 78 | t.Run("RedisStoreOperations", func(t *testing.T) { 79 | testRedisStoreOperations(t, middleware) 80 | }) 81 | } 82 | 83 | func TestGinJWTMiddleware_RedisStoreFallback(t *testing.T) { 84 | gin.SetMode(gin.TestMode) 85 | 86 | // Create middleware with invalid Redis configuration (should fallback to memory) 87 | middleware := &GinJWTMiddleware{ 88 | Realm: "test zone", 89 | Key: []byte("secret key"), 90 | Timeout: time.Hour, 91 | MaxRefresh: time.Hour * 24, 92 | IdentityKey: "id", 93 | Authenticator: testAuthenticator, 94 | PayloadFunc: testPayloadFunc, 95 | // Enable Redis with invalid config to test fallback 96 | UseRedisStore: true, 97 | RedisConfig: &store.RedisConfig{ 98 | Addr: "invalid-host:6379", // This should fail 99 | }, 100 | } 101 | 102 | // Initialize middleware (should fallback to memory store) 103 | err := middleware.MiddlewareInit() 104 | require.NoError(t, err, "middleware initialization should not fail even with invalid Redis") 105 | 106 | // Verify that it fell back to in-memory store 107 | assert.NotNil(t, middleware.inMemoryStore, "should have created in-memory store as fallback") 108 | assert.Equal( 109 | t, 110 | middleware.RefreshTokenStore, 111 | middleware.inMemoryStore, 112 | "should use in-memory store as fallback", 113 | ) 114 | } 115 | 116 | func TestGinJWTMiddleware_FunctionalOptions(t *testing.T) { 117 | gin.SetMode(gin.TestMode) 118 | 119 | host, port := setupRedisContainerForJWT(t) 120 | 121 | redisAddr := fmt.Sprintf("%s:%s", host, port) 122 | 123 | t.Run("EnableRedisStoreDefault", func(t *testing.T) { 124 | middleware := &GinJWTMiddleware{ 125 | Realm: "test zone", 126 | Key: []byte("secret key"), 127 | Timeout: time.Hour, 128 | MaxRefresh: time.Hour * 24, 129 | IdentityKey: "id", 130 | } 131 | 132 | // Test EnableRedisStore with no options (default) 133 | result := middleware.EnableRedisStore() 134 | assert.Equal(t, middleware, result, "should return self for chaining") 135 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 136 | assert.NotNil(t, middleware.RedisConfig, "should set default Redis config") 137 | assert.Equal(t, "localhost:6379", middleware.RedisConfig.Addr, "should use default address") 138 | }) 139 | 140 | t.Run("EnableRedisStoreWithAddr", func(t *testing.T) { 141 | middleware := &GinJWTMiddleware{ 142 | Realm: "test zone", 143 | Key: []byte("secret key"), 144 | Timeout: time.Hour, 145 | MaxRefresh: time.Hour * 24, 146 | IdentityKey: "id", 147 | } 148 | 149 | // Test EnableRedisStore with address option 150 | result := middleware.EnableRedisStore(WithRedisAddr(redisAddr)) 151 | assert.Equal(t, middleware, result, "should return self for chaining") 152 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 153 | assert.Equal(t, redisAddr, middleware.RedisConfig.Addr, "should set custom address") 154 | }) 155 | 156 | t.Run("EnableRedisStoreWithAuth", func(t *testing.T) { 157 | middleware := &GinJWTMiddleware{ 158 | Realm: "test zone", 159 | Key: []byte("secret key"), 160 | Timeout: time.Hour, 161 | MaxRefresh: time.Hour * 24, 162 | IdentityKey: "id", 163 | } 164 | 165 | // Test EnableRedisStore with auth options 166 | result := middleware.EnableRedisStore( 167 | WithRedisAddr(redisAddr), 168 | WithRedisAuth("testpass", 1), 169 | ) 170 | assert.Equal(t, middleware, result, "should return self for chaining") 171 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 172 | assert.Equal(t, redisAddr, middleware.RedisConfig.Addr, "should set custom address") 173 | assert.Equal(t, "testpass", middleware.RedisConfig.Password, "should set custom password") 174 | assert.Equal(t, 1, middleware.RedisConfig.DB, "should set custom DB") 175 | }) 176 | 177 | t.Run("EnableRedisStoreWithCache", func(t *testing.T) { 178 | middleware := &GinJWTMiddleware{ 179 | Realm: "test zone", 180 | Key: []byte("secret key"), 181 | Timeout: time.Hour, 182 | MaxRefresh: time.Hour * 24, 183 | IdentityKey: "id", 184 | } 185 | 186 | // Test EnableRedisStore with cache options 187 | cacheSize := 64 * 1024 * 1024 // 64MB 188 | cacheTTL := 30 * time.Second 189 | result := middleware.EnableRedisStore( 190 | WithRedisAddr(redisAddr), 191 | WithRedisCache(cacheSize, cacheTTL), 192 | ) 193 | assert.Equal(t, middleware, result, "should return self for chaining") 194 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 195 | assert.Equal(t, redisAddr, middleware.RedisConfig.Addr, "should set address") 196 | assert.Equal(t, cacheSize, middleware.RedisConfig.CacheSize, "should set cache size") 197 | assert.Equal(t, cacheTTL, middleware.RedisConfig.CacheTTL, "should set cache TTL") 198 | }) 199 | 200 | t.Run("EnableRedisStoreWithPool", func(t *testing.T) { 201 | middleware := &GinJWTMiddleware{ 202 | Realm: "test zone", 203 | Key: []byte("secret key"), 204 | Timeout: time.Hour, 205 | MaxRefresh: time.Hour * 24, 206 | IdentityKey: "id", 207 | } 208 | 209 | // Test EnableRedisStore with pool options 210 | poolSize := 20 211 | maxIdleTime := time.Hour 212 | maxLifetime := 2 * time.Hour 213 | result := middleware.EnableRedisStore( 214 | WithRedisAddr(redisAddr), 215 | WithRedisPool(poolSize, maxIdleTime, maxLifetime), 216 | ) 217 | assert.Equal(t, middleware, result, "should return self for chaining") 218 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 219 | assert.Equal(t, poolSize, middleware.RedisConfig.PoolSize, "should set pool size") 220 | assert.Equal( 221 | t, 222 | maxIdleTime, 223 | middleware.RedisConfig.ConnMaxIdleTime, 224 | "should set max idle time", 225 | ) 226 | assert.Equal( 227 | t, 228 | maxLifetime, 229 | middleware.RedisConfig.ConnMaxLifetime, 230 | "should set max lifetime", 231 | ) 232 | }) 233 | 234 | t.Run("EnableRedisStoreWithKeyPrefix", func(t *testing.T) { 235 | middleware := &GinJWTMiddleware{ 236 | Realm: "test zone", 237 | Key: []byte("secret key"), 238 | Timeout: time.Hour, 239 | MaxRefresh: time.Hour * 24, 240 | IdentityKey: "id", 241 | } 242 | 243 | // Test EnableRedisStore with key prefix option 244 | keyPrefix := "test-jwt:" 245 | result := middleware.EnableRedisStore( 246 | WithRedisAddr(redisAddr), 247 | WithRedisKeyPrefix(keyPrefix), 248 | ) 249 | assert.Equal(t, middleware, result, "should return self for chaining") 250 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 251 | assert.Equal(t, keyPrefix, middleware.RedisConfig.KeyPrefix, "should set key prefix") 252 | }) 253 | 254 | t.Run("EnableRedisStoreWithAllOptions", func(t *testing.T) { 255 | middleware := &GinJWTMiddleware{ 256 | Realm: "test zone", 257 | Key: []byte("secret key"), 258 | Timeout: time.Hour, 259 | MaxRefresh: time.Hour * 24, 260 | IdentityKey: "id", 261 | Authenticator: testAuthenticator, 262 | PayloadFunc: testPayloadFunc, 263 | } 264 | 265 | // Test EnableRedisStore with all options 266 | result := middleware.EnableRedisStore( 267 | WithRedisAddr(redisAddr), 268 | WithRedisAuth("testpass", 1), 269 | WithRedisCache(32*1024*1024, 15*time.Second), 270 | WithRedisPool(25, 2*time.Hour, 4*time.Hour), 271 | WithRedisKeyPrefix("test-app:"), 272 | ) 273 | 274 | assert.Equal(t, middleware, result, "should return self for chaining") 275 | assert.True(t, middleware.UseRedisStore, "should enable Redis store") 276 | assert.Equal(t, redisAddr, middleware.RedisConfig.Addr, "should set address") 277 | assert.Equal(t, "testpass", middleware.RedisConfig.Password, "should set password") 278 | assert.Equal(t, 1, middleware.RedisConfig.DB, "should set DB") 279 | assert.Equal(t, 32*1024*1024, middleware.RedisConfig.CacheSize, "should set cache size") 280 | assert.Equal(t, 15*time.Second, middleware.RedisConfig.CacheTTL, "should set cache TTL") 281 | assert.Equal(t, 25, middleware.RedisConfig.PoolSize, "should set pool size") 282 | assert.Equal( 283 | t, 284 | 2*time.Hour, 285 | middleware.RedisConfig.ConnMaxIdleTime, 286 | "should set max idle time", 287 | ) 288 | assert.Equal( 289 | t, 290 | 4*time.Hour, 291 | middleware.RedisConfig.ConnMaxLifetime, 292 | "should set max lifetime", 293 | ) 294 | assert.Equal(t, "test-app:", middleware.RedisConfig.KeyPrefix, "should set key prefix") 295 | 296 | // Test that it actually works (but use working address for actual initialization) 297 | middleware.EnableRedisStore(WithRedisAddr(redisAddr)) // Reset to working address 298 | err := middleware.MiddlewareInit() 299 | assert.NoError(t, err, "configuration with all options should initialize successfully") 300 | }) 301 | } 302 | 303 | func createTestMiddleware(redisAddr string) *GinJWTMiddleware { 304 | middleware := &GinJWTMiddleware{ 305 | Realm: "test zone", 306 | Key: []byte("secret key"), 307 | Timeout: time.Hour, 308 | MaxRefresh: time.Hour * 24, 309 | IdentityKey: "id", 310 | Authenticator: testAuthenticator, 311 | PayloadFunc: testPayloadFunc, 312 | } 313 | 314 | // Configure Redis using functional options 315 | middleware.EnableRedisStore( 316 | WithRedisAddr(redisAddr), 317 | WithRedisCache( 318 | 1024*1024, 319 | 50*time.Millisecond, 320 | ), // 1MB for testing, very short TTL for testing 321 | WithRedisKeyPrefix("test-jwt:"), 322 | ) 323 | 324 | return middleware 325 | } 326 | 327 | func testAuthenticator(c *gin.Context) (any, error) { 328 | var loginVals struct { 329 | Username string `json:"username"` 330 | Password string `json:"password"` 331 | } 332 | if err := c.ShouldBind(&loginVals); err != nil { 333 | return "", ErrMissingLoginValues 334 | } 335 | 336 | if loginVals.Username == testAdmin && loginVals.Password == testAdmin { 337 | return map[string]any{ 338 | "username": testAdmin, 339 | "userid": 1, 340 | }, nil 341 | } 342 | 343 | return nil, ErrFailedAuthentication 344 | } 345 | 346 | func testPayloadFunc(data any) gojwt.MapClaims { 347 | if v, ok := data.(map[string]any); ok { 348 | return gojwt.MapClaims{ 349 | "id": v["userid"], 350 | "username": v["username"], 351 | } 352 | } 353 | return gojwt.MapClaims{} 354 | } 355 | 356 | func testLoginAndRefreshFlow(t *testing.T, r *gin.Engine) { 357 | // Test login 358 | w := httptest.NewRecorder() 359 | req, _ := http.NewRequestWithContext( 360 | context.Background(), 361 | "POST", 362 | "/login", 363 | strings.NewReader(`{"username":"`+testAdmin+`","password":"`+testAdmin+`"}`), 364 | ) 365 | req.Header.Set("Content-Type", "application/json") 366 | r.ServeHTTP(w, req) 367 | 368 | assert.Equal(t, 200, w.Code, "login should succeed") 369 | assert.Contains(t, w.Body.String(), "access_token", "response should contain access token") 370 | assert.Contains(t, w.Body.String(), "refresh_token", "response should contain refresh token") 371 | 372 | // Extract tokens from response 373 | var loginResp map[string]any 374 | err := parseJSON(w.Body.String(), &loginResp) 375 | require.NoError(t, err, "should be able to parse login response") 376 | 377 | accessToken := loginResp["access_token"].(string) 378 | refreshToken := loginResp["refresh_token"].(string) 379 | 380 | // Test protected endpoint with access token 381 | w = httptest.NewRecorder() 382 | req, _ = http.NewRequestWithContext(context.Background(), "GET", "/auth/hello", nil) 383 | req.Header.Set("Authorization", "Bearer "+accessToken) 384 | r.ServeHTTP(w, req) 385 | 386 | assert.Equal(t, 200, w.Code, "protected endpoint should be accessible with valid token") 387 | 388 | // Test refresh token 389 | w = httptest.NewRecorder() 390 | req, _ = http.NewRequestWithContext( 391 | context.Background(), 392 | "POST", 393 | "/refresh", 394 | strings.NewReader(fmt.Sprintf(`{"refresh_token":"%s"}`, refreshToken)), 395 | ) 396 | req.Header.Set("Content-Type", "application/json") 397 | r.ServeHTTP(w, req) 398 | 399 | assert.Equal(t, 200, w.Code, "refresh should succeed") 400 | assert.Contains( 401 | t, 402 | w.Body.String(), 403 | "access_token", 404 | "refresh response should contain new access token", 405 | ) 406 | assert.Contains( 407 | t, 408 | w.Body.String(), 409 | "refresh_token", 410 | "refresh response should contain new refresh token", 411 | ) 412 | } 413 | 414 | func testTokenPersistenceAcrossRequests(t *testing.T, r *gin.Engine) { 415 | // Login and get refresh token 416 | w := httptest.NewRecorder() 417 | req, _ := http.NewRequestWithContext( 418 | context.Background(), 419 | "POST", 420 | "/login", 421 | strings.NewReader(`{"username":"`+testAdmin+`","password":"`+testAdmin+`"}`), 422 | ) 423 | req.Header.Set("Content-Type", "application/json") 424 | r.ServeHTTP(w, req) 425 | 426 | var loginResp map[string]any 427 | err := parseJSON(w.Body.String(), &loginResp) 428 | require.NoError(t, err) 429 | 430 | refreshToken := loginResp["refresh_token"].(string) 431 | 432 | // Simulate some time passing and multiple refresh requests 433 | for i := 0; i < 3; i++ { 434 | time.Sleep(10 * time.Millisecond) // Small delay to simulate real usage 435 | 436 | w = httptest.NewRecorder() 437 | req, _ = http.NewRequestWithContext( 438 | context.Background(), 439 | "POST", 440 | "/refresh", 441 | strings.NewReader(fmt.Sprintf(`{"refresh_token":"%s"}`, refreshToken)), 442 | ) 443 | req.Header.Set("Content-Type", "application/json") 444 | r.ServeHTTP(w, req) 445 | 446 | assert.Equal(t, 200, w.Code, fmt.Sprintf("refresh %d should succeed", i+1)) 447 | 448 | // Update refresh token for next iteration 449 | var refreshResp map[string]any 450 | err := parseJSON(w.Body.String(), &refreshResp) 451 | require.NoError(t, err, fmt.Sprintf("should parse refresh response %d", i+1)) 452 | refreshToken = refreshResp["refresh_token"].(string) 453 | } 454 | } 455 | 456 | func testRedisStoreOperations(t *testing.T, middleware *GinJWTMiddleware) { 457 | // Verify that Redis store is being used 458 | redisStore, ok := middleware.RefreshTokenStore.(*store.RedisRefreshTokenStore) 459 | require.True(t, ok, "should be using Redis store") 460 | 461 | // Test store operations directly 462 | ctx := context.Background() 463 | testToken := "direct-test-token" 464 | testData := map[string]any{"test": "data"} 465 | expiry := time.Now().Add(time.Hour) 466 | 467 | // Test Set 468 | err := redisStore.Set(ctx, testToken, testData, expiry) 469 | assert.NoError(t, err, "direct set should succeed") 470 | 471 | // Test Get 472 | retrievedData, err := redisStore.Get(ctx, testToken) 473 | assert.NoError(t, err, "direct get should succeed") 474 | assert.Equal(t, testData, retrievedData, "retrieved data should match") 475 | 476 | // Test Count 477 | count, err := redisStore.Count(ctx) 478 | assert.NoError(t, err, "count should succeed") 479 | assert.GreaterOrEqual(t, count, 1, "count should include our test token") 480 | 481 | // Test Delete 482 | err = redisStore.Delete(ctx, testToken) 483 | assert.NoError(t, err, "direct delete should succeed") 484 | 485 | // Verify deletion - wait for cache TTL to expire 486 | time.Sleep(100 * time.Millisecond) 487 | 488 | // The Get method should return an error for deleted tokens 489 | _, err = redisStore.Get(ctx, testToken) 490 | assert.Error(t, err, "token should not exist after deletion") 491 | } 492 | 493 | // Helper function to parse JSON response 494 | func parseJSON(jsonStr string, v any) error { 495 | return json.Unmarshal([]byte(jsonStr), v) 496 | } 497 | -------------------------------------------------------------------------------- /_example/basic/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 8 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 9 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 10 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 11 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 12 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 13 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 14 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 15 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 16 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 18 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 19 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 20 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 21 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 22 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 23 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 24 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 25 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 26 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 27 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 28 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 33 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 34 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 35 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 36 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 37 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 41 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 42 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= 45 | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 46 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 47 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 48 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 49 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 50 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 51 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 52 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 53 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 54 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 55 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 56 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 57 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 58 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 59 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 60 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 61 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 62 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= 63 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 64 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 65 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 66 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 67 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 71 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 72 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 76 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 80 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 81 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 82 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 83 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 84 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 85 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 86 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 87 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 88 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 89 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 90 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 91 | github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= 92 | github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= 93 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 94 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 95 | github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= 96 | github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 97 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 98 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 99 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 100 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 101 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 102 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 103 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 104 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 105 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 106 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 107 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 110 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 111 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 112 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 113 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 114 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 115 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 116 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 117 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 118 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 119 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 120 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 121 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 122 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 123 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 127 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 128 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 129 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 130 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 131 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 132 | github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk= 133 | github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 134 | github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= 135 | github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= 136 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 137 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 138 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 139 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 140 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 142 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 144 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 145 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 146 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 147 | github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= 148 | github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= 149 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= 150 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= 151 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 152 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 153 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 154 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 155 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 156 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 157 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 158 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 159 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 160 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 161 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 162 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 163 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 164 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 165 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 166 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 167 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 168 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 169 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 170 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 171 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 172 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 173 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 174 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 175 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 176 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 177 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 178 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 179 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 180 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 181 | golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= 182 | golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 183 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 184 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 185 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 186 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 187 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 189 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 190 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 191 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 192 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 193 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 194 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 195 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | -------------------------------------------------------------------------------- /_example/authorization/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 8 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 9 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 10 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 11 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 12 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 13 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 14 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 15 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 16 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 18 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 19 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 20 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 21 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 22 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 23 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 24 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 25 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 26 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 27 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 28 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 33 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 34 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 35 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 36 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 37 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 41 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 42 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= 45 | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 46 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 47 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 48 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 49 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 50 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 51 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 52 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 53 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 54 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 55 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 56 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 57 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 58 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 59 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 60 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 61 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 62 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= 63 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 64 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 65 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 66 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 67 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 71 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 72 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 76 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 80 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 81 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 82 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 83 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 84 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 85 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 86 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 87 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 88 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 89 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 90 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 91 | github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= 92 | github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= 93 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 94 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 95 | github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= 96 | github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 97 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 98 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 99 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 100 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 101 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 102 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 103 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 104 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 105 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 106 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 107 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 110 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 111 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 112 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 113 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 114 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 115 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 116 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 117 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 118 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 119 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 120 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 121 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 122 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 123 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 127 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 128 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 129 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 130 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 131 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 132 | github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk= 133 | github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 134 | github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= 135 | github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= 136 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 137 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 138 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 139 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 140 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 142 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 144 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 145 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 146 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 147 | github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= 148 | github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= 149 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= 150 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= 151 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 152 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 153 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 154 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 155 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 156 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 157 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 158 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 159 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 160 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 161 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 162 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 163 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 164 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 165 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 166 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 167 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 168 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 169 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 170 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 171 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 172 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 173 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 174 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 175 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 176 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 177 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 178 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 179 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 180 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 181 | golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= 182 | golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 183 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 184 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 185 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 186 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 187 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 189 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 190 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 191 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 192 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 193 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 194 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 195 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | -------------------------------------------------------------------------------- /_example/redis_store/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 8 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 9 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 10 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 11 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 12 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 13 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 14 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 15 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 16 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 18 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 19 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 20 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 21 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 22 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 23 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 24 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 25 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 26 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 27 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 28 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 33 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 34 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 35 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 36 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 37 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 41 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 42 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= 45 | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 46 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 47 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 48 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 49 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 50 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 51 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 52 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 53 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 54 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 55 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 56 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 57 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 58 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 59 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 60 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 61 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 62 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= 63 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 64 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 65 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 66 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 67 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 71 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 72 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 76 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 80 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 81 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 82 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 83 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 84 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 85 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 86 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 87 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 88 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 89 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 90 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 91 | github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= 92 | github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= 93 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 94 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 95 | github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= 96 | github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 97 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 98 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 99 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 100 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 101 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 102 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 103 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 104 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 105 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 106 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 107 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 110 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 111 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 112 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 113 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 114 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 115 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 116 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 117 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 118 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 119 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 120 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 121 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 122 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 123 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 127 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 128 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 129 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 130 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 131 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 132 | github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk= 133 | github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 134 | github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= 135 | github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= 136 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 137 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 138 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 139 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 140 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 142 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 144 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 145 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 146 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 147 | github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= 148 | github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= 149 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= 150 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= 151 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 152 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 153 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 154 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 155 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 156 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 157 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 158 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 159 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 160 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 161 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 162 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 163 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 164 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 165 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 166 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 167 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 168 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 169 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 170 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 171 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 172 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 173 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 174 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 175 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 176 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 177 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 178 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 179 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 180 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 181 | golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= 182 | golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 183 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 184 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 185 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 186 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 187 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 189 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 190 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 191 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 192 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 193 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 194 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 195 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | -------------------------------------------------------------------------------- /_example/token_generator/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= 2 | dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 6 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 7 | github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4= 8 | github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= 9 | github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= 10 | github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= 11 | github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= 12 | github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= 13 | github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= 14 | github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 15 | github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= 16 | github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 17 | github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= 18 | github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= 19 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 20 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 21 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 22 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 23 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 24 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 25 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 26 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 27 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 28 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 29 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 30 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 31 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 33 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 34 | github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= 35 | github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 36 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 37 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 38 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 39 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 40 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 41 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 42 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 43 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 44 | github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= 45 | github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 46 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 47 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 48 | github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= 49 | github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= 50 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 51 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 52 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 53 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 54 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 55 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 56 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 57 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 58 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 59 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 60 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 61 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 62 | github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= 63 | github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 64 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 65 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 66 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 67 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 71 | github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 72 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 73 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 74 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 75 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 76 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 77 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 78 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 79 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 80 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 81 | github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= 82 | github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 83 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 84 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 85 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= 86 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= 87 | github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= 88 | github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 89 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 90 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 91 | github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= 92 | github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= 93 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 94 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 95 | github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= 96 | github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= 97 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 98 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 99 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 100 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 101 | github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= 102 | github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 103 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 104 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 105 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 106 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 107 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 108 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 109 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 110 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 111 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 112 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 113 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 114 | github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= 115 | github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= 116 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 117 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 118 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 119 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 120 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 121 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 122 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 123 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 124 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 125 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 126 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= 127 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 128 | github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= 129 | github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= 130 | github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= 131 | github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= 132 | github.com/redis/rueidis v1.0.66 h1:7rvyrl0vL/cAEkE97+L5v3MJ3Vg8IKz+KIxUTfT+yJk= 133 | github.com/redis/rueidis v1.0.66/go.mod h1:Lkhr2QTgcoYBhxARU7kJRO8SyVlgUuEkcJO1Y8MCluA= 134 | github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= 135 | github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= 136 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 137 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 138 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 139 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 140 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 141 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 142 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 143 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 144 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 145 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 146 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 147 | github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= 148 | github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= 149 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0 h1:p54qELdCx4Gftkxzf44k9RJRRhaO/S5ehP9zo8SUTLM= 150 | github.com/testcontainers/testcontainers-go/modules/redis v0.39.0/go.mod h1:P1mTbHruHqAU2I26y0RADz1BitF59FLbQr7ceqN9bt4= 151 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= 152 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 153 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 154 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 155 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 156 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 157 | github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= 158 | github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= 159 | github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= 160 | github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= 161 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 162 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 163 | github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= 164 | github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= 165 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= 166 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= 167 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 168 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 169 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 170 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 171 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= 172 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 173 | go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= 174 | go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= 175 | go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= 176 | go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= 177 | go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= 178 | go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= 179 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 180 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 181 | golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= 182 | golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= 183 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 184 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 185 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 186 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 187 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 189 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 190 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 191 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 192 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 193 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 194 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 195 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 196 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 197 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 198 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 199 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | --------------------------------------------------------------------------------