Securing a Go-Backed Scrappy Twitter API with Magic
Maricris Bonzo
Posted on January 20, 2021
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.
Table of Contents
- Demo
- A High-Level View
- Getting Started
- The Scrappy Twitter Go Rest API
- Securing the Go Rest API with Magic Admin SDK
Demo
To test out our Live demo, click the button below to import 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.
-
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.
-
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 Test Publishable and Secret keys soon.
Note: Test API keys are always allowed to be used on localhost. For added security, you can specify the URLs that are allowed to use your live API keys in Magic's Dashboard Settings. Doing so will block your Live API keys from working anywhere except the URLs in your whitelisted domains.
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 Test Secret Key and paste it as the value for
MAGIC_TEST_SECRET_KEY
in.env
:
MAGIC_TEST_SECRET_KEY=sk_test_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 Test keys from your Magic app:
NEXT_PUBLIC_MAGIC_TEST_PUBLISHABLE_KEY=pk_test_XXXXX NEXT_PUBLIC_MAGIC_TEST_SECRET_KEY=sk_test_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: -
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:
├── README.md
├── .env
├── main.go
├── structs.go
├── 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.
// Tweet is struct or data type with an Id, Copy and Author
type Tweet struct {
ID string `json:"ID"`
Copy string `json:"Copy"`
Author string `json:"Author"`
}
// Tweets is an array of Tweet structs
var 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
myRouter.HandleFunc("/tweets", returnAllTweets)
-
DELETE "/tweet/{id}" to delete a tweet
myRouter.HandleFunc("/tweet/{id}", deleteATweet).Methods("DELETE")
-
GET "/tweet/{i}" to get a single tweet
myRouter.HandleFunc("/tweet/{id}", returnSingleTweet)
-
POST "/tweet" to create a tweet
myRouter.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()
// Returns ALL tweets ✨ func returnAllTweets(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint Hit: returnAllTweets") json.NewEncoder(w).Encode(Tweets) }
-
DELETE "/tweet/{id}" =>
deleteATweet()
// Deletes a tweet ✨ func deleteATweet(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint Hit: deleteATweet") // Parse the path parameters vars := mux.Vars(r) // Extract the `id` of the tweet we wish to delete id := vars["id"] // Loop through all our tweets for index, tweet := range Tweets { /* Checks whether or not our id path parameter matches one of our tweets. */ if tweet.ID == id { // Updates our Tweets array to remove the tweet Tweets = append(Tweets[:index], Tweets[index+1:]...) } } w.Write([]byte("Yay! Tweet has been DELETED.")) }
-
GET "/tweet/{i}" =>
returnSingleTweet()
// Returns a SINGLE tweet ✨ func returnSingleTweet(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint Hit: returnSingleTweet") vars := mux.Vars(r) key := vars["id"] /* Loop over all of our Tweets If the tweet.Id equals the key we pass in Return the tweet encoded as JSON */ for _, tweet := range Tweets { if tweet.ID == key { json.NewEncoder(w).Encode(tweet) } } }
-
POST "/tweet" =>
createATweet()
// Creates a tweet ✨ func createATweet(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint Hit: createATweet") /* Get the body of our POST request Unmarshal this into a new Tweet struct */ reqBody, _ := ioutil.ReadAll(r.Body) var tweet Tweet json.Unmarshal(reqBody, &tweet) /* Update our global Tweets array to include Our new Tweet */ Tweets = append(Tweets, tweet) json.NewEncoder(w).Encode(tweet) w.Write([]byte("Yay! Tweet CREATED.")) }
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:
"github.com/joho/godotenv" "github.com/magiclabs/magic-admin-go" "github.com/magiclabs/magic-admin-go/client" "github.com/magiclabs/magic-admin-go/token"
-
Load the
.env
file and get the Test Secret Key:
// Load .env file from given path var err = godotenv.Load(".env") // Get env variables var magicSecretKey = os.Getenv("MAGIC_TEST_SECRET_KEY")
-
Instantiate Magic:
var 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:
func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
/* More code is coming! */
next(res, req)
}
}
💁🏻♀️ 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
:
func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
fmt.Println("Middleware Hit: checkBearerToken")
return func(res http.ResponseWriter, req *http.Request) {
// Check whether or not DIDT exists in HTTP Header Request
if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
fmt.Fprintf(res, "Bearer token is required")
return
}
// Retrieve DIDT token from HTTP Header Request
didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
/* More code is coming! */
next(res, req)
}
}
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:
func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
fmt.Println("Middleware Hit: checkBearerToken")
return func(res http.ResponseWriter, req *http.Request) {
// Check whether or not DIDT exists in HTTP Header Request
if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
fmt.Fprintf(res, "Bearer token is required")
return
}
// Retrieve DIDT token from HTTP Header Request
didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
// Create a Token instance to interact with the DID token
tk, err := token.NewToken(didToken)
if err != nil {
fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
res.Write([]byte(err.Error()))
return
}
// Validate the Token instance before using it
if err := tk.Validate(); err != nil {
fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
return
}
/* More code is coming! */
next(res, req)
}
}
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:
func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
fmt.Println("Middleware Hit: checkBearerToken")
return func(res http.ResponseWriter, req *http.Request) {
// Check whether or not DIDT exists in HTTP Header Request
if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
fmt.Fprintf(res, "Bearer token is required")
return
}
// Retrieve DIDT token from HTTP Header Request
didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
// Create a Token instance to interact with the DID token
tk, err := token.NewToken(didToken)
if err != nil {
fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
res.Write([]byte(err.Error()))
return
}
// Validate the Token instance before using it
if err := tk.Validate(); err != nil {
fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
return
}
// Get the user's information
userInfo, err := magicSDK.User.GetMetadataByIssuer(tk.GetIssuer())
if err != nil {
fmt.Fprintf(res, "Error when calling GetMetadataByIssuer: %s", err.Error())
return
}
/* More code is coming! */
next(res, req)
}
}
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):
type key string
const userInfoKey key = "userInfo"
💁🏻♀️ Update checkBearerToken
one last time to use context values to store the user’s information:
func checkBearerToken(next httpHandlerFunc) httpHandlerFunc {
fmt.Println("Middleware Hit: checkBearerToken")
return func(res http.ResponseWriter, req *http.Request) {
// Check whether or not DIDT exists in HTTP Header Request
if !strings.HasPrefix(req.Header.Get("Authorization"), authBearer) {
fmt.Fprintf(res, "Bearer token is required")
return
}
// Retrieve DIDT token from HTTP Header Request
didToken := req.Header.Get("Authorization")[len(authBearer)+1:]
// Create a Token instance to interact with the DID token
tk, err := token.NewToken(didToken)
if err != nil {
fmt.Fprintf(res, "Malformed DID token error: %s", err.Error())
res.Write([]byte(err.Error()))
return
}
// Validate the Token instance before using it
if err := tk.Validate(); err != nil {
fmt.Fprintf(res, "DID token failed validation: %s", err.Error())
return
}
// Get the the user's information
userInfo, err := magicSDK.User.GetMetadataByIssuer(tk.GetIssuer())
if err != nil {
fmt.Fprintf(res, "Error when calling GetMetadataByIssuer: %s", err.Error())
return
}
// Use context values to store user's info
ctx := context.WithValue(req.Context(), userInfoKey, userInfo)
req = req.WithContext(ctx)
next(res, req)
}
}
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.
func handleRequests() {
/* REST OF THE CODE IS OMITTED */
// Delete a tweet ✨
myRouter.HandleFunc("/tweet/{id}", checkBearerToken(deleteATweet)).Methods("DELETE")
// Create a tweet ✨
myRouter.HandleFunc("/tweet", checkBearerToken(createATweet)).Methods("POST")
/* REST OF THE CODE IS OMITTED */
}
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)
.
// Creates a tweet ✨
func createATweet(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: createATweet")
// Get the authenticated author's info from context values
userInfo := r.Context().Value(userInfoKey)
userInfoMap := userInfo.(*magic.UserInfo)
/*
Get the body of our POST request
Unmarshal this into a new Tweet struct
Add the authenticated author to the tweet
*/
reqBody, _ := ioutil.ReadAll(r.Body)
var tweet Tweet
json.Unmarshal(reqBody, &tweet)
tweet.Author = userInfoMap.Email
/*
Update our global Tweets array to include
Our new Tweet
*/
Tweets = append(Tweets, tweet)
json.NewEncoder(w).Encode(tweet)
fmt.Println("Yay! Tweet CREATED.")
}
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.
// Deletes a tweet ✨
func deleteATweet(w http.ResponseWriter, r *http.Request) {
fmt.Println("Endpoint Hit: deleteATweet")
// Get the authenticated author's info from context values
userInfo := r.Context().Value(userInfoKey)
userInfoMap := userInfo.(*magic.UserInfo)
// Parse the path parameters
vars := mux.Vars(r)
// Extract the `id` of the tweet we wish to delete
id := vars["id"]
// Loop through all our tweets
for index, tweet := range Tweets {
/*
Checks whether or not our id and author path
parameter matches one of our tweets.
*/
if (tweet.ID == id) && (tweet.Author == userInfoMap.Email) {
// Updates our Tweets array to remove the tweet
Tweets = append(Tweets[:index], Tweets[index+1:]...)
w.Write([]byte("Yay! Tweet has been DELETED."))
return
}
}
w.Write([]byte("Ooh. You can't delete someone else's tweet."))
}
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 🙋🏻♀️.
Posted on January 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.