Over the past few months I’ve been writing a GraphQL series using the Go programming language. First we saw how to get started with GraphQL and Go, followed by an alternative way to handle data relationships by using resolvers on GraphQL objects. Going a step further we saw how to include JSON web tokens (JWT) for authorization on GraphQL objects, but without a database.
The logical next step in this GraphQL with Golang journey would be to wire up Couchbase to a fully functional GraphQL powered API that includes authorization with JSON web tokens (JWT). We’re going to see how to handle account creation, JWT validation, and working with live data through GraphQL queries.
Before diving into some design and development, if you haven’t seen my previous tutorials on the subject, you probably should. I wouldn’t recommend getting into the JWT side of things until you have an understanding of using GraphQL with Golang.
Including Couchbase in a GraphQL with JWT Application
Instead of reiterating on the process of creating a GraphQL powered application, we’re going to start from where we left off in the series. The previous JWT tutorial in the series left us with the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
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) } |
Our goal now is to swap out all that mock data with real data that exists in Couchbase. We won’t worry about creating blog data in this tutorial, but if you want to learn about mutations, check out one of the previous tutorials.
The obvious first step towards using dynamic data is to set up our database, Couchbase. Create the following global variable to be used in each of our GraphQL objects:
1 |
var bucket *gocb.Bucket |
With the global Bucket reference created, let’s establish a connection to our Couchbase cluster and open a bucket. This can be done in our project’s main
function:
1 2 3 |
cluster, _ := gocb.Connect("couchbase://localhost") cluster.Authenticate(gocb.PasswordAuthenticator{Username: "example", Password: "123456"}) bucket, _ = cluster.OpenBucket("example", "") |
The above code assumes a locally running cluster and RBAC as well as Bucket information already created and defined. If you haven’t properly configured your Couchbase instance for this application, take a moment to do so.
Since we’re working with a NoSQL database and no longer mock data, our native Go structures need to change slightly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
type User struct { Id string `json:"id,omitempty"` Username string `json:"username"` Password string `json:"password"` Type string `json:"type"` } type Blog struct { Id string `json:"id,omitempty"` Title string `json:"title"` Content string `json:"content"` Author string `json:"author"` Pageviews int32 `json:"pageviews"` Type string `json:"type"` } |
By adding a Type
property, we can write better queries because we can differentiate our data. Changing the Go data structures does not mean we need to update our GraphQL objects. What we expect to return versus what we expect to work with can be different.
In the previous example we were generating our JSON web token with passed information. In reality, we want to generate our JWT with actual account information. To make this possible, we need to create an endpoint for account creation:
1 2 3 4 5 6 7 8 9 10 |
func CreateAccountEndpoint(response http.ResponseWriter, request *http.Request) { response.Header().Set("content-type", "application/json") var account User json.NewDecoder(request.Body).Decode(&account) hash, _ := bcrypt.GenerateFromPassword([]byte(account.Password), 10) account.Password = string(hash) id, _ := uuid.NewV4() bucket.Insert(id.String(), account, 0) response.Write([]byte(`{ "id": "` + id.String() + `" }`)) } |
The above function will take a username and password, hash the password with bcrypt, and insert it into the database. We’ll be querying the database for this account and comparing the hash with a password as a means of authentication. To do this, we should probably update our CreateTokenEndpoint
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
func CreateTokenEndpoint(response http.ResponseWriter, request *http.Request) { response.Header().Set("content-type", "application/json") var user User _ = json.NewDecoder(request.Body).Decode(&user) query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var params []interface{} params = append(params, user.Username) results, _ := bucket.ExecuteN1qlQuery(query, params) var account User results.One(&account) if bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(user.Password)) != nil { response.Write([]byte(`{ "message": "incorrect password" }`)) return } token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "Username": account.Username, }) tokenString, error := token.SignedString(jwtSecret) if error != nil { fmt.Println(error) } response.Write([]byte(`{ "token": "` + tokenString + `" }`)) } |
Notice that instead of taking the passed username and password and creating a JWT from it, we’re doing a database query. If the information doesn’t match what was passed, we’ll return an error, otherwise we’ll continue to create a JWT based on our username.
Assuming that we have a solid way to create accounts and generate JSON web tokens from them, we can begin altering our GraphQL objects to use Couchbase rather than mock data.
Inside the main
function we have a rootQuery
object with a blogs
query as well as an account
query. We’ll be defining our blogs
query first and it would look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
"blogs": &graphql.Field{ Type: graphql.NewList(blogType), Resolve: func(params graphql.ResolveParams) (interface{}, error) { query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'blog'") results, _ := bucket.ExecuteN1qlQuery(query, nil) var result Blog var blogs []Blog for results.Next(&result) { blogs = append(blogs, result) } return blogs, nil }, }, |
Instead of returning a mock list of blog data we are doing a N1QL query and returning the results. The Go data structure is mapped to our GraphQL object.
Even though we’re returning blog data through our N1QL query, the pageviews
property is still protected with JWT as defined in the object.
The final query we have looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
"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 } var user User mapstructure.Decode(account, &user) query := gocb.NewN1qlQuery("SELECT example.* FROM example WHERE type = 'account' AND username = $1") var n1qlParams []interface{} n1qlParams = append(n1qlParams, user.Username) results, _ := bucket.ExecuteN1qlQuery(query, n1qlParams) results.One(&user) return user, nil }, }, |
Notice that we’re retrieving the decoded token information and using it as a parameter in our N1QL query. This is how we can query for a particular account based on the token data, or the currently signed in user.
Try creating some data in the database and see what happens.
Conclusion
We brought our GraphQL series with Go to a close by configuring Couchbase in our JWT authorization example. In reality, adding Couchbase didn’t change any of our JWT example, it just gave us a source of data to be used. If you dig through the previous tutorials in this series, you’ll get a deep dive into GraphQL which includes querying, mutating, and protecting queries as well as pieces of data. All the things you’d expect in a production ready API, but with GraphQL instead of a traditional REST API approach.