JWT
JWT returns a JSON Web Token (JWT) auth middleware.
For valid token, it sets the token in Ctx.Locals (and in the underlying context.Context when PassLocalsToContext is enabled) and calls next handler.
For invalid token, it returns "401 - Unauthorized" error.
For missing token, it returns "400 - Bad Request" error.
Either way the response carries the WWW-Authenticate challenge required by RFC 9110 and RFC 6750. See Standards compliance.
Special thanks and credits to Echo
Compatible with Fiber v3.
Go version support
We only support the latest two versions of Go. Visit https://go.dev/doc/devel/release for more information.
Install
go get -u github.com/gofiber/fiber/v3
go get -u github.com/gofiber/contrib/v3/jwt
go get -u github.com/golang-jwt/jwt/v5
Signature
jwtware.New(config ...jwtware.Config) func(fiber.Ctx) error
jwtware.FromContext(ctx any) *jwt.Token // jwt "github.com/golang-jwt/jwt/v5"
FromContext accepts a fiber.Ctx, fiber.CustomCtx, *fasthttp.RequestCtx, or a standard context.Context (e.g. the value returned by c.Context() when PassLocalsToContext is enabled). It returns a *jwt.Token from github.com/golang-jwt/jwt/v5.
Config
| Property | Type | Description | Default |
|---|---|---|---|
| Next | func(fiber.Ctx) bool | Defines a function to skip this middleware when it returns true | nil |
| SuccessHandler | func(fiber.Ctx) error | Executed when a token is valid. | c.Next() |
| ErrorHandler | func(fiber.Ctx, error) error | ErrorHandler defines a function which is executed for an invalid token. | 401 Invalid or expired JWT |
| Realm | string | Protected area named in the WWW-Authenticate challenge sent with a rejection. | "Restricted" |
| SigningKey | SigningKey | Signing key used to validate the token. Used as a fallback if SigningKeys is empty. | nil |
| SigningKeys | map[string]SigningKey | Map of signing keys used to validate tokens via the kid header. | nil |
| Claims | jwt.Claims | Claims are extendable claims data defining token content. | jwt.MapClaims{} |
| Extractor | Extractor | Function used to extract the token from the request. | FromAuthHeader("Bearer") |
| TokenProcessorFunc | func(token string) (string, error) | TokenProcessorFunc processes the token extracted using the Extractor. | nil |
| KeyFunc | jwt.Keyfunc | User-defined function that supplies the public key for token validation. | nil (uses internal default) |
| JWKSetURLs | []string | List of JSON Web Key (JWK) Set URLs used to obtain signing keys for parsing JWTs. | nil |
| ParserOptions | []jwt.ParserOption | List of jwt.ParserOption, provides additional options for JWT parsing. | nil |
| KnownCriticalHeaders | []string | JWS crit header parameters this application understands and processes itself. | nil |
Available Extractors
JWT middleware uses the shared Fiber extractors (github.com/gofiber/fiber/v3/extractors) and provides several helpers for different token sources. Import them with:
import "github.com/gofiber/fiber/v3/extractors"
For an overview and additional examples, see the Fiber Extractors guide:
-
extractors.FromAuthHeader(prefix string)- Extracts token from the Authorization header using the given scheme prefix (e.g., "Bearer"). This is the recommended and most secure method. -
extractors.FromHeader(header string)- Extracts token from the specified HTTP header -
extractors.FromQuery(param string)- Extracts token from URL query parameters -
extractors.FromParam(param string)- Extracts token from URL path parameters -
extractors.FromCookie(key string)- Extracts token from cookies -
extractors.FromForm(param string)- Extracts token from form data -
extractors.Chain(extrs ...extractors.Extractor)- Tries multiple extractors in order until one succeeds
Security Considerations
⚠️ Security Warning: When choosing an extractor, consider the security implications:
- URL-based extractors (
FromQuery,FromParam): Tokens can leak through server logs, browser referrer headers, proxy logs, and browser history. Use only for development or when security is not a primary concern. - Form-based extractors (
FromForm): Similar risks to URL extractors, especially if forms are submitted via GET requests. - Header-based extractors (
FromAuthHeader,FromHeader): Most secure as headers are not typically logged or exposed in referrers. - Cookie-based extractors (
FromCookie): Secure for web applications but requires proper cookie security settings (HttpOnly, Secure, SameSite).
Recommendation: Use FromAuthHeader("Bearer") (the default) for production applications unless you have specific requirements that necessitate alternative extractors.
Standards compliance
What the middleware enforces
-
The signing algorithm comes from your configuration, not from the token. When
SigningKey.JWTAlgis set - or when every entry ofSigningKeyssets it - the parser rejects any otheralgbefore a key is looked up, as RFC 8725 Section 3.1 asks. Passjwt.WithValidMethodsinParserOptionsto pin the algorithms yourself; a value you pass wins over the derived one. Configurations that leave the algorithm open (KeyFunc,JWKSetURLs, or aSigningKeywithoutJWTAlg) are only as strict as the key material and the JWKalgallow, so pinning is worth it. -
alg: noneis refused, and keys are never read out of the token: thejwk,jku,x5uandx5cheader parameters are ignored, per RFC 8725 Sections 3.4 and 3.5. This describes the built-in key lookup -SigningKey,SigningKeysandJWKSetURLs. AKeyFuncof your own is handed the parsed token and can read whatever it likes out of the header, and aSigningKey.Keyset tojwt.UnsafeAllowNoneSignatureTypeacceptsalg: noneby design; neither is something the middleware overrides. -
Critical header parameters (
crit) are checked as RFC 7515 Section 4.1.11 requires: a token that marks a header parameter critical is rejected unless the parameter is listed inKnownCriticalHeaders. Acritvalue that is not a non-empty array of names, repeats a name, names a registered JOSE parameter such asalg, or names a parameter the header does not contain, is rejected as well. Nothing else usescrit, so leavingKnownCriticalHeadersunset is the safe default.b64(RFC 7797) cannot be declared: it changes how the payload is encoded, which the parser has already settled before your code sees the token, so naming it panics atNewand tokens carrying it are always rejected. -
expandnbfare validated by default on every request bygithub.com/golang-jwt/jwt/v5(RFC 7519 Sections 4.1.4 and 4.1.5). Usejwt.WithLeeway,jwt.WithExpirationRequiredorjwt.WithNotBeforeRequiredinParserOptionsto tighten this - and note thatjwt.WithoutClaimsValidation()inParserOptionsturns it off entirely, so an expired token is accepted. -
Base64url segments must be unpadded by default (RFC 7515 Section 2) -
jwt.WithPaddingAllowed()inParserOptionsaccepts padded segments. AnAuthorizationheader read byFromAuthHeaderwith a scheme - the defaultFromAuthHeader("Bearer")included - also has to be well-formedtoken68(RFC 9110 Section 11.6.2); that check is the extractor's, soFromAuthHeader(""),FromHeader("Authorization")and an extractor of your own hand the header over unchecked, and aTokenProcessorFuncruns after it in any case. -
Rejections carry a challenge. Every 400, 401 and 407 response the middleware produces gets a
WWW-Authenticate(orProxy-Authenticate) header, which RFC 9110 Section 15.5.2 requires of a 401 and RFC 6750 Section 3 requires of a resource server refusing a bearer token:WWW-Authenticate: Bearer realm="Restricted", error="invalid_token", error_description="The access token expired"The schemes are taken from the extractor: an
Authorizationheader names its own, and every other source - a query parameter, a cookie, a form field, a route parameter, one of your own - carries what RFC 6750 calls a bearer token and so contributesBearer. A chain offers each of them, in the order it tries them, so a client refused on the credential it did send is never told to retry with a scheme it never used. The realm comes fromRealm, and theerror_descriptionfrom the reason the token failed. Error parameters are only added for the bearer scheme, as RFC 6750 defines them, and a request that presented no usable credential is answered with a bareBearer realm="..."instead, which RFC 6750 Section 3.1 asks for. A challenge already on the response is never replaced. One yourErrorHandlerwrote is the answer and is left exactly as it is; one inherited from an authentication middleware that ran earlier is kept and this middleware's added beside it, which RFC 9110 Section 11.6.1 allows and which leaves the client both a scheme it may be able to use and the reason its token was refused.A custom
ErrorHandlerhas to leave the status somewhere the middleware can read it: on the response (c.SendStatus(...), orc.Status(...)followed by a send), or in a returned*fiber.Error. Both work, includingfiber.ErrUnauthorizedon its own. What it cannot do is return some error of its own and rely onfiber.Config.ErrorHandlerto turn that into a 401 later - the status does not exist yet when the challenge is applied, and guessing at it would putWWW-Authenticateon the 403s and 500s such a handler also produces. If you do route rejections through your own error type, set the challenge there:ErrorHandler: func(c fiber.Ctx, err error) error {return fiber.ErrUnauthorized // or c.SendStatus(fiber.StatusUnauthorized)}, -
Tokens in the URL are not stored in shared caches. When the request URL carries a token - a query parameter, a form parameter (Fiber's
FormValuereads the query before the body), or a route parameter - the successful response is markedCache-Control: private, as RFC 6750 Section 2.3 asks, since the URL a shared cache keys on contains the token. A policy the handler set is kept, except thatpublicis dropped andprivateadded unless the policy already keeps the response out of shared caches; a policy too malformed to parse, such as one with an unterminated quoted string, is replaced rather than appended to.What decides this is whether the URL carries a credential, not which extractor supplied the one that authenticated: a chain that preferred a cookie still answered a request whose URL a cache would key on. A request whose URL holds nothing is untouched, including one to a chain that could have read the query but found it absent or empty -
?token=names the parameter without carrying a credential, and the extractors read it as no credential too.The three sources above are the ones a middleware can recognise. An extractor of your own reads wherever you wrote it to, so if that is somewhere in the URL, mark the response yourself - there is nothing in its metadata to tell this middleware where it looked.
Register a cache inside this middleware, so that every request is authenticated before it can be answered:
app.Use(jwtware.New(cfg))app.Use(cache.New()) // runs only for authenticated requestsA cache registered outside it answers from its store before this middleware runs at all, so a hit is served with no authentication whatsoever. Fiber's cache keys on the method, path and query by default - not on cookies (
KeyCookies) or on theAuthorizationheader (KeyHeaders) - so with any extractor that reads a cookie or a header, a stored response is handed to requests that present no credential at all. Put a cache in front of this middleware only if its key covers every credential your extractor reads.Either way, a cache key that does not identify the user shares one user's response with the next: that is your key's business, not this middleware's. What this middleware guarantees is the directive, and for a URL that carries a token it goes on before your handler runs, so an inner cache sees it when it decides whether to store. The one case that leaves is a handler which replaces
Cache-Controloutright, since such a cache reads the replacement before this middleware regains control.
What your application has to configure
Some rules cannot be applied by a middleware, because only the application knows the values involved:
-
aud- RFC 7519 Section 4.1.3 requires a token whose audience is not this application to be rejected. The audience is only checked when you name it:app.Use(jwtware.New(jwtware.Config{SigningKey: jwtware.SigningKey{JWTAlg: jwtware.RS256, Key: publicKey},ParserOptions: []jwt.ParserOption{jwt.WithAudience("https://api.example.com"),jwt.WithIssuer("https://issuer.example.com"),jwt.WithExpirationRequired(),},})) -
typ- if your deployment issues several kinds of JWT, check the media type as RFC 8725 Section 3.11 recommends. For example, an OAuth 2.0 access token per RFC 9068 carries"typ": "at+jwt":SuccessHandler: func(c fiber.Ctx) error {if typ, _ := jwtware.FromContext(c).Header["typ"].(string); !strings.EqualFold(typ, "at+jwt") {// A rejection from here does not pass through the middleware's own// rejection path, so it has to carry its own challenge.c.Set(fiber.HeaderWWWAuthenticate,`Bearer realm="Restricted", error="invalid_token", error_description="The access token is of an unexpected type"`)return c.Status(fiber.StatusUnauthorized).SendString("unexpected token type")}return c.Next()}, -
jti- replay detection needs state the middleware does not keep.
Status code for a request without credentials
A request that carries no credentials at all is answered with 400 and
missing or malformed JWT, which is what this middleware has always done and
what the extractor can tell us: it reports a missing and a malformed credential
as the same error, which is also why its challenge names no error code. RFC 6750 Section
3.1 reserves 400 for a
malformed request and answers a request that "lacks any authentication
information" with 401 instead. If you need those OAuth 2.0 semantics exactly,
say so in an ErrorHandler:
ErrorHandler: func(c fiber.Ctx, err error) error {
if errors.Is(err, extractors.ErrNotFound) {
if c.Get(fiber.HeaderAuthorization) == "" {
// No credentials at all: challenge without naming an error.
c.Set(fiber.HeaderWWWAuthenticate, `Bearer realm="Restricted"`)
return c.SendStatus(fiber.StatusUnauthorized)
}
return c.Status(fiber.StatusBadRequest).SendString(jwtware.ErrMissingToken.Error())
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT")
},
HS256 Example
package main
import (
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
jwtware "github.com/gofiber/contrib/v3/jwt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
app := fiber.New()
// Login route
app.Post("/login", login)
// Unauthenticated route
app.Get("/", accessible)
// JWT Middleware
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
Extractor: extractors.FromAuthHeader("Bearer"),
}))
// Restricted Routes
app.Get("/restricted", restricted)
app.Listen(":3000")
}
func login(c fiber.Ctx) error {
user := c.FormValue("user")
pass := c.FormValue("pass")
// Throws Unauthorized error
if user != "john" || pass != "doe" {
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": "John Doe",
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret"))
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
func accessible(c fiber.Ctx) error {
return c.SendString("Accessible")
}
func restricted(c fiber.Ctx) error {
user := jwtware.FromContext(c)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}
Cookie Extractor Example
package main
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
jwtware "github.com/gofiber/contrib/v3/jwt"
)
func main() {
app := fiber.New()
// JWT Middleware with cookie extractor
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte("secret")},
Extractor: extractors.FromCookie("token"),
}))
app.Get("/protected", func(c fiber.Ctx) error {
return c.SendString("Protected route")
})
app.Listen(":3000")
}
HS256 Test
Login using username and password to retrieve a token.
curl --data "user=john&pass=doe" http://localhost:3000/login
Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
}
Request a restricted resource using the token in Authorization request header.
curl localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
Response
Welcome John Doe
RS256 Example
package main
import (
"crypto/rand"
"crypto/rsa"
"log"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
"github.com/golang-jwt/jwt/v5"
jwtware "github.com/gofiber/contrib/v3/jwt"
)
var (
// Obviously, this is just a test example. Do not do this in production.
// In production, you would have the private key and public key pair generated
// in advance. NEVER add a private key to any GitHub repo.
privateKey *rsa.PrivateKey
)
func main() {
app := fiber.New()
// Just as a demo, generate a new private/public key pair on each run. See note above.
rng := rand.Reader
var err error
privateKey, err = rsa.GenerateKey(rng, 2048)
if err != nil {
log.Fatalf("rsa.GenerateKey: %v", err)
}
// Login route
app.Post("/login", login)
// Unauthenticated route
app.Get("/", accessible)
// JWT Middleware
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
JWTAlg: jwtware.RS256,
Key: privateKey.Public(),
},
Extractor: extractors.FromAuthHeader("Bearer"),
}))
// Restricted Routes
app.Get("/restricted", restricted)
app.Listen(":3000")
}
func login(c fiber.Ctx) error {
user := c.FormValue("user")
pass := c.FormValue("pass")
// Throws Unauthorized error
if user != "john" || pass != "doe" {
return c.SendStatus(fiber.StatusUnauthorized)
}
// Create the Claims
claims := jwt.MapClaims{
"name": "John Doe",
"admin": true,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Create token
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// Generate encoded token and send it as response.
t, err := token.SignedString(privateKey)
if err != nil {
log.Printf("token.SignedString: %v", err)
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(fiber.Map{"token": t})
}
func accessible(c fiber.Ctx) error {
return c.SendString("Accessible")
}
func restricted(c fiber.Ctx) error {
user := jwtware.FromContext(c)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}
Retrieving the token with PassLocalsToContext
When fiber.Config{PassLocalsToContext: true} is set, the JWT token stored by the middleware is also available in the underlying context.Context. Use jwtware.FromContext with any of the supported context types:
// From a fiber.Ctx (most common usage)
token := jwtware.FromContext(c)
// From the underlying context.Context (useful in service layers or when PassLocalsToContext is enabled)
token := jwtware.FromContext(c.Context())
RS256 Test
The RS256 is actually identical to the HS256 test above.
JWK Set Test
The tests are identical to basic JWT tests above, with exception that JWKSetURLs to valid public keys collection in JSON Web Key (JWK) Set format should be supplied. See RFC 7517.
Custom KeyFunc example
KeyFunc defines a user-defined function that supplies the public key for a token validation. The function shall take care of verifying the signing algorithm and selecting the proper key. A user-defined KeyFunc can be useful if tokens are issued by an external party.
When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. This is one of the three options to provide a token validation key. The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. Required if neither SigningKeys nor SigningKey is provided. Default to an internal implementation verifying the signing algorithm and selecting the proper key.
package main
import (
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
jwtware "github.com/gofiber/contrib/v3/jwt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
app := fiber.New()
app.Use(jwtware.New(jwtware.Config{
KeyFunc: customKeyFunc(),
Extractor: extractors.FromAuthHeader("Bearer"),
}))
app.Get("/ok", func(c fiber.Ctx) error {
return c.SendString("OK")
})
}
func customKeyFunc() jwt.Keyfunc {
return func(t *jwt.Token) (interface{}, error) {
// Always check the signing method
if t.Method.Alg() != jwtware.HS256 {
return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"])
}
// TODO custom implementation of loading signing key like from a database
signingKey := "secret"
return []byte(signingKey), nil
}
}