JSON Web Tokens are a popular way of managing authorization and authentication policies for web applications. Sending around a base64-encoded token that can be verified by every service using it, enables uncomplicated (and stateless) application design compared to other mechanisms.
In many cases, JWT-based flows will be built around asymmetric signing and verification, meaning that instead of using the same secret key for both signing a token on the issuing side and verifying a token on the consuming side, we'll have a private and public key (in our example an RSA keypair), that will be used to generate the signature using the private key and verify it using the public key that can be distributed freely.
One widely-adopted distribution method of keys is the concept of a JSON Web Key (defined in RFC7517), which specifies that cryptographic
keys can be represented as JSON, allowing us to upload a collection of our public keys (called a JSON Web Key Set, JWKS in short) to an accessible endpoint (usually .well-known/jwks.json
),
to be then fetched by internal services to verify tokens without needing any further configuration.
Now to the services that will implement the necessary business to verify our token signatures: One of the most well-maintained Go libraries for all things JWTs (and all other related mechanisms),
is go-jose
by Square. While you can build everything around JavaScript Object Signing and Encryption with it, getting started easier said than done.
Let's assume we're building a simple flow to verify tokens of incoming requests using the JSON Web Key Set we're hosting on our public domain. The first step will be to fetch the JWKS and parse it, and while there might be libraries for this process, it's really straightforward to build as it's just a request and some marshalling to a provided struct on our part:
package main
import (
"encoding/json"
"fmt"
"gopkg.in/square/go-jose.v2"
"io/ioutil"
"net/http"
)
func fetchJwks() (*jose.JSONWebKeySet, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", os.Getenv("JWKS_URL"), nil)
if err != nil {
return nil, fmt.Errorf("could not create jwks request: %w", err)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("could not fetch jwks: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return nil, fmt.Errorf("received non-200 response code")
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("could not read response body: %w", err)
}
jwks := jose.JSONWebKeySet{}
err = json.Unmarshal(body, &jwks)
if err != nil {
return nil, fmt.Errorf("could not unmarshal jwks into struct: %w", err)
}
return &jwks, nil
}
While this is just a plain request, we can even enhance it by adding some lightweight caching, so that we don't have to fetch the JWKS every time, but that's optional.
func verifyToken(bearerToken string) error {
// Parse bearer token from request
token, err := jwt.ParseSigned(bearerToken)
if err != nil {
return fmt.Errorf("could not parse Bearer token: %w", err)
}
// Get jwks
jsonWebKeySet, err := fetchJwks()
if err != nil {
return fmt.Errorf("could not load JWKS: %w", err)
}
// Get claims out of token (validate signature while doing that)
claims := jwt.Claims{}
err = token.Claims(jsonWebKeySet, &claims)
if err != nil {
return fmt.Errorf("could not retrieve claims: %w", err)
}
// Validate claims (issuer, expiresAt, etc.)
err = claims.Validate(jwt.Expected{})
if err != nil {
return fmt.Errorf("could not validate claims: %w", err)
}
return nil
}
This is everything we need to parse and validate JSON Web Tokens from incoming requests: First, we attempt to
parse the signed token into an internal data structure, then we extract the claims (all internal fields) while
validating the signature using our JSON Web Key Set we fetched from the remote endpoint. It's important that
tokens include the Key ID (kid
) claim that can be found in the JWKS, otherwise, this step will fail. Finally, we'll validate
the token claims, such as expiresAt
and the issuer, for example.
If we need to retrieve any claims for us to use later, for example, the token subject, we can simply pull it out of the claims after they're validated.
I hoped you enjoyed this post and might have an easier time to build your token logic for future projects! If you've got any questions, suggestions, or feedback in general, don't hesitate to reach out on Twitter or by mail.