JWT Auth and Go Func Routing in Google Cloud Functions

Seattle Skyline, Photo by Saurabh Deoras

Let’s discuss how to secure Go based cloud HTTP functions using JWT tokens. Assuming you already know about JWT and cloud functions, I’ll go over a simple Go code pattern that I use as a proxy and an authenticator prior to calling requested cloud function.

An HTTP client calls a router, which parses information in the HTTP request to determine which function to route the request to. It also authenticates the request. The pattern is as follows:

Payload is the serialized content containing information to be sent to a cloud function (Func 1 or 2 in example above) and the JWT encoded token.

// Payload contains payload for a function and a JWT token for authenticating.
type Payload struct {
FuncData *FuncData
TokenString string
}

Data to be sent to function is encoded within FuncData as follows:

// FuncData is a payload for a particular function.
type FuncData struct {
Id int
Data []byte
}

The important info here is the Id of the function. The proxy maintains a map of function id’s and functional literals that is uses to invoke appropriate calls after authenticating the request.

Id’s of various registered functions can simply be a table as shown below:

const (
HandlerAuthOnly = iota
HandlerHelloWorld
)

With these data structures an HTTP client could prepare the HTTP request body with the payload for a particular function.

Let’s assume that the client has JWT token. We will see later on how such tokens could be obtained. First, let’s prepare the payload and create a valid HTTP request. Assuming that the Payload and FuncData are imported under p namespace, we could do following:

data := new(p.Payload)
data.FuncData = new(p.FuncData)
data.FuncData.Id = p.HandlerHelloWorld
data.FuncData.Data = []byte("i am authenticated with jwt!!")
data.TokenString = tokenString

b, err := json.Marshal(data)
if err != nil {
// handle error
}

The serialized payload can now be used to prepare HTTP request:

req, err := http.NewRequest("POST", "https://us-central1-"+
os.Getenv("GCLOUD_PROJECT_NAME")+
".cloudfunctions.net/router",
bytes.NewReader(b))
if err != nil {
// handle error
}

req.Header.Set("Content-Type", "application/json")

And such HTTP request can then be triggered using an HTTP client. It produces a response, which we can analyze later.

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
// handle error
}
defer resp.Body.Close()

So far, we defined a couple of data types to help serialize content for HTTP call. We included the payload for a cloud function and a JWT token in the HTTP request. Let’s now look at the server side.

Server side entry point is an exported HTTP handler. For this discussion, this would be the only exported function on the server side.

func Route(w http.ResponseWriter, r *http.Request) {...}

The server receives the payload inside r *http.Request. It can unpack the body of the HTTP request and take following steps:

  • Authenticate based on JWT token
  • Identify the function to proxy request to
  • Build r *http.Request object with just the func data and trigger an upstream call.

Code below shows the steps to unpack HTTP request and perform basic checks:

if len(secretKey) == 0 {
http.Error(w, "jwt secret is invalid", http.StatusInternalServerError)
return
}

p := new(Payload)

if r.Body == nil {
http.Error(w, "req body is nil", http.StatusBadRequest)
return
}

b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer r.Body.Close()

if err := json.Unmarshal(b, p); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

if len(p.TokenString) == 0 {
http.Error(w, "token is not valid", http.StatusBadRequest)
return
}

Server can now decode JWT token:

token, err := jwt.Parse(p.TokenString, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

return secretKey, nil
})
if err != nil {
http.Error(w, fmt.Sprintf("%s:%v", "invalid token", err), http.StatusBadRequest)
return
}

if token == nil {
http.Error(w, fmt.Sprintf("%s:%v", "invalid token", "nil"), http.StatusBadRequest)
return
}

if !token.Valid {
http.Error(w, fmt.Sprintf("%s:%s", "invalid token", "invalid"), http.StatusBadRequest)
return
}

At this point we are authenticated! You might have noticed that we used the secretKey for decoding, which is a global variable and being instantiated using an environment variable. Server side defines following global variables for bookkeeping:

var once sync.Once
var secretKey []byte
var registry map[int]func(w http.ResponseWriter, r *http.Request)

Server uses sync.Once to initialize the secretKey and registry. sync.Once ensures that we call this initialization only once per setup, i.e., we could have several instances of cloud function running concurrently, however, only one of them will perform the initialization and rest will block till initialization is complete.

func init() {
once.Do(func() {
secretKey = []byte(os.Getenv("JWT_SECRET_KEY"))
registry = make(map[int]func(w http.ResponseWriter, r *http.Request))
registry[HandlerHelloWorld] = helloWorld
})
}

The registry helps server identify which function to call. It can rebuild the HTTP request and forward to respective function as follows

if f, ok := registry[p.FuncData.Id]; ok {
r.Body = ioutil.NopCloser(bytes.NewReader(p.FuncData.Data))
f(w, r)
}

Finally helloWorld can be called after authentication

// helloWorld is called after authentication via Route func.
func helloWorld(w http.ResponseWriter, r *http.Request) {
if r.Body == nil {
http.Error(w, "req body is nil", http.StatusBadRequest)
return
}

b, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer r.Body.Close()

_, _ = fmt.Fprintf(w, "hello world called with: %s", string(b))
}

Finally let’s look at how tokens can be generated. Using the same secretKey, following code snippet shows how a token is generated:

token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["user"] = "name"
tokenString, err := token.SignedString(secretKey)
if err != nil {
// handle error
}

All code shown is available here. I did not get a chance to discuss how to rotate these JWT tokens on a periodic basis. I’ll hopefully cover that in one of my next posts. I hope this gave you an idea on how JWT tokens could be used to add a layer of authentication on Google cloud functions.

I shot the featured image a few years ago on a cloud day, which are certainly not rare in Seattle, but a gap opened up in the clouds and sun directly lit the skylines. It was awesome lighting. I took multiple shots and stitched them together into this panorama.

Software engineer and entrepreneur currently building Kubernetes infrastructure and cloud native stack for edge/IoT and ML workflows.