JWT Auth and Go Func Routing in Google Cloud Functions

JWT Auth and Go Func Routing in Google Cloud FunctionsSaurabh DeorasBlockedUnblockFollowFollowingJan 21Seattle Skyline, Photo by Saurabh DeorasLet’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:The PayloadPayload 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.

HTTP CallLet’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.

HandlerHelloWorlddata.

FuncData.

Data = []byte("i am authenticated with jwt!!")data.

TokenString = tokenStringb, 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 SideServer 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 tokenIdentify the function to proxy request toBuild 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.

Oncevar secretKey []bytevar 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 followsif f, ok := registry[p.

FuncData.

Id]; ok { r.

Body = ioutil.

NopCloser(bytes.

NewReader(p.

FuncData.

Data)) f(w, r)}Hello WorldFinally 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))}Generating TokensFinally 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}Code and Remaining ItemsAll 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.

Seattle SkylineI 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.

.. More details

Leave a Reply