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
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:
- POST a tweet (protected): http://localhost:8080/tweet
- GET all tweets (unprotected): http://localhost:8080/tweets
- GET a single tweet (unprotected): http://localhost:8080/tweet/1
- DELETE a tweet (protected): http://localhost:8080/tweet/2
✨ Noteworthy Packages:
- gorilla/handlers: Lets us enable CORS.
- gorilla/mux: Lets us build a powerful HTTP router and URL matcher.
- magic-admin-go/client: Lets us instantiate the Magic Admin SDK for Go.
- magic-admin-go/token: Lets us create a Token instance.
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
- Sign up for an account on Magic.
- Create an app.
- Keep this Magic tab open. You’ll need both of your app’s Publishable and Secret keys soon.
#Server
git clone https://github.com/magiclabs/scrappy-twitter-api-server
cd scrappy-twitter-api-server
mv .env.example .env
- 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;
- Run all
.go
files withgo run .
#Client
git clone https://github.com/magiclabs/scrappy-twitter-api-client
cd scrappy-twitter-api-client
mv .env.local.example .env.local
- 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.
- Install package dependencies:
yarn
- Start the Next.js production server:
yarn dev
#Postman
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)
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).
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:
GET "/tweets" to get all tweets
01myRouter.HandleFunc("/tweets", returnAllTweets)
DELETE "/tweet/{id}" to delete a tweet
01myRouter.HandleFunc("/tweet/{id}", deleteATweet).Methods("DELETE")
GET "/tweet/{i}" to get a single tweet
01myRouter.HandleFunc("/tweet/{id}", returnSingleTweet)
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
:
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}
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}
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}
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
Get the Go Magic Admin SDK package:
go get github.com/magiclabs/magic-admin-go
Configure the Magic Admin SDK in
handlers.go
: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"
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")
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:
- Wrap
createATweet
anddeleteATweet
handlers inmain.go
’shandleRequests
function with thischeckBearerToken
middleware. - 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:
userInfo
is not nil- 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 🙋🏻♀️.