Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Authorization With GraphQL, Golang, and Couchbase NoSQL Using JWT

DZone's Guide to

Authorization With GraphQL, Golang, and Couchbase NoSQL Using JWT

Let's take a look at a tutorial on how to wire up a Couchbase to a fully functional GraphQL powered API that includes authorization with JSON web tokens.

· Database Zone ·
Free Resource

Discover Tarantool's unique features which include powerful stored procedures, SQL support, smart cache, and the speed of 1 million ACID transactions on a single CPU core!

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:

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:

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:

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:

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:

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:

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:

"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:

"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 examples, 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.

Discover Tarantool's unique features such as powerful stored procedures, SQL support, smart cache, and the speed of 1 million ACID transactions on a single CPU.

Topics:
database ,graphql ,tutorial ,jwt ,nosql ,golang ,go

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}