How to Secure a Scrappy Twitter API App on Go with Magic

Magic Staff · January 20, 2021
How to Secure a Scrappy Twitter API App on Go with Magic

Hi there 🙋🏻‍♀️. The Scrappy Twitter API is a Go-backend project that is secured by the Magic Admin SDK for Go. This SDK makes it super easy to leverage Decentralized ID (DID) Tokens to authenticate your users for your app.

#⁠Demo

Click here to test out our Live demo by importing the Magic-secured Scrappy Twitter API Postman collection.

Alternatively, you could also manually import this static snapshot of the collection: https://www.getpostman.com/collections/595abf685418eeb96401

This Postman collection has the following routes:

  • POST a tweet (protected): https://scrappy-secure-go-twitter-api.wl.r.appspot.com/tweet
  • GET all tweets (unprotected): https://scrappy-secure-go-twitter-api.wl.r.appspot.com/tweets
  • GET a single tweet (unprotected): https://scrappy-secure-go-twitter-api.wl.r.appspot.com/tweet/1
  • DELETE a tweet (protected): https://scrappy-secure-go-twitter-api.wl.r.appspot.com/tweet/2

You’ll only be able to make requests with the Get All Tweets and Get a Single Tweet endpoints because they’re unprotected. To post or delete a tweet, you’ll need to pass in a DID token to the Request Header.

💁🏻‍♀️ Create an account here to get a DID token.

Great! Now that you’ve got a DID token, you can pass it into the Postman Collection’s HTTP Authorization request header as a Bearer Token and be able to send a Create a Tweet or Delete a Tweet request.

Keep reading if you want to learn how we secured this Go-backed Scrappy Twitter API with the Magic Admin SDK for Go. 🪄🔐

#A High-level View

Here are the building blocks of this project and how each part connects to one another.

#Client

🔗 GitHub Repo ⁠This Next.js app authenticates the user and generates the DID token required to make POST or DELETE requests with the Scrappy Twitter API. ⁠ ⁠✨ Noteworthy Package Dependencies:

  • Magic SDK: Allows users to sign up or log in.
  • SWR: Lets us get user info using a hook.
  • @hapi/iron: Lets us encrypt the login cookie for more security.

#Server

🔗 GitHub Repo

⁠This Go server is where all of the Scrappy Twitter API requests are handled. Once the user has generated a DID token from the client side, they can pass it into their Request Header as a Bearer token to hit protected endpoints. ⁠ ⁠✨ API routes:

⁠✨ Noteworthy Packages:

⁠⁠In this article, we’ll only be focusing on the Server’s code to show you how we secured the Go Rest API.

#Getting Started

#Prerequisites

#Magic

  1. Sign up for an account on Magic.
  2. Create an app.
  3. Keep this Magic tab open. You’ll need both of your app’s Publishable and Secret keys soon.

#Server

  1. git clone https://github.com/magiclabs/scrappy-twitter-api-server
  2. cd scrappy-twitter-api-server
  3. mv .env.example .env
  4. Go back to the Magic tab to copy your app’s Secret Key and paste it as the value for MAGIC_SECRET_KEY in .env:
    • MAGIC_SECRET_KEY = sk_XXXXXXXXXX;
  5. Run all .go files with go run .

#Client

  1. git clone https://github.com/magiclabs/scrappy-twitter-api-client
  2. cd scrappy-twitter-api-client
  3. mv .env.local.example .env.local
  4. Populate .env.local with the correct Live keys from your Magic app:
    • NEXT_PUBLIC_MAGIC_PUBLISHABLE_KEY=pk_XXXXX
    • NEXT_PUBLIC_HAPI_IRON_SECRET=this-is-a-secret-value-with-at-least-32-characters
    • Note: The NEXT_PUBLIC_HAPI_IRON_SECRET is needed by @hapi/iron to encrypt an object. Feel free to leave the default value as is while in DEV.
  5. Install package dependencies: yarn
  6. Start the Next.js production server: yarn dev

#Postman

  1. Import the DEV version of the Scrappy Twitter API Postman Collection:

    01[![Run in Postman](https://run.pstmn.io/button.svg)](https://god.postman.co/run-collection/1aa913713995cb16bb70)
  2. Generate a DID token on the Client you just started up.

    Note: When you log in from the Client side, the Magic Client SDK generates a DID token which is then converted to an ID token so that it has a longer lifespan (8 hours).

  3. Pass this DID token as a Bearer token into the collection’s HTTP Authorization request header.

Awesome! Now that you have your own local Next.js client and Go server running, let's dive into the server's code.

#The Scrappy Twitter Go Rest API

#File Structure

This is a simplified view of the Go server's file structure:

01├── README.md
02├── .env
03├── main.go
04├── structs.go
05├── handlers.go

#A Local Database

To keep things simple, when you create or delete a tweet, instead of updating an external database, the Tweets array that’s globally defined and initialized in structs.go is updated accordingly.

01// Tweet is struct or data type with an Id, Copy and Author
02type Tweet struct {
03   ID     string `json:"ID"`
04   Copy   string `json:"Copy"`
05   Author string `json:"Author"`
06}
07
08// Tweets is an array of Tweet structs
09var Tweets []Tweet

And when you get all tweets, or a single tweet, the same Tweets array is sent back to the client.

#The Routes and Handlers

In summary, this Scrappy Twitter Go Rest API has 4 key routes that are defined in main.go’s handleRequests function:

  1. GET "/tweets" to get all tweets

    01myRouter.HandleFunc("/tweets", returnAllTweets)
  2. DELETE "/tweet/{id}" to delete a tweet

    01myRouter.HandleFunc("/tweet/{id}", deleteATweet).Methods("DELETE")
  3. GET "/tweet/{i}" to get a single tweet

    01myRouter.HandleFunc("/tweet/{id}", returnSingleTweet)
  4. POST "/tweet" to create a tweet

    01myRouter.HandleFunc("/tweet", createATweet).Methods("POST")

Note: The POST and DELETE routes are currently unprotected. Move to the next section to see how we can use a DID token to protect them.

As you can see, each of these routes have their own handlers to properly respond to requests. These handlers are defined in handlers.go:

  1. GET "/tweets" => returnAllTweets()

    01// Returns ALL tweets ✨
    02func returnAllTweets(w http.ResponseWriter, r *http.Request) {
    03   fmt.Println("Endpoint Hit: returnAllTweets")
    04   json.NewEncoder(w).Encode(Tweets)
    05}
  2. DELETE "/tweet/{id}" => deleteATweet()

    01// Deletes a tweet ✨
    02func deleteATweet(w http.ResponseWriter, r *http.Request) {
    03   fmt.Println("Endpoint Hit: deleteATweet")
    04
    
    05   // Parse the path parameters
    06   vars := mux.Vars(r)
    07
    
    08   // Extract the `id` of the tweet we wish to delete
    09   id := vars["id"]
    10
    
    11   // Loop through all our tweets
    12   for index, tweet := range Tweets {
    13
    
    14       /*
    15           Checks whether or not our id path
    16           parameter matches one of our tweets.
    17       */
    18       if tweet.ID == id {
    19
    
    20           // Updates our Tweets array to remove the tweet
    21           Tweets = append(Tweets[:index], Tweets[index+1:]...)
    22       }
    23   }
    24
    
    25   w.Write([]byte("Yay! Tweet has been DELETED."))
    26}
  3. GET "/tweet/{i}" => returnSingleTweet()

    01// Returns a SINGLE tweet ✨
    02func returnSingleTweet(w http.ResponseWriter, r *http.Request) {
    03   fmt.Println("Endpoint Hit: returnSingleTweet")
    04   vars := mux.Vars(r)
    05   key := vars["id"]
    06
    
    07   /*
    08       Loop over all of our Tweets
    09       If the tweet.Id equals the key we pass in
    10       Return the tweet encoded as JSON
    11   */
    12   for _, tweet := range Tweets {
    13       if tweet.ID == key {
    14           json.NewEncoder(w).Encode(tweet)
    15       }
    16   }
    17}
  4. POST "/tweet" => createATweet()

    01// Creates a tweet ✨
    02func createATweet(w http.ResponseWriter, r *http.Request) {
    03   fmt.Println("Endpoint Hit: createATweet")
    04   /*
    05       Get the body of our POST request
    06       Unmarshal this into a new Tweet struct
    07   */
    08   reqBody, _ := ioutil.ReadAll(r.Body)
    09   var tweet Tweet
    10   json.Unmarshal(reqBody, &tweet)
    11
    
    12   /*
    13       Update our global Tweets array to include
    14       Our new Tweet
    15   */
    16   Tweets = append(Tweets, tweet)
    17   json.NewEncoder(w).Encode(tweet)
    18
    
    19   w.Write([]byte("Yay! Tweet CREATED."))
    20}

#Securing the Go Rest API with Magic Admin SDK

Now it’s time to show you how to protect the DELETE "/tweet/{id}" and POST "/tweet" routes, such that only authenticated users are able to create a tweet and only the author of a specific tweet is allowed to delete it.

#Magic Setup

  1. Get the Go Magic Admin SDK package: go get github.com/magiclabs/magic-admin-go

  2. Configure the Magic Admin SDK in handlers.go:

    1. Import the following packages:

      01"github.com/joho/godotenv"
      02"github.com/magiclabs/magic-admin-go"
      03"github.com/magiclabs/magic-admin-go/client"
      04"github.com/magiclabs/magic-admin-go/token"
    2. Load the .env file and get the Live Secret Key:

      01// Load .env file from given path
      02var err = godotenv.Load(".env")
      03
      
      04// Get env variables
      05var magicSecretKey = os.Getenv("MAGIC_SECRET_KEY")
    3. Instantiate Magic:

      01var magicSDK = client.New(magicSecretKey, magic.NewDefaultClient())

#Magic Admin SDK for Go

In order to protect the routes to POST or DELETE a tweet, we’ll be creating a Gorilla Mux middleware to check whether or not the user is authorized to make requests to these endpoints.

💡 You can think of a middleware as reusable code for HTTP request handling.

#checkBearerToken()

Let’s call this middleware checkBearerToken() and define it in handlers.go.

To implement the middleware behavior, we’ll be using chainable closures. This way, we could wrap each handler with a checkBearerToken middleware.

Here’s the initial look of our function:

01func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
02   return func(res http.ResponseWriter, req *http.Request) {
03      /* More code is coming! */
04      next(res, req)
05   }
06}

💁🏻‍♀️ Now let’s update checkBearerToken to make sure the DID token exists in the HTTP Header Request. If it does, store the value into a variable called didToken:

01func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
02   fmt.Println("Middleware Hit: checkBearerToken")
03   return func(res http.ResponseWriter, req *http.Request) {
04
05       // Check whether or not DIDT exists in HTTP Header Request
06       if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
07           fmt.Fprintf(res, "Bearer token is required")
08           return
09       }
10
11       // Retrieve DIDT token from HTTP Header Request
12       didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
13
14      /* More code is coming! */
15      next(res, req)
16   }
17}

Cool. Now that we’ve got a DID token, we can use it to create an instance of a Token. The Token resource provides methods to interact with the DID Token. We’ll need to interact with the DID Token to get the authenticated user’s information. But first, we’ll need to validate it.

💁🏻‍♀️ Update checkBearerToken to include this code:

01func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
02   fmt.Println("Middleware Hit: checkBearerToken")
03   return func(res http.ResponseWriter, req *http.Request) {
04
05       // Check whether or not DIDT exists in HTTP Header Request
06       if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
07           fmt.Fprintf(res, "Bearer token is required")
08           return
09       }
10
11       // Retrieve DIDT token from HTTP Header Request
12       didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
13
14       // Create a Token instance to interact with the DID token
15       tk, err := token.NewToken(didToken)
16       if err != nil {
17           fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
18           res.Write([]byte(err.Error()))
19           return
20       }
21
22       // Validate the Token instance before using it
23       if err := tk.Validate(); err != nil {
24           fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
25           return
26       }
27
28      /* More code is coming! */
29      next(res, req)
30   }
31}

Now that we’ve validated the Token (tk), we can call tk.GetIssuer() to retrieve the iss; a Decentralized ID of the Magic user who generated the DID Token. We’ll be passing iss into magicSDK.User.GetMetadataByIssuer to get the authenticated user’s information.

💁🏻‍♀️ Update checkBearerToken again:

01func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
02   fmt.Println("Middleware Hit: checkBearerToken")
03   return func(res http.ResponseWriter, req *http.Request) {
04
05       // Check whether or not DIDT exists in HTTP Header Request
06       if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
07           fmt.Fprintf(res, "Bearer token is required")
08           return
09       }
10
11       // Retrieve DIDT token from HTTP Header Request
12       didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
13
14       // Create a Token instance to interact with the DID token
15       tk, err := token.NewToken(didToken)
16       if err != nil {
17           fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
18           res.Write([]byte(err.Error()))
19           return
20       }
21
22       // Validate the Token instance before using it
23       if err := tk.Validate(); err != nil {
24           fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
25           return
26       }
27
28       // Get the user's information
29       userInfo, err := magicSDK.User.GetMetadataByIssuer(tk.GetIssuer())
30       if err != nil {
31           fmt.Fprintf(res, "Error when calling GetMetadataByIssuer: %s", err.Error())
32           return
33       }
34
35      /* More code is coming! */
36      next(res, req)
37   }
38}

Awesome. If the request was able to make it past this point, then we can be assured that it was an authenticated request. All we need to do now is pass the user’s information to the handler the middleware is chained to. We’ll be using Go's Package context to achieve this.

💡 In short, the Package context will make it easy for us to store objects as context values and pass them to all handlers that are chained to our checkBearerToken middleware.

Make sure to import the "context" in handlers.go.

Then create a userInfoKey at the top of handlers.go (we'll be passing userInfoKey into context.WithValue soon):

01type key string
02const userInfoKey key = "userInfo"

💁🏻‍♀️ Update checkBearerToken one last time to use context values to store the user’s information:

01func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
02   fmt.Println("Middleware Hit: checkBearerToken")
03   return func(res http.ResponseWriter, req *http.Request) {
04
05       // Check whether or not DIDT exists in HTTP Header Request
06       if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
07           fmt.Fprintf(res, "Bearer token is required")
08           return
09       }
10
11       // Retrieve DIDT token from HTTP Header Request
12       didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
13
14       // Create a Token instance to interact with the DID token
15       tk, err := token.NewToken(didToken)
16       if err != nil {
17           fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
18           res.Write([]byte(err.Error()))
19           return
20       }
21
22       // Validate the Token instance before using it
23       if err := tk.Validate(); err != nil {
24           fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
25           return
26       }
27
28       // Get the the user's information
29       userInfo, err := magicSDK.User.GetMetadataByIssuer(tk.GetIssuer())
30       if err != nil {
31           fmt.Fprintf(res, "Error when calling GetMetadataByIssuer: %s", err.Error())
32           return
33       }
34
35       // Use context values to store user's info
36       ctx := context.WithValue(req.Context(), userInfoKey, userInfo)
37       req = req.WithContext(ctx)
38       next(res, req)
39   }
40}

Looks good! Writing checkBearerToken() is the bulk of the work needed to Magic-ally protect the routes for posting or deleting a tweet.

All that’s left to do now is:

  1. Wrap createATweet and deleteATweet handlers in main.go’s handleRequests function with this checkBearerToken middleware.
  2. Update these handlers to get the user’s information from the context values, and then tag each tweet with the user’s email so that authors are able to delete their own tweet.

#handleRequests()

All we did in main.go’s handleRequest() is wrap the deleteATweet and createATweet handlers with the checkBearerToken middleware function.

01func handleRequests() {
02
03   /* REST OF THE CODE IS OMITTED */
04
05   // Delete a tweet ✨
06   myRouter.HandleFunc("/tweet/{id}", checkBearerToken(deleteATweet)).Methods("DELETE")
07
08   // Create a tweet ✨
09   myRouter.HandleFunc("/tweet", checkBearerToken(createATweet)).Methods("POST")
10
11
12   /* REST OF THE CODE IS OMITTED */
13}

#createATweet()

To access the key-value pairs in userInfo object, we first needed to get the object by calling r.Context().Value(userInfoKey) with the userInfoKey we defined earlier, and then we needed to assert two things:

  1. userInfo is not nil
  2. the value stored in userInfo is of type *magic.UserInfo

Both of these assertions are done with userInfo.(*magic.UserInfo).

01// Creates a tweet ✨
02func createATweet(w http.ResponseWriter, r *http.Request) {
03
04   fmt.Println("Endpoint Hit: createATweet")
05
06   // Get the authenticated author's info from context values
07   userInfo := r.Context().Value(userInfoKey)
08   userInfoMap := userInfo.(*magic.UserInfo)
09
10   /*
11       Get the body of our POST request
12       Unmarshal this into a new Tweet struct
13       Add the authenticated author to the tweet
14   */
15   reqBody, _ := ioutil.ReadAll(r.Body)
16   var tweet Tweet
17   json.Unmarshal(reqBody, &tweet)
18   tweet.Author = userInfoMap.Email
19
20   /*
21       Update our global Tweets array to include
22       Our new Tweet
23   */
24   Tweets = append(Tweets, tweet)
25   json.NewEncoder(w).Encode(tweet)
26
27   fmt.Println("Yay! Tweet CREATED.")
28}

As you can see, we’ve also added the authenticated author to the tweet.

#deleteATweet()

Now that we know which author created the tweet, we’ll be able to only allow that author to delete it.

01// Deletes a tweet ✨
02func deleteATweet(w http.ResponseWriter, r *http.Request) {
03   fmt.Println("Endpoint Hit: deleteATweet")
04
05   // Get the authenticated author's info from context values
06   userInfo := r.Context().Value(userInfoKey)
07   userInfoMap := userInfo.(*magic.UserInfo)
08
09   // Parse the path parameters
10   vars := mux.Vars(r)
11   // Extract the `id` of the tweet we wish to delete
12   id := vars["id"]
13
14   // Loop through all our tweets
15   for index, tweet := range Tweets {
16
17       /*
18           Checks whether or not our id and author path
19           parameter matches one of our tweets.
20       */
21       if (tweet.ID == id) && (tweet.Author == userInfoMap.Email) {
22
23           // Updates our Tweets array to remove the tweet
24           Tweets = append(Tweets[:index], Tweets[index+1:]...)
25           w.Write([]byte("Yay! Tweet has been DELETED."))
26           return
27       }
28   }
29
30   w.Write([]byte("Ooh. You can't delete someone else's tweet."))
31}

Yay! Now you know how to protect Go RESTful API routes 🎉. Feel free to create and delete your own tweet, and also try to delete our default tweet in the Postman Collection to test our protected endpoints.

I hope you enjoyed how quick and easy it was to secure the Go-backed Scrappy Twitter API with the Magic Admin SDK for Go. Next time you want to build a Go REST API for authenticated users, this guide will always have your back.

Btw, if you run into any issues, feel free to reach out @seemcat.

Till next time 🙋🏻‍♀️.

Let's make some magic!