Hello again. To start off the new year, I’m going to do a “Snippets” post focused on using JWTs in Go.

First things first, here as some helpful references that likely contain all the info you want:

Since this is a glorified excuse for me to write down some boilerplate patterns, for the sake of brevity I’m going to skip most of the typical intro matter relating to JWTs (i.e., what are they, what you can use them for, what the tradeoffs are, etc). There’s much better resources for that on the Internet that most readers should either read or are already familiar with.

If you’re really unfamiliar with JWTs and for some reason still want to read this post, you can think of JWTs as those Take-A-Number tickets that you used to grab at delis and bakeries (in this analogy our application is the business running the counter). The business hands out tickets (with associated metadata like ticket number) to customers, who then later give them back to us to prove they’re supposed to be served. The neat part of JWTs is that we can (cryptographically) guarantee that the token (ticket) that a customer gives us really did come from our application (ticket machine).

With that out of the way, it’s time to get into it. The snippets I’m going to post typically take the form of middleware that wrap handlers on an HTTP server, since that’s primary domain where I’ve used JWTs. Let’s assume the client will be passing an access token via an HTTP header and a refresh token via an HTTP cookie.

The first snippet I’m going to show will be the necessary constants and struct definitions we’ll need in these middleware implementations.

type contextKey int

const (
    accessTokenContextKey       contextKey = 0
    accessTokenClaimsContextKey contextKey = 1
    refreshTokenCookieName      string = "refresh_token"
)

type CustomJWT struct {
    *jwt.StandardClaims
    isAdmin bool `json:"is_admin"`
}

The next snippet will show how to generate a JWT that you can use as an access token for authorization (e.g., like in an HTTP header Authorization: Bearer [access_token] manner). We’re just taking a StandardClaims instance from the golang-jwt/jwt package and embedding it in our custom JWT struct. We’re passing in a secret key to the signing function. This key should never be shared; anyone with the secret can generate tokens on your behalf, so it should be some lengthy string nobody can guess or steal.

func generateAccessToken(expiresAt time.Time) (string, error) {
    t := jwt.New(jwt.SigningMethodHS256)
    standardClaims := &jwt.StandardClaims{
        ExpiresAt: expiresAt.Unix(),
    }
    t.Claims = &CustomJWT{
        StandardClaims: standardClaims,
        isAdmin: false,
    }
    return t.SignedString([]byte(os.Getenv("SECRET_KEY")))
}

Now that you can issue tokens, what do you do when you receive them? You might want to move them from the request header (or from an HTTP Cookie) into the request context. Here’s how to do that:

func bearerToContext() handlerAdapter {
    return func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
            ctx := context.WithValue(r.Context(), accessTokenContextKey, token)
            r = r.WithContext(ctx)
            next(w, r)
        }
    }
}

func cookieToContext() handlerAdapter {
    return func(next http.HandlerFunc) http.HandlerFunc {
        type response struct {
            Error string `json:"error"`
        }
        return func(w http.ResponseWriter, r *http.Request) {
            c, err := r.Cookie(refreshTokenCookieName)
            if err != nil {
                if errors.Is(err, http.ErrNoCookie) {
                    resp := response{Error: fmt.Sprintf("No %s cookie.", refreshTokenCookieName)}
                    w.WriteHeader(http.StatusUnauthorized)
                    json.NewEncoder(w).Encode(resp)
                    return
                }
                resp := response{Error: "Error extracting JWT cookie from request."}
                w.WriteHeader(http.StatusInternalServerError)
                json.NewEncoder(w).Encode(resp)
                return
            }
            token := c.Value
            ctx := context.WithValue(r.Context(), refreshTokenContextKey, token)
            r = r.WithContext(ctx)
            next(w, r)
        }
    }
}

Ok, so at this point we can move any token type we want into the request context. This lets us access whatever tokens we want uniformly via r.Context(). Let’s write a middleware that extracts an access token from the context, parses it into our custom struct, and adds the result to the request context. We’ll also add a simple middleware function to check that the user agent making the request provided a token that has admin privileges.

func parseBearerClaimsToContext() handlerAdapter {
    return func(next http.HandlerFunc) http.HandlerFunc {
        type response struct {
            Error string `json:"error"`
        }
        return func(w http.ResponseWriter, r *http.Request) {
            // NOTE: assumes middleware guarantees access_token is present
            access_token := r.Context().Value(accessTokenContextKey).(string)
            kf := func(token *jwt.Token) (interface{}, error) {
                return []byte(os.Getenv("SECRET_KEY")), nil
            }
            claims := &CustomJWT{}
            token, err := jwt.ParseWithClaims(access_token, claims, kf)
            if err != nil {
                if errors.Is(err, jwt.ErrSignatureInvalid) {
                    resp := response{Error: "Invalid token signature."}
                    w.WriteHeader(http.StatusUnauthorized)
                    json.NewEncoder(w).Encode(resp)
                    return
                }
                resp := response{Error: "Failed to parse JWT."}
                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(resp)
                return
            }
            if !token.Valid {
                resp := response{Error: "Invalid token."}
                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(resp)
                return
            }
            ctx := context.WithValue(r.Context(), accessTokenClaimsContextKey, claims)
            r = r.WithContext(ctx)
            next(w, r)
        }
    }
}

func isAdmin() handlerAdapter {
    return func(next http.HandlerFunc) http.HandlerFunc {
        type response struct {
            Error string `json:"error"`
        }
        return func(w http.ResponseWriter, r *http.Request) {
            // NOTE: assumes middleware guarantees claims is present
            claims := r.Context().Value(accessTokenClaimsContextKey).(CustomJWT)
            if !claims.isAdmin {
                resp := response{Error: "Bearer is not admin."}
                w.WriteHeader(http.StatusUnauthorized)
                json.NewEncoder(w).Encode(resp)
                return
            }
            next(w, r)
        }
    }
}

Great. So how do we wire this all together? Below are some example routes. These are overly verbose due to the contrived middleware use cases implemented above, but you should get the idea. Also note the adaptHandler pattern I’m using here; see My HTTP handlers post if you’re not clear with what’s going on here:

func (s *service) setupHTTPRoutes() {
    s.router.Handle("/auth/get-access-token", adaptHandler(
        s.getAccessToken(),
        setContentType("application/json"),
        cookieToContext(),
    )).Methods("GET")
    s.router.Handle("/users/create", adaptHandler(
        s.createUser(),
        setContentType("application/json"),
        bearerToContext(),
        parseBearerClaimsToContext(),
        isAdmin(),
    )).Methods("GET")
}

At this point we’ve made our CustomJWT available in the request context. Subsequent handlers can leverage the metadata included in the token and associate with the request (e.g., the username or account ID that’s making the request). Of course, this is dependent on our application issuing tokens with the relevant metadata, but that’s trivially done by adding fields to CustomJWT and updating the logic responsible for issuing tokens (which may or may not live in generateAccessToken).

That’s all for this post. Hopefully these snippets are helpful and illustrate a starting point for leveraging JWTs in your Go applications.