jwt

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 18, 2024 License: MIT Imports: 10 Imported by: 1

README

echo-jwt Go Report Card codecov

A JWT middleware for the Echo framework using lestrrat-go/jwx.

Motivation

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.

Installing

go get github.com/alexferl/echo-jwt

Using

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
Code example
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"}
Exempt routes

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,
}))
Configuration
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
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrAuthorizationHeader        = "authorization header malformed"
	ErrAuthorizationHeaderStatus  = http.StatusUnauthorized
	ErrAuthorizationScheme        = "authorization scheme not supported"
	ErrAuthorizationSchemeStatus  = http.StatusUnauthorized
	ErrBodyMissingKey             = "body missing refresh token key"
	ErrBodyMissingKeyStatus       = http.StatusUnprocessableEntity
	ErrMethodNotAllowed           = "method not allowed"
	ErrMethodNotAllowedStatus     = http.StatusMethodNotAllowed
	ErrRequestMalformed           = "request malformed"
	ErrRequestMalformedStatus     = http.StatusBadRequest
	ErrRouteNotFound              = "route not found"
	ErrRouteNotFoundStatus        = http.StatusNotFound
	ErrTokenExpired               = "token expired"
	ErrTokenExpiredStatus         = http.StatusUnauthorized
	ErrTokenInvalid               = "token invalid"
	ErrTokenInvalidStatus         = http.StatusUnauthorized
	ErrTokenInvalidIssuedAt       = "token invalid issued at"
	ErrTokenInvalidIssuedAtStatus = http.StatusUnauthorized
	ErrTokenNotYetValid           = "token not yet valid"
	ErrTokenNotYetValidStatus     = http.StatusUnauthorized
)
View Source
var DefaultConfig = Config{
	Skipper:         middleware.DefaultSkipper,
	ExemptRoutes:    map[string][]string{"/login": {http.MethodPost}},
	ExemptMethods:   []string{http.MethodOptions},
	OptionalRoutes:  map[string][]string{},
	ParseTokenFunc:  parseToken,
	Options:         []jwt.ParseOption{jwt.WithValidate(true)},
	ContextKey:      "token",
	CookieKey:       "access_token",
	AuthHeader:      "Authorization",
	AuthScheme:      "Bearer",
	UseRefreshToken: false,
	RefreshToken: &RefreshToken{
		ContextKey:        "refresh_token",
		ContextKeyEncoded: "refresh_token_encoded",
		CookieKey:         "refresh_token",
		BodyMIMEType:      echo.MIMEApplicationJSON,
		BodyKey:           "refresh_token",
		Routes: map[string][]string{
			"/auth/refresh": {http.MethodPost},
			"/auth/logout":  {http.MethodPost},
		},
	},
}

Functions

func JWT

func JWT(key any) echo.MiddlewareFunc

func JWTWithConfig

func JWTWithConfig(config Config) echo.MiddlewareFunc

Types

type Config

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 added in v0.4.0

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
}

type TokenSource added in v0.6.0

type TokenSource int
const (
	Unset TokenSource = iota
	Cookie
	Header
)

func (TokenSource) String added in v0.6.0

func (s TokenSource) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL