This commit is contained in:
GitLab Deploy Bot
2025-10-21 23:45:13 +07:00
parent 6c387b420c
commit bb60e987e5
3548 changed files with 4952576 additions and 116 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Fiber
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.
+300
View File
@@ -0,0 +1,300 @@
---
id: jwt
---
# JWT
![Release](https://img.shields.io/github/v/tag/gofiber/contrib?filter=jwt*)
[![Discord](https://img.shields.io/discord/704680098577514527?style=flat&label=%F0%9F%92%AC%20discord&color=00ACD7)](https://gofiber.io/discord)
![Test](https://github.com/gofiber/contrib/workflows/Tests/badge.svg)
![Security](https://github.com/gofiber/contrib/workflows/Security/badge.svg)
![Linter](https://github.com/gofiber/contrib/workflows/Linter/badge.svg)
JWT returns a JSON Web Token (JWT) auth middleware.
For valid token, it sets the user in Ctx.Locals and calls next handler.
For invalid token, it returns "401 - Unauthorized" error.
For missing token, it returns "400 - Bad Request" error.
Special thanks and credits to [Echo](https://echo.labstack.com/middleware/jwt)
**Note: Requires Go 1.19 and above**
## Install
This middleware supports Fiber v1 & v2, install accordingly.
```
go get -u github.com/gofiber/fiber/v2
go get -u github.com/gofiber/contrib/jwt
go get -u github.com/golang-jwt/jwt/v5
```
## Signature
```go
jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error
```
## Config
| Property | Type | Description | Default |
|:---------------|:--------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------|
| Filter | `func(*fiber.Ctx) bool` | Defines a function to skip middleware | `nil` |
| SuccessHandler | `func(*fiber.Ctx) error` | SuccessHandler defines a function which is executed for a valid token. | `nil` |
| ErrorHandler | `func(*fiber.Ctx, error) error` | ErrorHandler defines a function which is executed for an invalid token. | `401 Invalid or expired JWT` |
| SigningKey | `interface{}` | Signing key to validate token. Used as fallback if SigningKeys has length 0. | `nil` |
| SigningKeys | `map[string]interface{}` | Map of signing keys to validate token with kid field usage. | `nil` |
| ContextKey | `string` | Context key to store user information from the token into context. | `"user"` |
| Claims | `jwt.Claim` | Claims are extendable claims data defining token content. | `jwt.MapClaims{}` |
| TokenLookup | `string` | TokenLookup is a string in the form of `<source>:<name>` that is used | `"header:Authorization"` |
| AuthScheme | `string` | AuthScheme to be used in the Authorization header. The default value (`"Bearer"`) will only be used in conjuction with the default `TokenLookup` value. | `"Bearer"` |
| KeyFunc | `func() jwt.Keyfunc` | KeyFunc defines a user-defined function that supplies the public key for a token validation. | `jwtKeyFunc` |
| JWKSetURLs | `[]string` | A slice of unique JSON Web Key (JWK) Set URLs to used to parse JWTs. | `nil` |
## HS256 Example
```go
package main
import (
"time"
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/contrib/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")},
}))
// 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 := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}
```
## HS256 Test
_Login using username and password to retrieve a token._
```
curl --data "user=john&pass=doe" http://localhost:3000/login
```
_Response_
```json
{
"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
```go
package main
import (
"crypto/rand"
"crypto/rsa"
"log"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
jwtware "github.com/gofiber/contrib/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(),
},
}))
// 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 := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
name := claims["name"].(string)
return c.SendString("Welcome " + name)
}
```
## 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](https://www.rfc-editor.org/rfc/rfc7517).
## 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.
```go
package main
import (
"fmt"
"github.com/gofiber/fiber/v2"
jwtware "github.com/gofiber/contrib/jwt"
"github.com/golang-jwt/jwt/v5"
)
func main() {
app := fiber.New()
app.Use(jwtware.New(jwtware.Config{
KeyFunc: customKeyFunc(),
}))
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
}
}
```
+232
View File
@@ -0,0 +1,232 @@
package jwtware
import (
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/MicahParks/keyfunc/v2"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
var (
// ErrJWTAlg is returned when the JWT header did not contain the expected algorithm.
ErrJWTAlg = errors.New("the JWT header did not contain the expected algorithm")
)
// Config defines the config for JWT middleware
type Config struct {
// Filter defines a function to skip middleware.
// Optional. Default: nil
Filter func(*fiber.Ctx) bool
// SuccessHandler defines a function which is executed for a valid token.
// Optional. Default: nil
SuccessHandler fiber.Handler
// ErrorHandler defines a function which is executed for an invalid token.
// It may be used to define a custom JWT error.
// Optional. Default: 401 Invalid or expired JWT
ErrorHandler fiber.ErrorHandler
// Signing key to validate token. Used as fallback if SigningKeys has length 0.
// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.
// The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey.
SigningKey SigningKey
// Map of signing keys to validate token with kid field usage.
// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.
// The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey.
SigningKeys map[string]SigningKey
// Context key to store user information from the token into context.
// Optional. Default: "user".
ContextKey string
// Claims are extendable claims data defining token content.
// Optional. Default value jwt.MapClaims
Claims jwt.Claims
// TokenLookup is a string in the form of "<source>:<name>" that is used
// to extract token from the request.
// Optional. Default value "header:Authorization".
// Possible values:
// - "header:<name>"
// - "query:<name>"
// - "param:<name>"
// - "cookie:<name>"
TokenLookup string
// AuthScheme to be used in the Authorization header.
// Optional. Default: "Bearer".
AuthScheme string
// KeyFunc is a function that supplies the public key for JWT cryptographic verification.
// The function shall take care of verifying the signing algorithm and selecting the proper key.
// Internally, github.com/MicahParks/keyfunc/v2 package is used project defaults. If you need more customization,
// you can provide a jwt.Keyfunc using that package or make your own implementation.
//
// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.
// The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey.
KeyFunc jwt.Keyfunc
// JWKSetURLs is a slice of HTTP URLs that contain the JSON Web Key Set (JWKS) used to verify the signatures of
// JWTs. Use of HTTPS is recommended. The presence of the "kid" field in the JWT header and JWKs is mandatory for
// this feature.
//
// By default, all JWK Sets in this slice will:
// * Refresh every hour.
// * Refresh automatically if a new "kid" is seen in a JWT being verified.
// * Rate limit refreshes to once every 5 minutes.
// * Timeout refreshes after 10 seconds.
//
// At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.
// The order of precedence is: KeyFunc, JWKSetURLs, SigningKeys, SigningKey.
JWKSetURLs []string
}
// SigningKey holds information about the recognized cryptographic keys used to sign JWTs by this program.
type SigningKey struct {
// JWTAlg is the algorithm used to sign JWTs. If this value is a non-empty string, this will be checked against the
// "alg" value in the JWT header.
//
// https://www.rfc-editor.org/rfc/rfc7518#section-3.1
JWTAlg string
// Key is the cryptographic key used to sign JWTs. For supported types, please see
// https://github.com/golang-jwt/jwt.
Key interface{}
}
// makeCfg function will check correctness of supplied configuration
// and will complement it with default values instead of missing ones
func makeCfg(config []Config) (cfg Config) {
if len(config) > 0 {
cfg = config[0]
}
if cfg.SuccessHandler == nil {
cfg.SuccessHandler = func(c *fiber.Ctx) error {
return c.Next()
}
}
if cfg.ErrorHandler == nil {
cfg.ErrorHandler = func(c *fiber.Ctx, err error) error {
if err.Error() == ErrJWTMissingOrMalformed.Error() {
return c.Status(fiber.StatusBadRequest).SendString(ErrJWTMissingOrMalformed.Error())
}
return c.Status(fiber.StatusUnauthorized).SendString("Invalid or expired JWT")
}
}
if cfg.SigningKey.Key == nil && len(cfg.SigningKeys) == 0 && len(cfg.JWKSetURLs) == 0 && cfg.KeyFunc == nil {
panic("Fiber: JWT middleware configuration: At least one of the following is required: KeyFunc, JWKSetURLs, SigningKeys, or SigningKey.")
}
if cfg.ContextKey == "" {
cfg.ContextKey = "user"
}
if cfg.Claims == nil {
cfg.Claims = jwt.MapClaims{}
}
if cfg.TokenLookup == "" {
cfg.TokenLookup = defaultTokenLookup
// set AuthScheme as "Bearer" only if TokenLookup is set to default.
if cfg.AuthScheme == "" {
cfg.AuthScheme = "Bearer"
}
}
if cfg.KeyFunc == nil {
if len(cfg.SigningKeys) > 0 || len(cfg.JWKSetURLs) > 0 {
var givenKeys map[string]keyfunc.GivenKey
if cfg.SigningKeys != nil {
givenKeys = make(map[string]keyfunc.GivenKey, len(cfg.SigningKeys))
for kid, key := range cfg.SigningKeys {
givenKeys[kid] = keyfunc.NewGivenCustom(key.Key, keyfunc.GivenKeyOptions{
Algorithm: key.JWTAlg,
})
}
}
if len(cfg.JWKSetURLs) > 0 {
var err error
cfg.KeyFunc, err = multiKeyfunc(givenKeys, cfg.JWKSetURLs)
if err != nil {
panic("Failed to create keyfunc from JWK Set URL: " + err.Error())
}
} else {
cfg.KeyFunc = keyfunc.NewGiven(givenKeys).Keyfunc
}
} else {
cfg.KeyFunc = signingKeyFunc(cfg.SigningKey)
}
}
return cfg
}
func multiKeyfunc(givenKeys map[string]keyfunc.GivenKey, jwkSetURLs []string) (jwt.Keyfunc, error) {
opts := keyfuncOptions(givenKeys)
multiple := make(map[string]keyfunc.Options, len(jwkSetURLs))
for _, url := range jwkSetURLs {
multiple[url] = opts
}
multiOpts := keyfunc.MultipleOptions{
KeySelector: keyfunc.KeySelectorFirst,
}
multi, err := keyfunc.GetMultiple(multiple, multiOpts)
if err != nil {
return nil, fmt.Errorf("failed to get multiple JWK Set URLs: %w", err)
}
return multi.Keyfunc, nil
}
func keyfuncOptions(givenKeys map[string]keyfunc.GivenKey) keyfunc.Options {
return keyfunc.Options{
GivenKeys: givenKeys,
RefreshErrorHandler: func(err error) {
log.Printf("Failed to perform background refresh of JWK Set: %s.", err)
},
RefreshInterval: time.Hour,
RefreshRateLimit: time.Minute * 5,
RefreshTimeout: time.Second * 10,
RefreshUnknownKID: true,
}
}
// getExtractors function will create a slice of functions which will be used
// for token sarch and will perform extraction of the value
func (cfg *Config) getExtractors() []jwtExtractor {
// Initialize
extractors := make([]jwtExtractor, 0)
rootParts := strings.Split(cfg.TokenLookup, ",")
for _, rootPart := range rootParts {
parts := strings.Split(strings.TrimSpace(rootPart), ":")
switch parts[0] {
case "header":
extractors = append(extractors, jwtFromHeader(parts[1], cfg.AuthScheme))
case "query":
extractors = append(extractors, jwtFromQuery(parts[1]))
case "param":
extractors = append(extractors, jwtFromParam(parts[1]))
case "cookie":
extractors = append(extractors, jwtFromCookie(parts[1]))
}
}
return extractors
}
func signingKeyFunc(key SigningKey) jwt.Keyfunc {
return func(token *jwt.Token) (interface{}, error) {
if key.JWTAlg != "" {
alg, ok := token.Header["alg"].(string)
if !ok {
return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: missing or unexpected JSON type", key.JWTAlg)
}
if alg != key.JWTAlg {
return nil, fmt.Errorf("unexpected jwt signing method: expected: %q: got: %q", key.JWTAlg, alg)
}
}
return key.Key, nil
}
}
+48
View File
@@ -0,0 +1,48 @@
package jwtware
const (
// HS256 represents a public cryptography key generated by a 256 bit HMAC algorithm.
HS256 = "HS256"
// HS384 represents a public cryptography key generated by a 384 bit HMAC algorithm.
HS384 = "HS384"
// HS512 represents a public cryptography key generated by a 512 bit HMAC algorithm.
HS512 = "HS512"
// ES256 represents a public cryptography key generated by a 256 bit ECDSA algorithm.
ES256 = "ES256"
// ES384 represents a public cryptography key generated by a 384 bit ECDSA algorithm.
ES384 = "ES384"
// ES512 represents a public cryptography key generated by a 512 bit ECDSA algorithm.
ES512 = "ES512"
// P256 represents a cryptographic elliptical curve type.
P256 = "P-256"
// P384 represents a cryptographic elliptical curve type.
P384 = "P-384"
// P521 represents a cryptographic elliptical curve type.
P521 = "P-521"
// RS256 represents a public cryptography key generated by a 256 bit RSA algorithm.
RS256 = "RS256"
// RS384 represents a public cryptography key generated by a 384 bit RSA algorithm.
RS384 = "RS384"
// RS512 represents a public cryptography key generated by a 512 bit RSA algorithm.
RS512 = "RS512"
// PS256 represents a public cryptography key generated by a 256 bit RSA algorithm.
PS256 = "PS256"
// PS384 represents a public cryptography key generated by a 384 bit RSA algorithm.
PS384 = "PS384"
// PS512 represents a public cryptography key generated by a 512 bit RSA algorithm.
PS512 = "PS512"
)
+59
View File
@@ -0,0 +1,59 @@
// 🚀 Fiber is an Express inspired web framework written in Go with 💖
// 📌 API Documentation: https://fiber.wiki
// 📝 Github Repository: https://github.com/gofiber/fiber
// Special thanks to Echo: https://github.com/labstack/echo/blob/master/middleware/jwt.go
package jwtware
import (
"reflect"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
)
var (
defaultTokenLookup = "header:" + fiber.HeaderAuthorization
)
// New ...
func New(config ...Config) fiber.Handler {
cfg := makeCfg(config)
extractors := cfg.getExtractors()
// Return middleware handler
return func(c *fiber.Ctx) error {
// Filter request to skip middleware
if cfg.Filter != nil && cfg.Filter(c) {
return c.Next()
}
var auth string
var err error
for _, extractor := range extractors {
auth, err = extractor(c)
if auth != "" && err == nil {
break
}
}
if err != nil {
return cfg.ErrorHandler(c, err)
}
var token *jwt.Token
if _, ok := cfg.Claims.(jwt.MapClaims); ok {
token, err = jwt.Parse(auth, cfg.KeyFunc)
} else {
t := reflect.ValueOf(cfg.Claims).Type().Elem()
claims := reflect.New(t).Interface().(jwt.Claims)
token, err = jwt.ParseWithClaims(auth, claims, cfg.KeyFunc)
}
if err == nil && token.Valid {
// Store user information from token into context.
c.Locals(cfg.ContextKey, token)
return cfg.SuccessHandler(c)
}
return cfg.ErrorHandler(c, err)
}
}
+60
View File
@@ -0,0 +1,60 @@
package jwtware
import (
"errors"
"strings"
"github.com/gofiber/fiber/v2"
)
var (
// ErrJWTMissingOrMalformed is returned when the JWT is missing or malformed.
ErrJWTMissingOrMalformed = errors.New("missing or malformed JWT")
)
type jwtExtractor func(c *fiber.Ctx) (string, error)
// jwtFromHeader returns a function that extracts token from the request header.
func jwtFromHeader(header string, authScheme string) func(c *fiber.Ctx) (string, error) {
return func(c *fiber.Ctx) (string, error) {
auth := c.Get(header)
l := len(authScheme)
if len(auth) > l+1 && strings.EqualFold(auth[:l], authScheme) {
return strings.TrimSpace(auth[l:]), nil
}
return "", ErrJWTMissingOrMalformed
}
}
// jwtFromQuery returns a function that extracts token from the query string.
func jwtFromQuery(param string) func(c *fiber.Ctx) (string, error) {
return func(c *fiber.Ctx) (string, error) {
token := c.Query(param)
if token == "" {
return "", ErrJWTMissingOrMalformed
}
return token, nil
}
}
// jwtFromParam returns a function that extracts token from the url param string.
func jwtFromParam(param string) func(c *fiber.Ctx) (string, error) {
return func(c *fiber.Ctx) (string, error) {
token := c.Params(param)
if token == "" {
return "", ErrJWTMissingOrMalformed
}
return token, nil
}
}
// jwtFromCookie returns a function that extracts token from the named cookie.
func jwtFromCookie(name string) func(c *fiber.Ctx) (string, error) {
return func(c *fiber.Ctx) (string, error) {
token := c.Cookies(name)
if token == "" {
return "", ErrJWTMissingOrMalformed
}
return token, nil
}
}