Repository: go-chi/jwtauth
Branch: master
Commit: 21df58695c5d
Files: 10
Total size: 33.0 KB
Directory structure:
gitextract_pgl6yup7/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── _example/
│ └── main.go
├── go.mod
├── go.sum
├── jwtauth.go
└── jwtauth_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
on:
push:
branches: '**'
paths-ignore:
- 'docs/**'
pull_request:
branches: '**'
paths-ignore:
- 'docs/**'
name: Test
jobs:
test:
env:
GOPATH: ${{ github.workspace }}
defaults:
run:
working-directory: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
strategy:
matrix:
go-version: [1.22.x, 1.23.x, 1.24.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v3
with:
path: ${{ env.GOPATH }}/src/github.com/${{ github.repository }}
- name: Test
run: |
go get -d -t ./...
go test -v ./...
================================================
FILE: .gitignore
================================================
vendor/
Gopkg.lock
.idea/
================================================
FILE: LICENSE
================================================
Copyright (c) 2015-Present https://github.com/go-chi authors
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Makefile
================================================
SHELL = bash -o pipefail
TEST_FLAGS ?= -v -race
all:
@echo "make <cmd>"
@echo ""
@echo "commands:"
@echo ""
@echo " + Development:"
@echo " - build"
@echo " - test"
@echo " - todo"
@echo " - clean"
@echo ""
@echo ""
##
## Development
##
build:
go build ./...
clean:
go clean -cache -testcache
test: test-clean
GOGC=off go test $(TEST_FLAGS) -run=$(TEST) ./...
test-clean:
GOGC=off go clean -testcache
bench:
@go test -timeout=25m -bench=.
todo:
@git grep TODO -- './*' ':!./vendor/' ':!./Makefile' || :
================================================
FILE: README.md
================================================
# jwtauth - JWT authentication middleware for HTTP services
[![GoDoc Widget]][godoc]
The `jwtauth` http middleware package provides a simple way to verify a JWT token
from a http request and send the result down the request context (`context.Context`).
Please note, `jwtauth` works with any Go http router, but resides under the go-chi group
for maintenance and organization - its only 3rd party dependency is the underlying jwt library
"github.com/lestrrat-go/jwx".
In a complete JWT-authentication flow, you'll first capture the token from a http
request, decode it, verify it and then validate that its correctly signed and hasn't
expired - the `jwtauth.Verifier` middleware handler takes care of all of that. The
`jwtauth.Verifier` will set the context values on keys `jwtauth.TokenCtxKey` and
`jwtauth.ErrorCtxKey`.
Next, it's up to an authentication handler to respond or continue processing after the
`jwtauth.Verifier`. The `jwtauth.Authenticator` middleware responds with a 401 Unauthorized
plain-text payload for all unverified tokens and passes the good ones through. You can
also copy the Authenticator and customize it to handle invalid tokens to better fit
your flow (ie. with a JSON error response body).
By default, the `Verifier` will search for a JWT token in a http request, in the order:
1. 'Authorization: BEARER T' request header
2. 'jwt' Cookie value
The first JWT string that is found as an authorization header
or cookie header is then decoded by the `lestrrat-go/jwx` library and a jwt.Token
object is set on the request context. In the case of a signature decoding error
the Verifier will also set the error on the request context.
The Verifier always calls the next http handler in sequence, which can either
be the generic `jwtauth.Authenticator` middleware or your own custom handler
which checks the request context jwt token and error to prepare a custom
http response.
Note: jwtauth supports custom verification sequences for finding a token
from a request by using the `Verify` middleware instantiator directly. The default
`Verifier` is instantiated by calling `Verify(ja, TokenFromHeader, TokenFromCookie)`.
# Usage
See the full [example](https://github.com/go-chi/jwtauth/blob/master/_example/main.go).
```go
package main
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
)
var tokenAuth *jwtauth.JWTAuth
func init() {
tokenAuth = jwtauth.New("HS256", []byte("secret"), nil)
// For debugging/example purposes, we generate and print
// a sample jwt token with claims `user_id:123` here:
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user_id": 123})
fmt.Printf("DEBUG: a sample jwt is %s\n\n", tokenString)
}
func main() {
addr := ":3333"
fmt.Printf("Starting server on %v\n", addr)
http.ListenAndServe(addr, router())
}
func router() http.Handler {
r := chi.NewRouter()
// Protected routes
r.Group(func(r chi.Router) {
// Seek, verify and validate JWT tokens
r.Use(jwtauth.Verifier(tokenAuth))
// Handle valid / invalid tokens. In this example, we use
// the provided authenticator middleware, but you can write your
// own very easily, look at the Authenticator method in jwtauth.go
// and tweak it, its not scary.
r.Use(jwtauth.Authenticator(tokenAuth))
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
_, claims, _ := jwtauth.FromContext(r.Context())
w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["user_id"])))
})
})
// Public routes
r.Group(func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome anonymous"))
})
})
return r
}
```
# Util
See https://github.com/goware/jwtutil for utility to help you generate JWT tokens.
`go install github.com/goware/jwtutil`
Usage: `jwtutil -secret=secret -encode -claims='{"user_id":111}'`
Output: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMTF9._cLJn0xFS0Mdr_4L_8XF8-8tv7bHyOQJXyWaNsSqlEs`
# LICENSE
[MIT](/LICENSE)
[godoc]: https://pkg.go.dev/github.com/go-chi/jwtauth/v5
[godoc widget]: https://godoc.org/github.com/go-chi/jwtauth?status.svg
================================================
FILE: _example/main.go
================================================
//
// jwtauth example
//
// Sample output:
//
// [peter@pak ~]$ curl -v http://localhost:3333/
// * Trying ::1...
// * Connected to localhost (::1) port 3333 (#0)
// > GET / HTTP/1.1
// > Host: localhost:3333
// > User-Agent: curl/7.49.1
// > Accept: */*
// >
// < HTTP/1.1 200 OK
// < Date: Tue, 13 Sep 2016 15:53:17 GMT
// < Content-Length: 17
// < Content-Type: text/plain; charset=utf-8
// <
// * Connection #0 to host localhost left intact
// welcome anonymous%
//
//
// [peter@pak ~]$ curl -v http://localhost:3333/admin
// * Trying ::1...
// * Connected to localhost (::1) port 3333 (#0)
// > GET /admin HTTP/1.1
// > Host: localhost:3333
// > User-Agent: curl/7.49.1
// > Accept: */*
// >
// < HTTP/1.1 401 Unauthorized
// < Content-Type: text/plain; charset=utf-8
// < X-Content-Type-Options: nosniff
// < Date: Tue, 13 Sep 2016 15:53:19 GMT
// < Content-Length: 13
// <
// Unauthorized
// * Connection #0 to host localhost left intact
//
//
// [peter@pak ~]$ curl -H"Authorization: BEARER eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjN9.PZLMJBT9OIVG2qgp9hQr685oVYFgRgWpcSPmNcw6y7M" -v http://localhost:3333/admin
// * Trying ::1...
// * Connected to localhost (::1) port 3333 (#0)
// > GET /admin HTTP/1.1
// > Host: localhost:3333
// > User-Agent: curl/7.49.1
// > Accept: */*
// > Authorization: BEARER eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjN9.PZLMJBT9OIVG2qgp9hQr685oVYFgRgWpcSPmNcw6y7M
// >
// < HTTP/1.1 200 OK
// < Date: Tue, 13 Sep 2016 15:54:26 GMT
// < Content-Length: 22
// < Content-Type: text/plain; charset=utf-8
// <
// * Connection #0 to host localhost left intact
// protected area. hi 123%
//
package main
import (
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var tokenAuth *jwtauth.JWTAuth
func init() {
tokenAuth = jwtauth.New("HS256", []byte("secret"), nil, jwt.WithAcceptableSkew(30*time.Second))
// For debugging/example purposes, we generate and print
// a sample jwt token with claims `user_id:123` here:
_, tokenString, _ := tokenAuth.Encode(map[string]interface{}{"user_id": 123})
fmt.Printf("DEBUG: a sample jwt is %s\n\n", tokenString)
}
func main() {
addr := ":3333"
fmt.Printf("Starting server on %v\n", addr)
http.ListenAndServe(addr, router())
}
func router() http.Handler {
r := chi.NewRouter()
// Protected routes
r.Group(func(r chi.Router) {
// Seek, verify and validate JWT tokens
r.Use(jwtauth.Verifier(tokenAuth))
// Handle valid / invalid tokens. In this example, we use
// the provided authenticator middleware, but you can write your
// own very easily, look at the Authenticator method in jwtauth.go
// and tweak it, its not scary.
r.Use(jwtauth.Authenticator(tokenAuth))
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
_, claims, _ := jwtauth.FromContext(r.Context())
w.Write([]byte(fmt.Sprintf("protected area. hi %v", claims["user_id"])))
})
})
// Public routes
r.Group(func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome anonymous"))
})
})
return r
}
================================================
FILE: go.mod
================================================
module github.com/go-chi/jwtauth/v5
go 1.23.0
toolchain go1.24.2
require (
github.com/go-chi/chi/v5 v5.2.1
github.com/lestrrat-go/jwx/v3 v3.0.2
)
require (
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/sys v0.33.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2 h1:SDxjGoH7qj0nBXVrcrxX8eD94wEnjR+EEuqqmeqQYlY=
github.com/lestrrat-go/httprc/v3 v3.0.0-beta2/go.mod h1:Nwo81sMxE0DcvTB+rJyynNhv/DUu2yZErV7sscw9pHE=
github.com/lestrrat-go/jwx/v3 v3.0.2 h1:N+XLjTJEzDZRP3S0SezclXFAfopwL+o5vaL+qg6rX1I=
github.com/lestrrat-go/jwx/v3 v3.0.2/go.mod h1:qO9w1qkQH77a0r9OXNM33YQPnV/evetKYRg58h1rBNE=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: jwtauth.go
================================================
package jwtauth
import (
"context"
"errors"
"net/http"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v3/transform"
)
type JWTAuth struct {
alg jwa.SignatureAlgorithm
signKey interface{} // private-key
verifyKey interface{} // public-key, only used by RSA and ECDSA algorithms
verifier jwt.ParseOption
validateOptions []jwt.ValidateOption
}
var (
TokenCtxKey = &contextKey{"Token"}
ErrorCtxKey = &contextKey{"Error"}
)
var (
ErrUnauthorized = errors.New("token is unauthorized")
ErrExpired = errors.New("token is expired")
ErrNBFInvalid = errors.New("token nbf validation failed")
ErrIATInvalid = errors.New("token iat validation failed")
ErrNoTokenFound = errors.New("no token found")
ErrAlgoInvalid = errors.New("algorithm mismatch")
)
func New(alg string, signKey interface{}, verifyKey interface{}, validateOptions ...jwt.ValidateOption) *JWTAuth {
sigAlg, _ := jwa.LookupSignatureAlgorithm(alg)
ja := &JWTAuth{
alg: sigAlg,
signKey: signKey,
verifyKey: verifyKey,
validateOptions: validateOptions,
}
if ja.verifyKey != nil {
ja.verifier = jwt.WithKey(ja.alg, ja.verifyKey)
} else {
ja.verifier = jwt.WithKey(ja.alg, ja.signKey)
}
return ja
}
// Verifier http middleware handler will verify a JWT string from a http request.
//
// Verifier will search for a JWT token in a http request, in the order:
// 1. 'Authorization: BEARER T' request header
// 2. Cookie 'jwt' value
//
// The first JWT string that is found as a query parameter, authorization header
// or cookie header is then decoded by the `jwt-go` library and a *jwt.Token
// object is set on the request context. In the case of a signature decoding error
// the Verifier will also set the error on the request context.
//
// The Verifier always calls the next http handler in sequence, which can either
// be the generic `jwtauth.Authenticator` middleware or your own custom handler
// which checks the request context jwt token and error to prepare a custom
// http response.
func Verifier(ja *JWTAuth) func(http.Handler) http.Handler {
return Verify(ja, TokenFromHeader, TokenFromCookie)
}
func Verify(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
hfn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
token, err := VerifyRequest(ja, r, findTokenFns...)
ctx = NewContext(ctx, token, err)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(hfn)
}
}
func VerifyRequest(ja *JWTAuth, r *http.Request, findTokenFns ...func(r *http.Request) string) (jwt.Token, error) {
var tokenString string
// Extract token string from the request by calling token find functions in
// the order they where provided. Further extraction stops if a function
// returns a non-empty string.
for _, fn := range findTokenFns {
tokenString = fn(r)
if tokenString != "" {
break
}
}
if tokenString == "" {
return nil, ErrNoTokenFound
}
return VerifyToken(ja, tokenString)
}
func VerifyToken(ja *JWTAuth, tokenString string) (jwt.Token, error) {
// Decode & verify the token
token, err := ja.Decode(tokenString)
if err != nil {
return token, ErrorReason(err)
}
if token == nil {
return nil, ErrUnauthorized
}
if err := jwt.Validate(token, ja.validateOptions...); err != nil {
return token, ErrorReason(err)
}
// Valid!
return token, nil
}
func (ja *JWTAuth) Encode(claims map[string]interface{}) (t jwt.Token, tokenString string, err error) {
t = jwt.New()
for k, v := range claims {
if err := t.Set(k, v); err != nil {
return nil, "", err
}
}
payload, err := ja.sign(t)
if err != nil {
return nil, "", err
}
tokenString = string(payload)
return
}
func (ja *JWTAuth) Decode(tokenString string) (jwt.Token, error) {
return ja.parse([]byte(tokenString))
}
func (ja *JWTAuth) ValidateOptions() []jwt.ValidateOption {
return ja.validateOptions
}
func (ja *JWTAuth) sign(token jwt.Token) ([]byte, error) {
return jwt.Sign(token, jwt.WithKey(ja.alg, ja.signKey))
}
func (ja *JWTAuth) parse(payload []byte) (jwt.Token, error) {
// we disable validation here because we use jwt.Validate to validate tokens
return jwt.Parse(payload, ja.verifier, jwt.WithValidate(false))
}
// ErrorReason will normalize the error message from the underlining
// jwt library
func ErrorReason(err error) error {
switch {
case errors.Is(err, jwt.TokenExpiredError()), err == ErrExpired:
return ErrExpired
case errors.Is(err, jwt.InvalidIssuedAtError()), err == ErrIATInvalid:
return ErrIATInvalid
case errors.Is(err, jwt.TokenNotYetValidError()), err == ErrNBFInvalid:
return ErrNBFInvalid
default:
return ErrUnauthorized
}
}
// Authenticator is a default authentication middleware to enforce access from the
// Verifier middleware request context values. The Authenticator sends a 401 Unauthorized
// response for any unverified tokens and passes the good ones through. It's just fine
// until you decide to write something similar and customize your client response.
func Authenticator(ja *JWTAuth) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
hfn := func(w http.ResponseWriter, r *http.Request) {
token, _, err := FromContext(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
if token == nil {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
}
return http.HandlerFunc(hfn)
}
}
func NewContext(ctx context.Context, t jwt.Token, err error) context.Context {
ctx = context.WithValue(ctx, TokenCtxKey, t)
ctx = context.WithValue(ctx, ErrorCtxKey, err)
return ctx
}
func FromContext(ctx context.Context) (jwt.Token, map[string]interface{}, error) {
token, _ := ctx.Value(TokenCtxKey).(jwt.Token)
var err error
claims := map[string]interface{}{}
if token != nil {
if err = transform.AsMap(token, claims); err != nil {
return token, nil, err
}
}
err, _ = ctx.Value(ErrorCtxKey).(error)
return token, claims, err
}
// UnixTime returns the given time in UTC milliseconds
func UnixTime(tm time.Time) int64 {
return tm.UTC().Unix()
}
// EpochNow is a helper function that returns the NumericDate time value used by the spec
func EpochNow() int64 {
return time.Now().UTC().Unix()
}
// ExpireIn is a helper function to return calculated time in the future for "exp" claim
func ExpireIn(tm time.Duration) int64 {
return EpochNow() + int64(tm.Seconds())
}
// Set issued at ("iat") to specified time in the claims
func SetIssuedAt(claims map[string]interface{}, tm time.Time) {
claims["iat"] = tm.UTC().Unix()
}
// Set issued at ("iat") to present time in the claims
func SetIssuedNow(claims map[string]interface{}) {
claims["iat"] = EpochNow()
}
// Set expiry ("exp") in the claims
func SetExpiry(claims map[string]interface{}, tm time.Time) {
claims["exp"] = tm.UTC().Unix()
}
// Set expiry ("exp") in the claims to some duration from the present time
func SetExpiryIn(claims map[string]interface{}, tm time.Duration) {
claims["exp"] = ExpireIn(tm)
}
// TokenFromCookie tries to retrieve the token string from a cookie named
// "jwt".
func TokenFromCookie(r *http.Request) string {
cookie, err := r.Cookie("jwt")
if err != nil {
return ""
}
return cookie.Value
}
// TokenFromHeader tries to retrieve the token string from the
// "Authorization" request header: "Authorization: BEARER T".
func TokenFromHeader(r *http.Request) string {
// Get token from authorization header.
bearer := r.Header.Get("Authorization")
if len(bearer) > 7 && strings.ToUpper(bearer[0:7]) == "BEARER " {
return bearer[7:]
}
return ""
}
// TokenFromQuery tries to retrieve the token string from the "jwt" URI
// query parameter.
//
// To use it, build our own middleware handler, such as:
//
// func Verifier(ja *JWTAuth) func(http.Handler) http.Handler {
// return func(next http.Handler) http.Handler {
// return Verify(ja, TokenFromQuery, TokenFromHeader, TokenFromCookie)(next)
// }
// }
func TokenFromQuery(r *http.Request) string {
// Get token from query param named "jwt".
return r.URL.Query().Get("jwt")
}
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "jwtauth context value " + k.name
}
================================================
FILE: jwtauth_test.go
================================================
package jwtauth_test
import (
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/jwtauth/v5"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwt"
"github.com/lestrrat-go/jwx/v3/transform"
)
var (
TokenAuthHS256 *jwtauth.JWTAuth
TokenSecret = []byte("secretpass")
TokenAuthRS256 *jwtauth.JWTAuth
PrivateKeyRS256String = `-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDsH6T+WrdRLKHEhbhbnItRo7X5tj0xssOSCJUiZbCHr52XftIr
hBD6HxbGaKUEzuaCDYGEcQZZRJ1KHfYmJtXPCz4Zp3qlhjNugvTaZoFtQ8RqiWVY
cHqCY6cmI+3cq2mVrd7MstpXKhC8dZ2MZnzx/zqaeiV21SiwxHed8LmWmQIDAQAB
AoGAff9I0L1hkrxJOg/M133KTe8Y3L4lG07z0wonYmp274CDjGKNDdF0KbPLOGaA
n/czw3Qnh5+0LpBRikpAng0dC06z0YnyzrkoPPawC4s2zJeY3NnajK9IfRAAVlby
cIJVmEL/xF3FFHhCfrJNWd+zthcHxCATJOBpH2pwhb4WLfECQQD/geZ/B6p8WlGb
amHFhBd/hQN6cq63RGujf3ecz5H+h4RqFyycaVr3t8QZBBd3O3jRB9FCcan2IxRa
UoYNGNB9AkEA7JQtfmb0p8cTHiDyV6qb8aNJFWipwQVVMmpaXvfC6Aue5uJiyHnx
iScLsj1ozewCgTvzL7MAsfj0k6qX3c01TQJBAPL2JCdhM8XB4N4Hf+dhHzMcWd1j
Fi6hOjWjrSsI2owNc2Wqmbo2GNF8BlW/ZUz02YLzixJCoVqzqtPkqyHjGcUCQQDk
msrbOeFvvo5arrt+uv21oXMdnOVr/xs0fFCXNBLC53fE4z1RO4SKY5CJy41abpR9
DNERZodlcovjpRTa31CBAkEAw8geqJ1+cfEDZYfJxJigFSwbwoLw6BH+GD4KAEdX
G1u9SGGYP19eC2mpQei4V5MqAYEbb82bqcebhwg8kAReNQ==
-----END RSA PRIVATE KEY-----
`
PublicKeyRS256String = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDsH6T+WrdRLKHEhbhbnItRo7X5
tj0xssOSCJUiZbCHr52XftIrhBD6HxbGaKUEzuaCDYGEcQZZRJ1KHfYmJtXPCz4Z
p3qlhjNugvTaZoFtQ8RqiWVYcHqCY6cmI+3cq2mVrd7MstpXKhC8dZ2MZnzx/zqa
eiV21SiwxHed8LmWmQIDAQAB
-----END PUBLIC KEY-----
`
)
func init() {
TokenAuthHS256 = jwtauth.New(jwa.HS256().String(), TokenSecret, nil, jwt.WithAcceptableSkew(30*time.Second))
}
//
// Tests
//
func TestSimple(t *testing.T) {
r := chi.NewRouter()
r.Use(
jwtauth.Verifier(TokenAuthHS256),
jwtauth.Authenticator(TokenAuthHS256),
)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
})
ts := httptest.NewServer(r)
defer ts.Close()
tt := []struct {
Name string
Authorization string
Status int
Resp string
}{
{Name: "empty token", Authorization: "", Status: 401, Resp: "no token found\n"},
{Name: "wrong token", Authorization: "Bearer asdf", Status: 401, Resp: "token is unauthorized\n"},
{Name: "wrong secret", Authorization: "Bearer " + newJwtToken([]byte("wrong")), Status: 401, Resp: "token is unauthorized\n"},
{Name: "wrong secret/alg", Authorization: "Bearer " + newJwt512Token([]byte("wrong")), Status: 401, Resp: "token is unauthorized\n"},
{Name: "wrong alg", Authorization: "Bearer " + newJwt512Token(TokenSecret, map[string]interface{}{}), Status: 401, Resp: "token is unauthorized\n"},
{Name: "expired within skew", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 29}), Status: 200, Resp: "welcome"},
{Name: "expired outside skew", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"exp": time.Now().Unix() - 31}), Status: 401, Resp: "token is expired\n"},
{Name: "valid token", Authorization: "Bearer " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"},
{Name: "valid Bearer", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"service": "test"}), Status: 200, Resp: "welcome"},
{Name: "valid BEARER", Authorization: "BEARER " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"},
{Name: "valid bearer", Authorization: "bearer " + newJwtToken(TokenSecret), Status: 200, Resp: "welcome"},
{Name: "valid claim", Authorization: "Bearer " + newJwtToken(TokenSecret, map[string]interface{}{"service": "test"}), Status: 200, Resp: "welcome"},
{Name: "invalid bearer_", Authorization: "BEARER_" + newJwtToken(TokenSecret), Status: 401, Resp: "no token found\n"},
{Name: "invalid bearerx", Authorization: "BEARERx" + newJwtToken(TokenSecret), Status: 401, Resp: "no token found\n"},
}
for _, tc := range tt {
h := http.Header{}
if tc.Authorization != "" {
h.Set("Authorization", tc.Authorization)
}
status, resp := testRequest(t, ts, "GET", "/", h, nil)
if status != tc.Status || resp != tc.Resp {
t.Errorf("test '%s' failed: expected Status: %d %q, got %d %q", tc.Name, tc.Status, tc.Resp, status, resp)
}
}
}
func TestSimpleRSA(t *testing.T) {
privateKeyBlock, _ := pem.Decode([]byte(PrivateKeyRS256String))
privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBlock.Bytes)
if err != nil {
t.Fatal(err.Error())
}
publicKeyBlock, _ := pem.Decode([]byte(PublicKeyRS256String))
publicKey, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
if err != nil {
t.Fatal(err.Error())
}
TokenAuthRS256 = jwtauth.New(jwa.RS256().String(), privateKey, publicKey)
claims := map[string]interface{}{
"key": "val",
"key2": "val2",
"key3": "val3",
}
_, tokenString, err := TokenAuthRS256.Encode(claims)
if err != nil {
t.Fatalf("Failed to encode claims %s\n", err.Error())
}
token, err := TokenAuthRS256.Decode(tokenString)
if err != nil {
t.Fatalf("Failed to decode token string %s\n", err.Error())
}
tokenClaims := map[string]interface{}{}
if err := transform.AsMap(token, tokenClaims); err != nil {
t.Fatalf("Failed to get claims %s\n", err.Error())
}
if !reflect.DeepEqual(claims, tokenClaims) {
t.Fatalf("The decoded claims don't match the original ones\n")
}
}
func TestSimpleRSAVerifyOnly(t *testing.T) {
tokenString := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWwiLCJrZXkyIjoidmFsMiIsImtleTMiOiJ2YWwzIn0.IK0G0Qi_c6N6uHRokHMSHQEeYxoi_T73A4RdEzJIfnbs5kA5hF0UhApSWUMZfsTYFNC2buYvWqbyj2kDdXcStpqTUPENGTKvJi66puwhN16BqEOS-jb7kVyf3vWif7XabY0_5S8H_aeqazaj4FemHvWnywJznuMWJRXWw83edpA"
claims := map[string]interface{}{
"key": "val",
"key2": "val2",
"key3": "val3",
}
publicKeyBlock, _ := pem.Decode([]byte(PublicKeyRS256String))
publicKey, err := x509.ParsePKIXPublicKey(publicKeyBlock.Bytes)
if err != nil {
t.Fatal(err.Error())
}
TokenAuthRS256 = jwtauth.New(jwa.RS256().String(), nil, publicKey)
_, _, err = TokenAuthRS256.Encode(claims)
if err == nil {
t.Fatalf("Expecting error when encoding claims without signing key")
}
token, err := TokenAuthRS256.Decode(tokenString)
if err != nil {
t.Fatalf("Failed to decode token string %s\n", err.Error())
}
tokenClaims := map[string]interface{}{}
if err := transform.AsMap(token, tokenClaims); err != nil {
t.Fatalf("Failed to get claims %s\n", err.Error())
}
if !reflect.DeepEqual(claims, tokenClaims) {
t.Fatalf("The decoded claims don't match the original ones\n")
}
}
func TestMore(t *testing.T) {
r := chi.NewRouter()
// Protected routes
r.Group(func(r chi.Router) {
r.Use(jwtauth.Verifier(TokenAuthHS256))
authenticator := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, _, err := jwtauth.FromContext(r.Context())
if err != nil {
http.Error(w, jwtauth.ErrorReason(err).Error(), http.StatusUnauthorized)
return
}
if err := jwt.Validate(token); err != nil {
http.Error(w, jwtauth.ErrorReason(err).Error(), http.StatusUnauthorized)
return
}
// Token is authenticated, pass it through
next.ServeHTTP(w, r)
})
}
r.Use(authenticator)
r.Get("/admin", func(w http.ResponseWriter, r *http.Request) {
_, claims, err := jwtauth.FromContext(r.Context())
if err != nil {
w.Write([]byte(fmt.Sprintf("error! %v", err)))
return
}
w.Write([]byte(fmt.Sprintf("protected, user:%v", claims["user_id"])))
})
})
// Public routes
r.Group(func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
})
})
ts := httptest.NewServer(r)
defer ts.Close()
// sending unauthorized requests
if status, resp := testRequest(t, ts, "GET", "/admin", nil, nil); status != 401 || resp != "token is unauthorized\n" {
t.Fatal(resp)
}
h := http.Header{}
h.Set("Authorization", "BEARER "+newJwtToken([]byte("wrong"), map[string]interface{}{}))
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" {
t.Fatal(resp)
}
h.Set("Authorization", "BEARER asdf")
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" {
t.Fatal(resp)
}
// wrong token secret and wrong alg
h.Set("Authorization", "BEARER "+newJwt512Token([]byte("wrong"), map[string]interface{}{}))
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" {
t.Fatal(resp)
}
// correct token secret but wrong alg
h.Set("Authorization", "BEARER "+newJwt512Token(TokenSecret, map[string]interface{}{}))
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is unauthorized\n" {
t.Fatal(resp)
}
h = newAuthHeader(map[string]interface{}{"exp": jwtauth.EpochNow() - 1000})
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 401 || resp != "token is expired\n" {
t.Fatal(resp)
}
// sending authorized requests
if status, resp := testRequest(t, ts, "GET", "/", nil, nil); status != 200 || resp != "welcome" {
t.Fatal(resp)
}
h = newAuthHeader((map[string]interface{}{"user_id": 31337, "exp": jwtauth.ExpireIn(5 * time.Minute)}))
if status, resp := testRequest(t, ts, "GET", "/admin", h, nil); status != 200 || resp != "protected, user:31337" {
t.Fatal(resp)
}
}
func TestEncodeClaims(t *testing.T) {
claims := map[string]interface{}{
"key1": "val1",
"key2": 2,
"key3": time.Now(),
"key4": []string{"1", "2"},
}
claims[jwt.JwtIDKey] = 1
if _, _, err := TokenAuthHS256.Encode(claims); err == nil {
t.Fatal("encoding invalid claims succeeded")
}
claims[jwt.JwtIDKey] = "123"
if _, _, err := TokenAuthHS256.Encode(claims); err != nil {
t.Fatalf("unexpected error encoding valid claims: %v", err)
}
}
//
// Test helper functions
//
func testRequest(t *testing.T, ts *httptest.Server, method, path string, header http.Header, body io.Reader) (int, string) {
req, err := http.NewRequest(method, ts.URL+path, body)
if err != nil {
t.Fatal(err)
return 0, ""
}
for k, v := range header {
req.Header.Set(k, v[0])
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
return 0, ""
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
return 0, ""
}
defer resp.Body.Close()
return resp.StatusCode, string(respBody)
}
func newJwtToken(secret []byte, claims ...map[string]interface{}) string {
token := jwt.New()
if len(claims) > 0 {
for k, v := range claims[0] {
token.Set(k, v)
}
}
tokenPayload, err := jwt.Sign(token, jwt.WithKey(jwa.HS256(), secret))
if err != nil {
log.Fatal(err)
}
return string(tokenPayload)
}
func newJwt512Token(secret []byte, claims ...map[string]interface{}) string {
// use-case: when token is signed with a different alg than expected
token := jwt.New()
if len(claims) > 0 {
for k, v := range claims[0] {
token.Set(k, v)
}
}
tokenPayload, err := jwt.Sign(token, jwt.WithKey(jwa.HS512(), secret))
if err != nil {
log.Fatal(err)
}
return string(tokenPayload)
}
func newAuthHeader(claims ...map[string]interface{}) http.Header {
h := http.Header{}
h.Set("Authorization", "BEARER "+newJwtToken(TokenSecret, claims...))
return h
}
gitextract_pgl6yup7/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _example/ │ └── main.go ├── go.mod ├── go.sum ├── jwtauth.go └── jwtauth_test.go
SYMBOL INDEX (40 symbols across 3 files)
FILE: _example/main.go
function init (line 73) | func init() {
function main (line 82) | func main() {
function router (line 88) | func router() http.Handler {
FILE: jwtauth.go
type JWTAuth (line 15) | type JWTAuth struct
method Encode (line 124) | func (ja *JWTAuth) Encode(claims map[string]interface{}) (t jwt.Token,...
method Decode (line 139) | func (ja *JWTAuth) Decode(tokenString string) (jwt.Token, error) {
method ValidateOptions (line 143) | func (ja *JWTAuth) ValidateOptions() []jwt.ValidateOption {
method sign (line 147) | func (ja *JWTAuth) sign(token jwt.Token) ([]byte, error) {
method parse (line 151) | func (ja *JWTAuth) parse(payload []byte) (jwt.Token, error) {
function New (line 37) | func New(alg string, signKey interface{}, verifyKey interface{}, validat...
function Verifier (line 70) | func Verifier(ja *JWTAuth) func(http.Handler) http.Handler {
function Verify (line 74) | func Verify(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) f...
function VerifyRequest (line 86) | func VerifyRequest(ja *JWTAuth, r *http.Request, findTokenFns ...func(r ...
function VerifyToken (line 105) | func VerifyToken(ja *JWTAuth, tokenString string) (jwt.Token, error) {
function ErrorReason (line 158) | func ErrorReason(err error) error {
function Authenticator (line 175) | func Authenticator(ja *JWTAuth) func(http.Handler) http.Handler {
function NewContext (line 197) | func NewContext(ctx context.Context, t jwt.Token, err error) context.Con...
function FromContext (line 203) | func FromContext(ctx context.Context) (jwt.Token, map[string]interface{}...
function UnixTime (line 220) | func UnixTime(tm time.Time) int64 {
function EpochNow (line 225) | func EpochNow() int64 {
function ExpireIn (line 230) | func ExpireIn(tm time.Duration) int64 {
function SetIssuedAt (line 235) | func SetIssuedAt(claims map[string]interface{}, tm time.Time) {
function SetIssuedNow (line 240) | func SetIssuedNow(claims map[string]interface{}) {
function SetExpiry (line 245) | func SetExpiry(claims map[string]interface{}, tm time.Time) {
function SetExpiryIn (line 250) | func SetExpiryIn(claims map[string]interface{}, tm time.Duration) {
function TokenFromCookie (line 256) | func TokenFromCookie(r *http.Request) string {
function TokenFromHeader (line 266) | func TokenFromHeader(r *http.Request) string {
function TokenFromQuery (line 285) | func TokenFromQuery(r *http.Request) string {
type contextKey (line 293) | type contextKey struct
method String (line 297) | func (k *contextKey) String() string {
FILE: jwtauth_test.go
function init (line 54) | func init() {
function TestSimple (line 62) | func TestSimple(t *testing.T) {
function TestSimpleRSA (line 111) | func TestSimpleRSA(t *testing.T) {
function TestSimpleRSAVerifyOnly (line 154) | func TestSimpleRSAVerifyOnly(t *testing.T) {
function TestMore (line 190) | func TestMore(t *testing.T) {
function TestEncodeClaims (line 280) | func TestEncodeClaims(t *testing.T) {
function testRequest (line 301) | func testRequest(t *testing.T, ts *httptest.Server, method, path string,...
function newJwtToken (line 328) | func newJwtToken(secret []byte, claims ...map[string]interface{}) string {
function newJwt512Token (line 343) | func newJwt512Token(secret []byte, claims ...map[string]interface{}) str...
function newAuthHeader (line 358) | func newAuthHeader(claims ...map[string]interface{}) http.Header {
Condensed preview — 10 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (37K chars).
[
{
"path": ".github/workflows/ci.yml",
"chars": 833,
"preview": "on:\n push:\n branches: '**'\n paths-ignore:\n - 'docs/**'\n pull_request:\n branches: '**'\n paths-ignore:\n"
},
{
"path": ".gitignore",
"chars": 25,
"preview": "vendor/\nGopkg.lock\n.idea/"
},
{
"path": "LICENSE",
"chars": 1098,
"preview": "Copyright (c) 2015-Present https://github.com/go-chi authors\n\nMIT License\n\nPermission is hereby granted, free of charge,"
},
{
"path": "Makefile",
"chars": 553,
"preview": "SHELL = bash -o pipefail\nTEST_FLAGS ?= -v -race\n\nall:\n\t@echo \"make <cmd>\"\n\t@echo \"\"\n\t@echo \"commands:\"\n"
},
{
"path": "README.md",
"chars": 4143,
"preview": "# jwtauth - JWT authentication middleware for HTTP services\n\n[![GoDoc Widget]][godoc]\n\nThe `jwtauth` http middleware pac"
},
{
"path": "_example/main.go",
"chars": 3147,
"preview": "//\n// jwtauth example\n//\n// Sample output:\n//\n// [peter@pak ~]$ curl -v http://localhost:3333/\n// * Trying ::1...\n// *"
},
{
"path": "go.mod",
"chars": 609,
"preview": "module github.com/go-chi/jwtauth/v5\n\ngo 1.23.0\n\ntoolchain go1.24.2\n\nrequire (\n\tgithub.com/go-chi/chi/v5 v5.2.1\n\tgithub.c"
},
{
"path": "go.sum",
"chars": 3172,
"preview": "github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1"
},
{
"path": "jwtauth.go",
"chars": 8757,
"preview": "package jwtauth\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/lestrrat-go/jwx/v3/jwa\"\n\t\"gi"
},
{
"path": "jwtauth_test.go",
"chars": 11504,
"preview": "package jwtauth_test\n\nimport (\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"ref"
}
]
About this extraction
This page contains the full source code of the go-chi/jwtauth GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 10 files (33.0 KB), approximately 11.1k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.