A JWT middleware for the Echo framework using lestrrat-go/jwx.
You might wonder why not use the JWT middleware that ships with Echo?
The reason is that it uses the golang-jwt/jwt library which,
although a good library, doesn't implement every JWT features while lestrrat-go/jwx
is the most complete implementation as of this writing.
I think echo-jwt also has better defaults, like RS256
as the default signing method and is also more flexible in what
parsing options you can pass to the token verification function through the Options
config.
I think other features like ExemptRoutes
, ExemptMethods
, OptionalRoutes
and RefreshToken
are useful features
that most developers would want to use without having to implement them themselves.
go get github.com/alexferl/echo-jwt
Before using the middleware you need to generate an RSA private key (RSASSA-PKCS-v1.5 using SHA-256) to sign and verify the tokens.
openssl genrsa -out private-key.pem 4096
package main
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/alexferl/echo-jwt"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v2/jwa"
jwx "github.com/lestrrat-go/jwx/v2/jwt"
)
var privateKey *rsa.PrivateKey
func main() {
e := echo.New()
e.GET("/", func(c echo.Context) error {
t := c.Get("token").(jwx.Token)
return c.JSON(http.StatusOK, t)
})
e.POST("/login", func(c echo.Context) error {
builder := jwx.NewBuilder().
Subject("1").
Issuer("http://localhost:1323").
IssuedAt(time.Now()).
NotBefore(time.Now()).
Expiration(time.Now().Add(time.Minute*10)).
Claim("name", c.QueryParam("name"))
token, err := builder.Build()
if err != nil {
panic(fmt.Sprintf("failed building token: %v\n", err))
}
signed, err := jwx.Sign(token, jwx.WithKey(jwa.RS256, privateKey))
if err != nil {
panic(fmt.Sprintf("failed signing token: %v\n", err))
}
return c.JSON(http.StatusOK, map[string]string{"access_token": string(signed)})
})
key, err := loadPrivateKey("/path/to/private-key.pem")
if err != nil {
panic(fmt.Sprintf("failed loading private key: %v\n", err))
}
privateKey = key
e.Use(jwt.JWT(key))
e.Logger.Fatal(e.Start("localhost:1323"))
}
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
b, err := io.ReadAll(f)
if err != nil {
return nil, err
}
block, _ := pem.Decode(b)
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block: %v", err)
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, nil
}
Getting a token:
curl -X POST http://localhost:1323/login\?name\=alex
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOj..."}
Using a token:
curl http://localhost:1323/ -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOj...'
{"exp":1666320946,"iat":1666320346,"iss":"http://localhost:1323","name":"name","nbf":1666320346,"sub":"1"}
By default, all routes except POST /login
will require a token in
the Authorization
header or as a cookie with the key access_token
.
You may define some additional exempted routes and methods that don't require a token:
e.Use(jwt.JWTWithConfig(jwt.Config{
ExemptRoutes: map[string][]string{
"/": {http.MethodGet},
"/login": {http.MethodPost},
"/users": {http.MethodPost, http.MethodGet},
"/users/:id": {http.MethodGet},
},
Key: key,
}))
type Config struct {
// Skipper defines a function to skip middleware.
Skipper middleware.Skipper
// Key defines the RSA key used to verify tokens.
// Required.
Key any
// ExemptRoutes defines routes and methods that don't require tokens.
// Optional. Defaults to /login [POST].
ExemptRoutes map[string][]string
// ExemptMethods defines methods that don't require tokens.
// Optional. Defaults to [OPTIONS].
ExemptMethods []string
// OptionalRoutes defines routes and methods that
// can optionally require a token.
// Optional.
OptionalRoutes map[string][]string
// ParseTokenFunc defines a function used to decode tokens.
// Optional.
ParseTokenFunc func(string, []jwt.ParseOption) (jwt.Token, error)
// AfterParseFunc defines a function that will run after
// the ParseTokenFunc has successfully run.
// Optional.
AfterParseFunc func(echo.Context, jwt.Token, string, TokenSource) *echo.HTTPError
// Options defines jwt.ParseOption options for parsing tokens.
// Optional. Defaults [jwt.WithValidate(true)].
Options []jwt.ParseOption
// ContextKey defines the key that will be used to store the token
// on the echo.Context when the token is successfully parsed.
// Optional. Defaults to "token".
ContextKey string
// CookieKey defines the key that will be used to read the token
// from an HTTP cookie.
// Optional. Defaults to "access_token".
CookieKey string
// AuthHeader defines the HTTP header that will be used to
// read the token from.
// Optional. Defaults to "Authorization".
AuthHeader string
// AuthScheme defines the authorization scheme in the AuthHeader.
// Optional. Defaults to "Bearer".
AuthScheme string
// UseRefreshToken controls whether refresh tokens are used or not.
// Optional. Defaults to false.
UseRefreshToken bool
// RefreshToken holds the configuration related to refresh tokens.
// Optional.
RefreshToken *RefreshToken
}
type RefreshToken struct {
// ContextKey defines the key that will be used to store the refresh token
// on the echo.Context when the token is successfully parsed.
// Optional. Defaults to "refresh_token".
ContextKey string
// ContextKeyEncoded defines the key that will be used to store the encoded
// refresh token on the echo.Context when the token is successfully parsed.
// Optional. Defaults to "refresh_token_encoded".
ContextKeyEncoded string
// CookieKey defines the key that will be used to read the refresh token
// from an HTTP cookie.
// Optional. Defaults to "refresh_token".
CookieKey string
// BodyMIMEType defines the expected MIME type of the request body.
// Returns a 400 Bad Request if the request's Content-Type header does not match.
// Optional. Defaults to "application/json".
BodyMIMEType string
// BodyKey defines the key that will be used to read the refresh token
// from the request's body.
// Returns a 422 UnprocessableEntity if the request's body key is missing.
// Optional. Defaults to "refresh_token".
BodyKey string
// Routes defines routes and methods that require a refresh token.
// Optional. Defaults to /auth/refresh [POST] and /auth/logout [POST].
Routes map[string][]string
}