JSON Web Token
- The JSON Web Token (JWT) is a compact, self-contained, signed JSON object that represents a security token crafted by auth0
- It is a base64 encoded string that contains a header, a payload and a signature
- The header contains the type of token and the algorithm used to sign the token
- The payload contains the claims of the token
- The signature is used to verify the token
- It is a standard way to authenticate and authorize users
- The claims may contain an expiration date useful for session handling
- Several implementations are available, like the Tideland Go JSON Web Token](https://pkg.go.dev/tideland.dev/go/jwt)
- Login and retrieving of right credentials has to be done via redirection to the authorization URL
HTTPX
JWTHandler as a wrapper for other handlers
package httpx
import (
"encoding/json"
"encoding/xml"
"net/http"
"strconv"
"time"
"tideland.dev/go/jwt"
)
// JWTHandlerConfig allows to control how the JWT handler works.
// All values are optional. In this case tokens are only decoded
// without using a cache, validated for the current time plus/minus
// a minute leeway, and there's no user defined gatekeeper function
// running afterwards.
type JWTHandlerConfig struct {
Cache *jwt.Cache
Key jwt.Key
Leeway time.Duration
Gatekeeper func(w http.ResponseWriter, r *http.Request, claims jwt.Claims) error
}
// JWTHandler checks for a valid token and then runs
// a gatekeeper function.
type JWTHandler struct {
handler http.Handler
cache *jwt.Cache
key jwt.Key
leeway time.Duration
gatekeeper func(w http.ResponseWriter, r *http.Request, claims jwt.Claims) error
}
// NewJWTHandler creates a handler checking for a valid JSON
// Web Token in each request.
func NewJWTHandler(handler http.Handler, config *JWTHandlerConfig) *JWTHandler {
h := &JWTHandler{
handler: handler,
leeway: time.Minute,
}
if config != nil {
if config.Cache != nil {
h.cache = config.Cache
}
if config.Key != nil {
h.key = config.Key
}
if config.Leeway != 0 {
h.leeway = config.Leeway
}
if config.Gatekeeper != nil {
h.gatekeeper = config.Gatekeeper
}
}
return h
}
// ServeHTTP implements the http.Handler interface. It checks for an existing
// and valid token before calling the wrapped handler.
func (h *JWTHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.isAuthorized(w, r) {
h.handler.ServeHTTP(w, r)
}
}
// isAuthorized checks the request for a valid token and if configured
// asks the gatekeepr if the request may pass.
func (h *JWTHandler) isAuthorized(w http.ResponseWriter, r *http.Request) bool {
var token *jwt.JWT
var err error
switch {
case h.cache != nil && h.key != nil:
token, err = h.cache.RequestVerify(r, h.key)
case h.cache != nil && h.key == nil:
token, err = h.cache.RequestDecode(r)
case h.cache == nil && h.key != nil:
token, err = jwt.RequestVerify(r, h.key)
default:
token, err = jwt.RequestDecode(r)
}
// Now do the checks.
if err != nil {
h.deny(w, r, err.Error(), http.StatusUnauthorized)
return false
}
if token == nil {
h.deny(w, r, "no JSON Web Token", http.StatusUnauthorized)
return false
}
if !token.IsValid(h.leeway) {
h.deny(w, r, "the JSON Web Token claims 'nbf' and/or 'exp' are not valid", http.StatusForbidden)
return false
}
if h.gatekeeper != nil {
err := h.gatekeeper(w, r, token.Claims())
if err != nil {
h.deny(w, r, "access rejected by gatekeeper: "+err.Error(), http.StatusUnauthorized)
return false
}
}
// All fine.
return true
}
// deny sends a negative feedback to the caller.
func (h *JWTHandler) deny(w http.ResponseWriter, r *http.Request, msg string, statusCode int) {
feedback := map[string]string{
"statusCode": strconv.Itoa(statusCode),
"message": msg,
}
accept := r.Header.Get(HeaderAccept)
w.WriteHeader(statusCode)
err := WriteBody(w, accept, feedback)
if err != nil {
logger.Errorf("JWT handler: %v", err)
}
}
Example
NestedMux
wraps several business logic handlers
LoggingHandler
wraps the NestedMux
and logs the requests
JWTHandler
wraps the LoggingHandler
and checks for a valid token
ServeMux
wraps the LoggingHandler
and serves all requests
- Gatekeeper here only looks for existing claim
access
with the value allowed
package main
import (
"log"
"net/http"
"os"
"./pkg/httpx"
"./pkg/user"
)
const (
// Prefix for API calls.
apiPrefix = "/api/v1"
// Claim name for access.
accessClaim = "access"
)
func main() {
mux := http.NewServeMux()
apimux := httpx.NewNestedMux(apiPrefix)
apilogger := httpx.NewLoggingHandler(log.New(os.Stdout, "api: ", log.LstdFlags), apimux)
gatekeeper := func(w http.ResponseWriter, r *http.Request, claims jwt.Claims) error {
access, ok := claims.GetString(accessClaim)
if !ok || access != "allowed" {
return errors.New("access is not allowed")
}
return nil
}
apijwt := httpx.NewJWTHandler(apilogger, &httpx.JWTHandlerConfig{
Key: []byte("secret"),
Gatekeeper: gatekeeper,
})
apimux.Handle("users", httpx.MethodWrapper(user.NewUsersHandler()))
apimux.Handle("users/addresses", httpx.MethodWrapper(user.NewUsersAddressesHandler()))
apimux.Handle("users/contracts", httpx.MethodWrapper(user.NewUsersContractsHandler()))
// Register further nested API handlers, then
// let the multiplexer serve all API requests.
mux.Handle(apiPrefix, apijwt)
// Register further multiplexed handlers, e.g. for static files.
http.ListenAndServe(":8080", mux)
}
Links