If you’ve been keeping up, you’ll remember I released a very popular tutorial titled, Getting Started with GraphQL Using Golang which was more or less a quick-start to using GraphQL in your web applications. Since then, I demonstrated an alternative way to work with related data in a tutorial titled, Maintain Data Relationships Through Resolvers with GraphQL in a Golang Application. Both articles are great, but they left out an important feature that most modern APIs must have. Most modern APIs must have a way to authorize particular users to access only certain pieces of data and not all data offered by the service.
One of the most popular ways to enforce some kind of authorization in an application is through the use of JSON web tokens (JWT). Users authenticate with a service and the service responds with a JWT to be used in every future request so that way the password is kept safe. The service can then validate the JWT to make sure it is correct and not expired.
We’re going to see how to protect particular GraphQL properties as well as entire queries using JSON web tokens and the Go programming language.
Before we get too invested in this tutorial, you should take a step back and view my getting started tutorial as well as my data relationships tutorial. The requirement is that you have somewhat of an understanding of how GraphQL works before proceeding.
I’ve been creating RESTful APIs much longer than I’ve been creating GraphQL APIs. A while back I wrote about using JWT in a RESTful API created with Golang and while different, much of the logic will be carried over into this tutorial.
Before we start adding code to the project, let’s create the project and download any project dependencies that will be used. Somewhere within your $GOPATH, create a project directory and a main.go file. We’re going to store all of our project code within this main.go file.
With the project created, execute the following from the command line:
go get github.com/dgrijalva/jwt-go
go get github.com/graphql-go/graphql
go get github.com/mitchellh/mapstructure
The mapstructure
package will allow us to easily convert map
variables into native Go data structures. While not absolutely necessary, the mapstructure
package will make our lives a little easier. The graphql
package will allow us to create GraphQL objects and process queries while the jwt-go
package will allow us to sign and validate JSON web tokens.
If you want to learn more about the mapstructure
package, check out my previous tutorial titled, Decode Map Values into Native Golang Structures.
Now that we have our project created and the appropriate dependencies in hand, let’s start adding some code. We’re going to start by adding our boilerplate code and the logic for creating and validating JSON web tokens.
Open your project’s main.go file and include the following code:
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
jwt "github.com/dgrijalva/jwt-go"
"github.com/graphql-go/graphql"
"github.com/mitchellh/mapstructure"
)
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Blog struct {
Id string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Pageviews int32 `json:"pageviews"`
}
var jwtSecret []byte = []byte("thepolyglotdeveloper")
var accountsMock []User = []User{
User{
Id: "1",
Username: "nraboy",
Password: "1234",
},
User{
Id: "2",
Username: "mraboy",
Password: "5678",
},
}
var blogsMock []Blog = []Blog{
Blog{
Id: "1",
Author: "nraboy",
Title: "Sample Article",
Content: "This is a sample article written by Nic Raboy",
Pageviews: 1000,
},
}
func ValidateJWT(t string) (interface{}, error) { }
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) { }
func main() {
fmt.Println("Starting the application at :12345...")
http.HandleFunc("/login", CreateTokenEndpoint)
http.ListenAndServe(":12345", nil)
}
Even though most of the above is a foundation to the core of our project, we should probably still take a moment to break it down, starting with our data model:
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Blog struct {
Id string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Pageviews int32 `json:"pageviews"`
}
This particular example will use accounts and blogs. Each account can have any number of blogs and each blog will be tied to account. However, we’re not going to be demonstrating a relationship. Instead, accounts will be protected via a JWT and only the Pageviews
property of a blog will be protected by a JWT.
When signing and validating a JWT, we need to be using a secret:
var jwtSecret []byte = []byte("thepolyglotdeveloper")
In reality the value should not be hard coded into our application because it is considered sensitive like a password, but for this example it is fine.
We’re not using a database in this example so hard coding some mock data will work out fine. We just need a few accounts and a few blogs to get a good idea about what is happening.
Now for the part that truly matters for this particular example. We need a way to create and validate JWT data. Even though this is a GraphQL API, we’re still going to have a RESTful endpoint for signing users in and returning a JWT.
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) {
var user User
_ = json.NewDecoder(request.Body).Decode(&user)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": user.Username,
"password": user.Password,
})
tokenString, error := token.SignedString(jwtSecret)
if error != nil {
fmt.Println(error)
}
response.Header().Set("content-type", "application/json")
response.Write([]byte(`{ "token": "` + tokenString + `" }`))
}
Remember, we don’t have a database for this example. What we’re doing is taking a username and password sent via the HTTP request and creating a JWT token from it. Then we sign this token using our secret and return the signed token back as a response. Had this been using a database, we probably would have added authentication logic for a particular user. If you really wanted to be production ready, you would not include the password in the token and you’d probably add an expiration time that you check when you validate. We’re not going to worry about any of this for our example.
The CreateTokenEndpoint
is an endpoint function that will be accessible via an HTTP request. Our validation function will be a little different.
func ValidateJWT(t string) (interface{}, error) {
if t == "" {
return nil, errors.New("Authorization token must be present")
}
token, _ := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("There was an error")
}
return jwtSecret, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var decodedToken interface{}
mapstructure.Decode(claims, &decodedToken)
return decodedToken, nil
} else {
return nil, errors.New("Invalid authorization token")
}
}
Our ValidateJWT
function is not an endpoint function. Instead we’re going to call it as we need it. If no token is present, we’re going to respond with an error, otherwise we’re going to parse the token using our secret. If the token is valid we’re going to decode it and return the decoded value, otherwise we’re going to return an error.
The CreateTokenEndpoint
is wired up in our main
function like so:
http.HandleFunc("/login", CreateTokenEndpoint)
http.ListenAndServe(":12345", nil)
There wasn’t much to the JWT signing and validation process. If we had a database things wouldn’t really be any more complex than what we saw. Most of our complexity resides in the GraphQL piece which comes next.
We’ve got our JWT logic and we’ve got our mock data in place. Now we need to define our GraphQL objects, queries, and schema which power the actual API. Take a look at the following GraphQL objects for our mock data:
var accountType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{
Name: "Account",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.String,
},
"username": &graphql.Field{
Type: graphql.String,
},
"password": &graphql.Field{
Type: graphql.String,
},
},
})
var blogType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{
Name: "Blog",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.String,
},
"title": &graphql.Field{
Type: graphql.String,
},
"content": &graphql.Field{
Type: graphql.String,
},
"author": &graphql.Field{
Type: graphql.String,
},
"pageviews": &graphql.Field{
Type: graphql.Int,
},
},
})
Nothing too complex right? We’ve essentially converted our native Go data structure to a GraphQL data structure of the same design. Aren’t we supposed to make our pageviews
field protected? Yes we are, and it can be done like the following:
"pageviews": &graphql.Field{
Type: graphql.Int,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
_, err := ValidateJWT(params.Context.Value("token").(string))
if err != nil {
return nil, err
}
return params.Source.(Blog).Pageviews, nil
},
},
We’ve added a Resolve
function for the particular property so we can define logic for the property. When the field is included in a query we call the ValidateJWT
token using a value that we’ll eventually pass into our Context
. If this token is present and valid, then return the value that already existed for this particular field, otherwise an error will be returned.
Before we get into the context side of things, let’s look at the possible queries.
Inside the main
function we might have the following:
rootQuery := graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"account": &graphql.Field{
Type: accountType,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
account, err := ValidateJWT(params.Context.Value("token").(string))
if err != nil {
return nil, err
}
for _, accountMock := range accountsMock {
if accountMock.Username == account.(User).Username {
return accountMock, nil
}
}
return &User{}, nil
},
},
"blogs": &graphql.Field{
Type: graphql.NewList(blogType),
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
return blogsMock, nil
},
},
},
})
The above says that we have an account
query and a blogs
query. In the account
query we run our ValidateJWT
function to check for a valid token. A valid token is required to run this query at all. If we have a valid token we check the token user against the mock data. We do this because we only want account data for the signed in user, not other users.
The blogs
query is a bit different. We have no restrictions on the query itself, only if the user tries to query for the pageviews
field.
We’d want to wire up a schema using the queries like so:
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
Query: rootQuery,
})
Finally, to parse our GraphQL queries we’d configure our endpoint like the following:
http.HandleFunc("/graphql", func(response http.ResponseWriter, request *http.Request) {
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: request.URL.Query().Get("query"),
Context: context.WithValue(context.Background(), "token", request.URL.Query().Get("token")),
})
json.NewEncoder(response).Encode(result)
})
Remember the context I mentioned earlier? We can define a value to pass other than the query itself using the Context
property. In this case we’d be collecting a token from the query parameters. We could easily collect this token from the header information of the request as well.
You may or may not be confused by everything we just saw. Don’t worry it took me a while to figure things out as well. Now we want to test what we’ve done to validate that everything works as expected.
After running the application, execute the following in your Terminal:
curl -g 'http://localhost:12345/graphql?query={blogs{title,content,author}}'
If everything went well, you should have received an array response with the three fields you requested. However, try to execute the following command instead:
curl -g 'http://localhost:12345/graphql?query={blogs{title,pageviews}}'
As a result, you should see something that looks like the following:
{
"data": {
"blogs": [
{
"pageviews": null,
"title": "Sample Article"
}
]
},
"errors": [
{
"message": "Authorization token must be present",
"locations": []
}
]
}
We didn’t get any pageviews back because we didn’t provide a valid JWT. We can get around this by providing a valid token in our request:
curl -g 'http://localhost:12345/graphql?query={blogs{title,pageviews}}&token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXNzd29yZCI6IjEyMzQiLCJ1c2VybmFtZSI6Im5yYWJveSJ9.GfqcK2g6J5rFtRCP8QKaEpeZJxn2ouvkBu_Hrt3qQ34'
As a result, we would probably end up with a valid response like the following:
{
"data": {
"blogs": [
{
"pageviews": 1000,
"title": "Sample Article"
}
]
}
}
Pretty awesome right? Alright, let’s test our other endpoint with a cURL request. Go ahead and execute the following:
curl -g 'http://localhost:12345/graphql?query={account{username}}'
I bet the query came back with an error, right? Try including a valid token and see what happens.
Try to mix and match your queries and see what you get. The pageviews should always be restricted to a valid token and the account should always be restricted to a currently logged in account.
In case I was a bit confusing throughout the article, I’d like to provide the completed application source code:
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
jwt "github.com/dgrijalva/jwt-go"
"github.com/graphql-go/graphql"
"github.com/mitchellh/mapstructure"
)
type User struct {
Id string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}
type Blog struct {
Id string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
Pageviews int32 `json:"pageviews"`
}
var jwtSecret []byte = []byte("thepolyglotdeveloper")
var accountsMock []User = []User{
User{
Id: "1",
Username: "nraboy",
Password: "1234",
},
User{
Id: "2",
Username: "mraboy",
Password: "5678",
},
}
var blogsMock []Blog = []Blog{
Blog{
Id: "1",
Author: "nraboy",
Title: "Sample Article",
Content: "This is a sample article written by Nic Raboy",
Pageviews: 1000,
},
}
var accountType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{
Name: "Account",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.String,
},
"username": &graphql.Field{
Type: graphql.String,
},
"password": &graphql.Field{
Type: graphql.String,
},
},
})
var blogType *graphql.Object = graphql.NewObject(graphql.ObjectConfig{
Name: "Blog",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.String,
},
"title": &graphql.Field{
Type: graphql.String,
},
"content": &graphql.Field{
Type: graphql.String,
},
"author": &graphql.Field{
Type: graphql.String,
},
"pageviews": &graphql.Field{
Type: graphql.Int,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
_, err := ValidateJWT(params.Context.Value("token").(string))
if err != nil {
return nil, err
}
return params.Source.(Blog).Pageviews, nil
},
},
},
})
func ValidateJWT(t string) (interface{}, error) {
if t == "" {
return nil, errors.New("Authorization token must be present")
}
token, _ := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("There was an error")
}
return jwtSecret, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
var decodedToken interface{}
mapstructure.Decode(claims, &decodedToken)
return decodedToken, nil
} else {
return nil, errors.New("Invalid authorization token")
}
}
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) {
var user User
_ = json.NewDecoder(request.Body).Decode(&user)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": user.Username,
"password": user.Password,
})
tokenString, error := token.SignedString(jwtSecret)
if error != nil {
fmt.Println(error)
}
response.Header().Set("content-type", "application/json")
response.Write([]byte(`{ "token": "` + tokenString + `" }`))
}
func main() {
fmt.Println("Starting the application at :12345...")
rootQuery := graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"account": &graphql.Field{
Type: accountType,
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
account, err := ValidateJWT(params.Context.Value("token").(string))
if err != nil {
return nil, err
}
for _, accountMock := range accountsMock {
if accountMock.Username == account.(User).Username {
return accountMock, nil
}
}
return &User{}, nil
},
},
"blogs": &graphql.Field{
Type: graphql.NewList(blogType),
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
return blogsMock, nil
},
},
},
})
schema, _ := graphql.NewSchema(graphql.SchemaConfig{
Query: rootQuery,
})
http.HandleFunc("/graphql", func(response http.ResponseWriter, request *http.Request) {
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: request.URL.Query().Get("query"),
Context: context.WithValue(context.Background(), "token", request.URL.Query().Get("token")),
})
json.NewEncoder(response).Encode(result)
})
http.HandleFunc("/login", CreateTokenEndpoint)
http.ListenAndServe(":12345", nil)
}
Just make sure that the code ends up in a new project file within your $GOPATH path. It is by no means the only solution to the authorization problem or even the best solution, but it is one that I’ve found to be easy to implement and it works well.
You just saw how to authorize users to obtain certain data fields or execute certain queries with GraphQL using JSON web tokens (JWT) in the Go programming language. Using JWT with GraphQL is an alternative to the RESTful API approach that I had previously demonstrated.
If you’re just getting started with GraphQL, this JWT stuff might be over your head. Take a step back and check out my getting started guide and then circle back.