JSON Web Tokens and Go

This post will discuss what JSON Web Tokens (JWTs) are, discuss how they might be used, show a command line example, and show some basic JWT Go code.

JSON Web Tokens (JWTs) standardize a compact, digitally signed, optionally encrypted representation of JSON data “claims”. A JWT claim is a string key and JSON value in a JWT claim set, a JSON object of zero or more such name/value pairs. The claim set is used as the payload or plaintext of a JSON Web Signature or JSON Web Encryption structure to produce a signed (for authenticity) or encrypted JWT, respectively.

Overview

In practice, JWTs are useful as authentication tokens which can be issued by a token service to a client (often a mobile app). Clients set the token string as a header (often Authorization) in subsequent requests so services may verify the authenticity of the data previously stored in the token.

JWTs can be used similarly to signed client-side web cookies for keeping a user id or session id which cannot be forged, but they differ in a few ways:

Example

A JOSE header describes how a JWT Claims Set is cryptographically prepared, either as a JSON Web Signature (JWS) or JSON Web Encryption structure (JWE).

Regardless of the type, a JWT is represented as a compact, URL-safe string sequence of three ‘.’ separated parts:

  1. base64 encoded header
  2. base64 encoded payload
  3. signature of the dot seprated header and payload.

Here is a demonstration of a signed JWT for auth to a hypothetical service. Follow along by running the commands in your terminal or by verifying with the jwt.io debugger widget.

Part 1: JOSE Header

{
  "alg": "HS256",
  "typ": "JWT"
}

Base64 encoded UTF-8 representation

$ echo -n '{"alg":"HS256","typ":"JWT"}' | base64
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Part 2: Claim Set

JSON claims, marked to expire Aug 8, 2016

{
  "aud": "mobile",
  "exp": 1470646848,
  "iss": "token.service",
  "sub": "dghubble"
}
# base64 encoded UTF-8 representation
$ echo -n '{"aud":"mobile","exp":1470646848,"iss":"token.service","sub":"dghubble"}' | base64
eyJhdWQiOiJtb2JpbGUiLCJleHAiOjE0NzA2NDY4NDgsImlzcyI6InRva2VuLnNlcnZpY2UiLCJzdWIiOiJkZ2h1YmJsZSJ9

The username “dghubble” is used as an example subject identifier here, but in practice a stable identifier such as a user id should be used (users can change their usernames).

Part 3: Signature

Concatenate the header and claim set with a dot separator to get the message. Use the chosen digital signing or MAC algorithm (SHA256 in this case) to sign the message with the secret key (“secret”).

$ MSG=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJtb2JpbGUiLCJleHAiOjE0NzA2NDY4NDgsImlzcyI6InRva2VuLnNlcnZpY2UiLCJzdWIiOiJkZ2h1YmJsZSJ9
$ echo -n $MSG | openssl dgst -sha256 -hmac "secret" -binary | base64
NBh0NUcOg34MoszOuyG+rAkskatslNwjKNsiSuG4D8U=

The final JWT is the string <JOSEHeader>.<payload>.<signature>:

$ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJtb2JpbGUiLCJleHAiOjE0NzA2NDY4NDgsImlzcyI6InRva2VuLnNlcnZpY2UiLCJzdWIiOiJkZ2h1YmJsZSJ9.NBh0NUcOg34MoszOuyG-rAkskatslNwjKNsiSuG4D8U

Paste this JWT in the jwt.io debugger encoded section to see that the data can be decoded and the validity of the JWT can be verified by anyone who knows the secret (“secret”).

bitcoin icon

At a high level, a JWT producer should

  1. Create a JOSE header with the signing algorithm and base64 encode the JSON string
  2. Populate the claim fields and base64 encode the JSON string
  3. Sign the concatenated header and claims
  4. Return the “header.payload.signature” JWT string

and a consumer should:

  1. Get the JOSE Header before the first “.” and read the alg enty. Do not allow none.
  2. Verify the signature
  3. Decode the claims payload into readable key/value pairs

Let’s discuss the header and claims in more detail.

The JOSE Header is a JSON object with JWS or JWE properties specified.

Parameters:

JWS/JWE Params:

The JOSE Header is base64 encoded to produce the first part of the dot separated JWT string.

Claims

Applications define which claims, if any, are required in a particular JWT communication context. However, the spec does require claim names to be unique and for libraries to reject name/value pairs which have the same name (or choose the lexically last pair). To avoid collisions, 3 kinds of claim names are defined.

1. Private Claim Names

If you control the JWT producer and consumer, you may choose your own claim names and take responsibility for preventing name collisions.

Recommendation: Choose short names to keep JWTs compact.

2. Registered Claim Names

Note: A JWT StringOrURI is a case-sensitive string value with the additional constraint that if the string contains “:“, it is a URI. In other words, don’t use “:” except in URI values.

3. Public Claim Names

To define a JWT for public consumption, choose a claim name with a collision resistant name such as an owned domain name or UUID.

JSON Field Ordering

Different libraries may serialize JSON fields in different orders (for example, Go’s encoding/json package happens to sort alphabetically). This produces JWTs which contain the same information, but a different signature. As long as two systems use the same signing key, a JWT issued by either system will validate correctly and a JSON deserialization package should leniently accept different field orderings.

Go Example

The github.com/dgrijalva/jwt-go package provides an implementation of JSON Web Tokens.

A JWT can be issued to a user who has authenticated in some way.

// create a JWT with claims
token := jwt.New(jwt.SigningMethodHS256)
token.Claims["id"] = account.GetID()
token.Claims["iat"] = time.Now().Unix()
token.Claims["exp"] = time.Now().Add(time.Second * 3600 * 24).Unix()
jwtString, err := token.SignedString([]byte("secret"))

A JWT can be read from a http.Request (e.g. from the Header) and validated in order to protect some resource.

jwtString := req.Header.Get("Authorization")
token, err := jwt.Parse(jwtString, "secret")
if err == nil && token.Valid {
    // token parsed, exp/nbf checks out, signature verified, Valid is true
    return token, nil
}
return nil, jwt.ErrNoTokenInRequest

In a Go web app, different handlers or different services may handle granting JWTs and gating access to different resources based on valid JWT headers. It is best to define a JWT Provider interface to wrap jwt-go. This allows a Provider implementation with a configured signing method and secret to be passed to handlers in order to perform JWT grants and validation.

// A Provider creates, signs, and retrieves JSON Web Tokens (JWTs).
type Provider interface {
    // New returns a new JWT Token using the Store's signing method.
    New() *jwt.Token
    // Sign digitally signs the Token to return the JWT byte slice.
    Sign(token *jwt.Token) ([]byte, error)
    // Get gets the valid JWT from the Authorization header. If the token is
    // missing, expired, or the signature does not validate, returns an error.
    Get(req *http.Request) (*jwt.Token, error)
}

The github.com/dghubble/jwts package shows such a Provider abstraction you can copy or import.

// JWT provider (defaults to SHA256, 1 week TTL)
jwtProvider := jwts.New([]byte("secret"))
// custom JWT provider
jwtProvider2 := jwtProvider := jwts.New([]byte("secret"), jwts.Config{Method: jwt.SigningMethodHS256, TTL: 3600 * 24 * 3})

When setting up a ServeMux, pass a jwtProvider to handlers for endpoints which grant JWTs.

func jwtGrantHandler(jwtProvider jwts.Provider) http.Handler {
    fn := func(w http.ResponseWriter, req *http.Request) {
        // create a new JWT with claims, jwts adds "iat" and "exp" claims
        token := h.jwtProvider.New()
        token.Claims["id"] = account.GetID()
        tokenBytes, err := h.jwtProvider.Sign(token)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
        }
        w.Header().Set("Content-Type", "application/json")
        w.Write(tokenBytes)
    }
    return http.HandlerFunc(fn)
}

Define a wrapper handler which requires a JWT before proceeding to protect endpoints.

func requireJWT(jwtProvider jwts.Provider, next ctxh.ContextHandler) ctxh.ContextHandler {
    fn := func(ctx context.Context, w http.ResponseWriter, req *http.Request) {
        token, err := jwtProvider.Get(req)
        if err != nil {
            renderJSON(w, &APIError{Message: "Unauthorized Request"})
            return
        }
        accountFloat, ok := token.Claims["id"].(float64)
        if !ok {
            renderJSON(w, &APIError{Message: "Unauthorized Request"})
            return
        }
        ctx = accounts.WithAccountID(ctx, int64(accountFloat))
        next.ServeHTTP(ctx, w, req)
    }
    return ctxh.ContextHandlerFunc(fn)
}

Finally, you may notice one minor difference between the base64 encodings that openssl produces and those that Go encoding/base64 produces. Go uses the RFC 4648 alternate encoding variant where - and _ replace + and / (for file path safety), while openssl uses the original encoding. Both variants should be accepted.

JWT Goals and Motivations

Security

JWT security could be the topic of multiple articles, but in general JWTs have many of the same concerns as web cookies or other access tokens.

Reference