Skip to main content

JSON Web Tokens and Go

<time datetime="2015-08-12 00:00:00 &#43;0000 UTC">12 Aug 2015</time><span class="px-2 text-primary-500">&middot;</span><span title="Reading time">9 mins</span>

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:

  • JWTs provide flexibility over cookie-based authentication. JWTs are URL-safe and can be passed as a header, query param, or post param over a secure channel. JWTs work well with any platform which can work with string tokens, which makes them a good fit for mobile clients.

  • JWTs can be symetrically or asymetrically signed. For authentication, symetric signatures work well since the server just validates token signatures, the client does not read the claims of the token. If needed, JWTs can be signed by a private key and verified with a public key by clients.

  • JWTs are decoupled from authentication protocols such as OAuth1 and OAuth2. For example, a backend service for a mobile app which uses 3rd-party login might allow a 3rd-party OAuth token to be exchanged for a JWT for the app’s API.

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).

  • JWS - the claims are digitally signed or MAC’d and set as the JWS payload.
  • JWE - the claims are encrypted and set as the JWE plaintext.

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:

  • “typ” - recommended to always use the value “JWT”
  • “cty” - unless nested signing or encryption is needed, don’t use

JWS/JWE Params:

  • “alg” - the cryptographic algorithm used to secure a JWS (e.g. SHA256, SHA512)

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 #

  • iss - Issuer who granted the JWT (e.g. “auth.service”) as a StringOrURI.
  • sub - Subject of the JWT (often a user) StringOrURI which should either be a globally unqiue value or locally unique within a JWT communication context (“123121432423”, “dghubble”).
  • aud - Audience who should receive or consume the JWT, typically a JSON array of one or more StringOrURI values (e.g. [“mobile”, “ios”, “android”]).
  • exp - Expiration time as a JSON numeric number of seconds since the epoch, after which the JWT will be rejected by library implementations.
  • nbf - Similar to “exp”, but the numeric time before which the JWT should not be accepted.
  • iat - Issued at time which defines when the JWT was issued.
  • jti - A globally unique JWT token id string used to identify the JWT.

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 #

  • Allow compact JWT’s to be passed in an HTTP Header or in URI query arguments
  • Offer a flexible claims system in which applications choose the claims to require and how to treat different claims
  • Provide a richer alternative to SWT (Simple Web Tokens) which allow only string claim values
  • Provide a simpler alternative to SAML (Security Assertion Markup Language) XML

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.

  • Avoid storing sensitive information in a JWT, just as you avoid storing sensitive information in cookies. If you must, use a JWE to encrypt the contents.
  • JWT theft or sniffing will grant an attacker access to the protected resources
  • Communications must occur over a secure channel (use HTTPS)
  • Replay attacks
  • Clients must store JWTs in a safe way
  • Clients must not send JWTs to untrusted servers (browsers agree to prevent cross-site cookie sending, JWT clients can shoot themselves in the foot).
  • JWT packages must disallow the none algorithm and should ensure unverified JWTs specify the expected signing algorithm (jwts does this).

Reference #